@pellux/goodvibes-agent 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.51 - 2026-05-31
6
+
7
+ - 6a8e8a6 Add local library workspace editors
8
+
9
+ ## 0.1.50 - 2026-05-31
10
+
11
+ - bdb654a Improve local library workspaces
12
+
5
13
  ## 0.1.49 - 2026-05-31
6
14
 
7
15
  - 445e694 Show isolated Agent Knowledge in TUI panel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
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,7 +20,26 @@ export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
20
20
 
21
21
  export type AgentWorkspaceFocusPane = 'categories' | 'actions';
22
22
 
23
- export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace';
23
+ export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'editor';
24
+
25
+ export type AgentWorkspaceLocalEditorKind = 'persona' | 'skill' | 'routine';
26
+
27
+ export interface AgentWorkspaceEditorField {
28
+ readonly id: string;
29
+ readonly label: string;
30
+ readonly value: string;
31
+ readonly required: boolean;
32
+ readonly multiline: boolean;
33
+ readonly hint: string;
34
+ }
35
+
36
+ export interface AgentWorkspaceLocalEditor {
37
+ readonly kind: AgentWorkspaceLocalEditorKind;
38
+ readonly title: string;
39
+ readonly fields: readonly AgentWorkspaceEditorField[];
40
+ readonly selectedFieldIndex: number;
41
+ readonly message: string;
42
+ }
24
43
 
25
44
  export interface AgentWorkspaceAction {
26
45
  readonly id: string;
@@ -28,6 +47,7 @@ export interface AgentWorkspaceAction {
28
47
  readonly detail: string;
29
48
  readonly command?: string;
30
49
  readonly targetCategoryId?: string;
50
+ readonly editorKind?: AgentWorkspaceLocalEditorKind;
31
51
  readonly kind: AgentWorkspaceActionKind;
32
52
  readonly safety: 'safe' | 'read-only' | 'delegates' | 'blocked';
33
53
  }
@@ -53,6 +73,19 @@ export interface AgentWorkspaceActionResult {
53
73
  readonly safety?: AgentWorkspaceAction['safety'];
54
74
  }
55
75
 
76
+ export interface AgentWorkspaceLocalLibraryItem {
77
+ readonly id: string;
78
+ readonly name: string;
79
+ readonly description: string;
80
+ readonly reviewState: string;
81
+ readonly source: string;
82
+ readonly tags: readonly string[];
83
+ readonly triggers: readonly string[];
84
+ readonly active?: boolean;
85
+ readonly enabled?: boolean;
86
+ readonly startCount?: number;
87
+ }
88
+
56
89
  type AgentWorkspaceConfigReader = {
57
90
  get(key: string): unknown;
58
91
  };
@@ -69,10 +102,13 @@ export interface AgentWorkspaceRuntimeSnapshot {
69
102
  readonly sessionMemoryCount: number;
70
103
  readonly localRoutineCount: number;
71
104
  readonly enabledRoutineCount: number;
105
+ readonly localRoutines: readonly AgentWorkspaceLocalLibraryItem[];
72
106
  readonly localSkillCount: number;
73
107
  readonly enabledSkillCount: number;
108
+ readonly localSkills: readonly AgentWorkspaceLocalLibraryItem[];
74
109
  readonly localPersonaCount: number;
75
110
  readonly activePersonaName: string;
111
+ readonly localPersonas: readonly AgentWorkspaceLocalLibraryItem[];
76
112
  readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
77
113
  readonly knowledgeIsolation: 'agent-only';
78
114
  readonly executionPolicy: 'serial-proactive';
@@ -143,6 +179,46 @@ function inferActiveRuntimeProfile(homeDirectory: string): string {
143
179
  return homeDirectory.includes(marker) ? basename(homeDirectory) : '(default home)';
144
180
  }
145
181
 
182
+ function summarizePersonaItem(persona: AgentPersonaRecord, activePersonaId: string | null): AgentWorkspaceLocalLibraryItem {
183
+ return {
184
+ id: persona.id,
185
+ name: persona.name,
186
+ description: persona.description,
187
+ reviewState: persona.reviewState,
188
+ source: persona.source,
189
+ tags: persona.tags,
190
+ triggers: persona.triggers,
191
+ active: persona.id === activePersonaId,
192
+ };
193
+ }
194
+
195
+ function summarizeSkillItem(skill: AgentSkillRecord): AgentWorkspaceLocalLibraryItem {
196
+ return {
197
+ id: skill.id,
198
+ name: skill.name,
199
+ description: skill.description,
200
+ reviewState: skill.reviewState,
201
+ source: skill.source,
202
+ tags: skill.tags,
203
+ triggers: skill.triggers,
204
+ enabled: skill.enabled,
205
+ };
206
+ }
207
+
208
+ function summarizeRoutineItem(routine: AgentRoutineRecord): AgentWorkspaceLocalLibraryItem {
209
+ return {
210
+ id: routine.id,
211
+ name: routine.name,
212
+ description: routine.description,
213
+ reviewState: routine.reviewState,
214
+ source: routine.source,
215
+ tags: routine.tags,
216
+ triggers: routine.triggers,
217
+ enabled: routine.enabled,
218
+ startCount: routine.startCount,
219
+ };
220
+ }
221
+
146
222
  export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): AgentWorkspaceRuntimeSnapshot {
147
223
  const host = readConfigString(context, 'controlPlane.host', '127.0.0.1');
148
224
  const port = readConfigNumber(context, 'controlPlane.port', 3421);
@@ -165,31 +241,43 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
165
241
  const personaSnapshot = (() => {
166
242
  try {
167
243
  const shellPaths = context.workspace?.shellPaths;
168
- if (!shellPaths) return { count: 0, activeName: '(none)' };
244
+ if (!shellPaths) return { count: 0, activeName: '(none)', items: [] };
169
245
  const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
170
- return { count: snapshot.personas.length, activeName: snapshot.activePersona?.name ?? '(none)' };
246
+ return {
247
+ count: snapshot.personas.length,
248
+ activeName: snapshot.activePersona?.name ?? '(none)',
249
+ items: snapshot.personas.map((persona) => summarizePersonaItem(persona, snapshot.activePersonaId)),
250
+ };
171
251
  } catch {
172
- return { count: 0, activeName: '(unavailable)' };
252
+ return { count: 0, activeName: '(unavailable)', items: [] };
173
253
  }
174
254
  })();
175
255
  const skillSnapshot = (() => {
176
256
  try {
177
257
  const shellPaths = context.workspace?.shellPaths;
178
- if (!shellPaths) return { count: 0, enabled: 0 };
258
+ if (!shellPaths) return { count: 0, enabled: 0, items: [] };
179
259
  const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
180
- return { count: snapshot.skills.length, enabled: snapshot.enabledSkills.length };
260
+ return {
261
+ count: snapshot.skills.length,
262
+ enabled: snapshot.enabledSkills.length,
263
+ items: snapshot.skills.map(summarizeSkillItem),
264
+ };
181
265
  } catch {
182
- return { count: 0, enabled: 0 };
266
+ return { count: 0, enabled: 0, items: [] };
183
267
  }
184
268
  })();
185
269
  const routineSnapshot = (() => {
186
270
  try {
187
271
  const shellPaths = context.workspace?.shellPaths;
188
- if (!shellPaths) return { count: 0, enabled: 0 };
272
+ if (!shellPaths) return { count: 0, enabled: 0, items: [] };
189
273
  const snapshot = AgentRoutineRegistry.fromShellPaths(shellPaths).snapshot();
190
- return { count: snapshot.routines.length, enabled: snapshot.enabledRoutines.length };
274
+ return {
275
+ count: snapshot.routines.length,
276
+ enabled: snapshot.enabledRoutines.length,
277
+ items: snapshot.routines.map(summarizeRoutineItem),
278
+ };
191
279
  } catch {
192
- return { count: 0, enabled: 0 };
280
+ return { count: 0, enabled: 0, items: [] };
193
281
  }
194
282
  })();
195
283
  const runtimeProfiles = (() => {
@@ -265,10 +353,13 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
265
353
  sessionMemoryCount,
266
354
  localRoutineCount: routineSnapshot.count,
267
355
  enabledRoutineCount: routineSnapshot.enabled,
356
+ localRoutines: routineSnapshot.items,
268
357
  localSkillCount: skillSnapshot.count,
269
358
  enabledSkillCount: skillSnapshot.enabled,
359
+ localSkills: skillSnapshot.items,
270
360
  localPersonaCount: personaSnapshot.count,
271
361
  activePersonaName: personaSnapshot.activeName,
362
+ localPersonas: personaSnapshot.items,
272
363
  knowledgeRoute: '/api/goodvibes-agent/knowledge',
273
364
  knowledgeIsolation: 'agent-only',
274
365
  executionPolicy: 'serial-proactive',
@@ -329,9 +420,9 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
329
420
  { id: 'setup-provider-model', label: 'Provider and model', detail: 'Choose the provider/model route for normal assistant chat.', command: '/model', kind: 'command', safety: 'safe' },
330
421
  { 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' },
331
422
  { 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' },
332
- { id: 'setup-personas', label: 'Personas', detail: 'Create or select the active local Agent persona.', command: '/personas', kind: 'command', safety: 'safe' },
333
- { id: 'setup-skills', label: 'Skills', detail: 'Create, review, and enable reusable local Agent skills.', command: '/agent-skills', kind: 'command', safety: 'safe' },
334
- { id: 'setup-routines', label: 'Routines', detail: 'Create, review, and enable local Agent routines before any explicit schedule promotion.', command: '/routines', kind: 'command', safety: 'safe' },
423
+ { id: 'setup-personas', label: 'Personas', detail: 'Create or select the active local Agent persona.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
424
+ { id: 'setup-skills', label: 'Skills', detail: 'Create, review, and enable reusable local Agent skills.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
425
+ { id: 'setup-routines', label: 'Routines', detail: 'Create, review, and enable local Agent routines before any explicit schedule promotion.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
335
426
  { id: 'setup-memory', label: 'Local memory', detail: 'Inspect local/session memory; secrets stay rejected or redacted.', command: '/memory', kind: 'command', safety: 'read-only' },
336
427
  { id: 'setup-channels', label: 'Channels', detail: 'Open companion pairing and channel readiness setup.', command: '/pair', kind: 'command', safety: 'safe' },
337
428
  { 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' },
@@ -410,9 +501,53 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
410
501
  detail: 'Memory, routines, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
411
502
  actions: [
412
503
  { id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
413
- { id: 'routines', label: 'Routine library', detail: 'Create, review, enable, and start local Agent routines in the main conversation.', command: '/routines', kind: 'command', safety: 'safe' },
414
- { id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
415
- { 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' },
504
+ { id: 'personas', label: 'Persona library', detail: 'Open the local persona workspace for active role selection and review.', targetCategoryId: 'personas', kind: 'workspace', safety: 'safe' },
505
+ { id: 'skills', label: 'Local skill library', detail: 'Open the local skill workspace for reusable procedures and review.', targetCategoryId: 'skills', kind: 'workspace', safety: 'safe' },
506
+ { id: 'routines', label: 'Routine library', detail: 'Open the local routine workspace for repeatable workflows and schedule promotion review.', targetCategoryId: 'routines', kind: 'workspace', safety: 'safe' },
507
+ ],
508
+ },
509
+ {
510
+ id: 'personas',
511
+ group: 'LEARN',
512
+ label: 'Personas',
513
+ summary: 'Local behavior profiles for the main assistant.',
514
+ detail: 'Personas shape the serial Agent in the main conversation. They are not background agents and they never spawn specialist roots.',
515
+ actions: [
516
+ { id: 'personas-list', label: 'List personas', detail: 'Print the full local persona library.', command: '/personas list', kind: 'command', safety: 'read-only' },
517
+ { 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' },
518
+ { id: 'personas-create', label: 'Create persona', detail: 'Open an in-workspace form for a local persona. No placeholder command is dispatched.', editorKind: 'persona', kind: 'editor', safety: 'safe' },
519
+ { id: 'personas-use', label: 'Use persona', detail: 'Activate a local persona by id or name.', command: '/personas use <id>', kind: 'command', safety: 'safe' },
520
+ { id: 'personas-review', label: 'Review persona', detail: 'Mark a local persona reviewed after inspecting it.', command: '/personas review <id>', kind: 'command', safety: 'safe' },
521
+ { 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' },
522
+ ],
523
+ },
524
+ {
525
+ id: 'skills',
526
+ group: 'LEARN',
527
+ label: 'Skills',
528
+ summary: 'Reusable local procedures the assistant can apply on demand.',
529
+ detail: 'Skills are local, reviewable procedures. Enabled skills inform the main conversation; secret-looking content is rejected.',
530
+ actions: [
531
+ { id: 'skills-list', label: 'List skills', detail: 'Print the full local Agent skill library.', command: '/agent-skills list', kind: 'command', safety: 'read-only' },
532
+ { id: 'skills-enabled', label: 'Enabled skills', detail: 'Show only skills currently injected into Agent guidance.', command: '/agent-skills enabled', kind: 'command', safety: 'read-only' },
533
+ { id: 'skills-create', label: 'Create skill', detail: 'Open an in-workspace form for a reusable local procedure. No placeholder command is dispatched.', editorKind: 'skill', kind: 'editor', safety: 'safe' },
534
+ { 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' },
535
+ { id: 'skills-review', label: 'Review skill', detail: 'Mark a local skill reviewed after inspecting it.', command: '/agent-skills review <id>', kind: 'command', safety: 'safe' },
536
+ ],
537
+ },
538
+ {
539
+ id: 'routines',
540
+ group: 'LEARN',
541
+ label: 'Routines',
542
+ summary: 'Repeatable workflows for the main conversation.',
543
+ detail: 'Routines run in the main conversation by default. Promotion to an external daemon schedule requires a real schedule command and --yes.',
544
+ actions: [
545
+ { id: 'routines-list', label: 'List routines', detail: 'Print the full local Agent routine library.', command: '/routines list', kind: 'command', safety: 'read-only' },
546
+ { id: 'routines-enabled', label: 'Enabled routines', detail: 'Show routines available for direct use.', command: '/routines enabled', kind: 'command', safety: 'read-only' },
547
+ { id: 'routines-create', label: 'Create routine', detail: 'Open an in-workspace form for a repeatable local workflow. No placeholder command is dispatched.', editorKind: 'routine', kind: 'editor', safety: 'safe' },
548
+ { 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' },
549
+ { 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' },
550
+ { id: 'routines-receipts', label: 'Promotion receipts', detail: 'Inspect local redacted routine schedule promotion receipts.', command: '/routines receipts', kind: 'command', safety: 'read-only' },
416
551
  ],
417
552
  },
418
553
  {
@@ -463,6 +598,70 @@ function parseCommand(command: string): { readonly name: string; readonly args:
463
598
  return { name: parts[0] ?? '', args: parts.slice(1) };
464
599
  }
465
600
 
601
+ function createLocalEditor(kind: AgentWorkspaceLocalEditorKind): AgentWorkspaceLocalEditor {
602
+ if (kind === 'persona') {
603
+ return {
604
+ kind,
605
+ title: 'Create Persona',
606
+ selectedFieldIndex: 0,
607
+ message: 'Enter a local behavior profile for the serial main-conversation assistant.',
608
+ fields: [
609
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short persona name.' },
610
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of when to use it.' },
611
+ { id: 'body', label: 'Instructions', value: '', required: true, multiline: true, hint: 'Operating guidance. Ctrl-J inserts a new line.' },
612
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
613
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this persona.' },
614
+ { id: 'activate', label: 'Activate now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
615
+ ],
616
+ };
617
+ }
618
+ if (kind === 'skill') {
619
+ return {
620
+ kind,
621
+ title: 'Create Skill',
622
+ selectedFieldIndex: 0,
623
+ message: 'Enter a reusable local procedure the assistant can apply from the main conversation.',
624
+ fields: [
625
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short skill name.' },
626
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of the procedure.' },
627
+ { id: 'procedure', label: 'Procedure', value: '', required: true, multiline: true, hint: 'Reusable steps. Ctrl-J inserts a new line.' },
628
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this skill.' },
629
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
630
+ { id: 'enabled', label: 'Enable now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
631
+ ],
632
+ };
633
+ }
634
+ return {
635
+ kind,
636
+ title: 'Create Routine',
637
+ selectedFieldIndex: 0,
638
+ message: 'Enter a repeatable workflow. It runs in the main conversation unless explicitly promoted to a daemon schedule.',
639
+ fields: [
640
+ { id: 'name', label: 'Name', value: '', required: true, multiline: false, hint: 'Short routine name.' },
641
+ { id: 'description', label: 'Description', value: '', required: true, multiline: false, hint: 'One-line summary of the workflow.' },
642
+ { id: 'steps', label: 'Steps', value: '', required: true, multiline: true, hint: 'Workflow steps. Ctrl-J inserts a new line.' },
643
+ { id: 'triggers', label: 'Triggers', value: '', required: false, multiline: false, hint: 'Comma-separated words that suggest this routine.' },
644
+ { id: 'tags', label: 'Tags', value: '', required: false, multiline: false, hint: 'Comma-separated optional tags.' },
645
+ { id: 'enabled', label: 'Enable now', value: 'yes', required: false, multiline: false, hint: 'yes/no.' },
646
+ ],
647
+ };
648
+ }
649
+
650
+ function splitList(value: string): string[] {
651
+ return value.split(',').map((part) => part.trim()).filter(Boolean);
652
+ }
653
+
654
+ function isAffirmative(value: string): boolean {
655
+ const normalized = value.trim().toLowerCase();
656
+ return normalized === '' || normalized === 'yes' || normalized === 'y' || normalized === 'true' || normalized === 'enabled' || normalized === 'on';
657
+ }
658
+
659
+ function editorCategoryId(kind: AgentWorkspaceLocalEditorKind): string {
660
+ if (kind === 'persona') return 'personas';
661
+ if (kind === 'skill') return 'skills';
662
+ return 'routines';
663
+ }
664
+
466
665
  export class AgentWorkspace {
467
666
  public active = false;
468
667
  public focusPane: AgentWorkspaceFocusPane = 'actions';
@@ -471,6 +670,7 @@ export class AgentWorkspace {
471
670
  public status = 'Ready. Choose an operator flow; ordinary assistant work stays in the main conversation.';
472
671
  public runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null = null;
473
672
  public lastActionResult: AgentWorkspaceActionResult | null = null;
673
+ public localEditor: AgentWorkspaceLocalEditor | null = null;
474
674
  private context: CommandContext | null = null;
475
675
  private dispatchCommand: AgentWorkspaceCommandDispatcher | null = null;
476
676
 
@@ -482,6 +682,7 @@ export class AgentWorkspace {
482
682
  this.focusPane = 'actions';
483
683
  this.status = 'Ready. Choose an operator flow; ordinary assistant work stays in the main conversation.';
484
684
  this.lastActionResult = null;
685
+ this.localEditor = null;
485
686
  this.clampSelection();
486
687
  }
487
688
 
@@ -492,6 +693,7 @@ export class AgentWorkspace {
492
693
 
493
694
  close(): void {
494
695
  this.active = false;
696
+ this.localEditor = null;
495
697
  }
496
698
 
497
699
  get categories(): readonly AgentWorkspaceCategory[] {
@@ -573,13 +775,86 @@ export class AgentWorkspace {
573
775
  };
574
776
  }
575
777
 
778
+ cancelLocalEditor(): void {
779
+ if (!this.localEditor) return;
780
+ const title = this.localEditor.title;
781
+ this.localEditor = null;
782
+ this.status = `${title} cancelled.`;
783
+ this.lastActionResult = {
784
+ kind: 'guidance',
785
+ title: `${title} cancelled`,
786
+ detail: 'No local Agent registry changes were written.',
787
+ };
788
+ }
789
+
790
+ moveEditorField(delta: number): void {
791
+ const editor = this.localEditor;
792
+ if (!editor) return;
793
+ const nextIndex = Math.max(0, Math.min(editor.fields.length - 1, editor.selectedFieldIndex + delta));
794
+ this.localEditor = { ...editor, selectedFieldIndex: nextIndex };
795
+ }
796
+
797
+ appendEditorText(text: string): void {
798
+ const editor = this.localEditor;
799
+ if (!editor || text.length === 0) return;
800
+ const field = editor.fields[editor.selectedFieldIndex];
801
+ if (!field) return;
802
+ this.replaceEditorField(editor.selectedFieldIndex, `${field.value}${text}`, editor.message);
803
+ }
804
+
805
+ appendEditorNewline(): void {
806
+ const editor = this.localEditor;
807
+ if (!editor) return;
808
+ const field = editor.fields[editor.selectedFieldIndex];
809
+ if (!field || !field.multiline) {
810
+ this.moveEditorField(1);
811
+ return;
812
+ }
813
+ this.replaceEditorField(editor.selectedFieldIndex, `${field.value}\n`, editor.message);
814
+ }
815
+
816
+ editorBackspace(): void {
817
+ const editor = this.localEditor;
818
+ if (!editor) return;
819
+ const field = editor.fields[editor.selectedFieldIndex];
820
+ if (!field || field.value.length === 0) return;
821
+ const characters = Array.from(field.value);
822
+ characters.pop();
823
+ this.replaceEditorField(editor.selectedFieldIndex, characters.join(''), editor.message);
824
+ }
825
+
826
+ submitEditorFieldOrForm(): void {
827
+ const editor = this.localEditor;
828
+ if (!editor) return;
829
+ if (editor.selectedFieldIndex < editor.fields.length - 1) {
830
+ this.moveEditorField(1);
831
+ return;
832
+ }
833
+ this.submitLocalEditor();
834
+ }
835
+
576
836
  activateSelected(): void {
837
+ if (this.localEditor) {
838
+ this.submitEditorFieldOrForm();
839
+ return;
840
+ }
577
841
  if (this.focusPane === 'categories') {
578
842
  this.focusActions();
579
843
  return;
580
844
  }
581
845
  const action = this.selectedAction;
582
846
  if (!action) return;
847
+ if (action.kind === 'editor' && action.editorKind) {
848
+ this.localEditor = createLocalEditor(action.editorKind);
849
+ this.status = `Editing ${this.localEditor.title}.`;
850
+ this.lastActionResult = {
851
+ kind: 'guidance',
852
+ title: this.localEditor.title,
853
+ detail: this.localEditor.message,
854
+ safety: action.safety,
855
+ };
856
+ return;
857
+ }
583
858
  if (action.kind === 'guidance' || !action.command) {
584
859
  if (action.kind === 'workspace' && action.targetCategoryId) {
585
860
  const targetIndex = this.categories.findIndex((category) => category.id === action.targetCategoryId);
@@ -674,6 +949,121 @@ export class AgentWorkspace {
674
949
  this.selectedCategoryIndex = Math.max(0, Math.min(this.selectedCategoryIndex, this.categories.length - 1));
675
950
  this.selectedActionIndex = Math.max(0, Math.min(this.selectedActionIndex, this.actions.length - 1));
676
951
  }
952
+
953
+ private replaceEditorField(index: number, value: string, message: string): void {
954
+ const editor = this.localEditor;
955
+ if (!editor) return;
956
+ const fields = editor.fields.map((field, fieldIndex) => fieldIndex === index ? { ...field, value } : field);
957
+ this.localEditor = { ...editor, fields, message };
958
+ }
959
+
960
+ private editorField(id: string): string {
961
+ const editor = this.localEditor;
962
+ return editor?.fields.find((field) => field.id === id)?.value.trim() ?? '';
963
+ }
964
+
965
+ private missingEditorField(): AgentWorkspaceEditorField | null {
966
+ const editor = this.localEditor;
967
+ if (!editor) return null;
968
+ return editor.fields.find((field) => field.required && field.value.trim().length === 0) ?? null;
969
+ }
970
+
971
+ private submitLocalEditor(): void {
972
+ const editor = this.localEditor;
973
+ if (!editor) return;
974
+ const missing = this.missingEditorField();
975
+ if (missing) {
976
+ const missingIndex = editor.fields.findIndex((field) => field.id === missing.id);
977
+ this.localEditor = {
978
+ ...editor,
979
+ selectedFieldIndex: Math.max(0, missingIndex),
980
+ message: `${missing.label} is required before saving.`,
981
+ };
982
+ this.status = `${missing.label} is required.`;
983
+ return;
984
+ }
985
+ const shellPaths = this.context?.workspace?.shellPaths;
986
+ if (!shellPaths) {
987
+ this.localEditor = { ...editor, message: 'Cannot save because Agent shell paths are unavailable.' };
988
+ this.status = 'Cannot save local Agent registry item without shell paths.';
989
+ this.lastActionResult = {
990
+ kind: 'error',
991
+ title: 'Local registry unavailable',
992
+ detail: 'The Agent workspace cannot locate the Agent-local registry files for this runtime.',
993
+ };
994
+ return;
995
+ }
996
+ try {
997
+ if (editor.kind === 'persona') {
998
+ const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
999
+ const created = registry.create({
1000
+ name: this.editorField('name'),
1001
+ description: this.editorField('description'),
1002
+ body: this.editorField('body'),
1003
+ tags: splitList(this.editorField('tags')),
1004
+ triggers: splitList(this.editorField('triggers')),
1005
+ source: 'user',
1006
+ provenance: 'agent-workspace',
1007
+ });
1008
+ if (isAffirmative(this.editorField('activate'))) registry.setActive(created.id);
1009
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1010
+ } else if (editor.kind === 'skill') {
1011
+ const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
1012
+ const created = registry.create({
1013
+ name: this.editorField('name'),
1014
+ description: this.editorField('description'),
1015
+ procedure: this.editorField('procedure'),
1016
+ triggers: splitList(this.editorField('triggers')),
1017
+ tags: splitList(this.editorField('tags')),
1018
+ enabled: isAffirmative(this.editorField('enabled')),
1019
+ source: 'user',
1020
+ provenance: 'agent-workspace',
1021
+ });
1022
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1023
+ } else {
1024
+ const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
1025
+ const created = registry.create({
1026
+ name: this.editorField('name'),
1027
+ description: this.editorField('description'),
1028
+ steps: this.editorField('steps'),
1029
+ triggers: splitList(this.editorField('triggers')),
1030
+ tags: splitList(this.editorField('tags')),
1031
+ enabled: isAffirmative(this.editorField('enabled')),
1032
+ source: 'user',
1033
+ provenance: 'agent-workspace',
1034
+ });
1035
+ this.finishLocalEditor(editor.kind, created.id, created.name);
1036
+ }
1037
+ } catch (error) {
1038
+ const detail = error instanceof Error ? error.message : String(error);
1039
+ this.localEditor = { ...editor, message: detail };
1040
+ this.status = detail;
1041
+ this.lastActionResult = {
1042
+ kind: 'error',
1043
+ title: `${editor.title} failed`,
1044
+ detail,
1045
+ };
1046
+ }
1047
+ }
1048
+
1049
+ private finishLocalEditor(kind: AgentWorkspaceLocalEditorKind, id: string, name: string): void {
1050
+ this.localEditor = null;
1051
+ const categoryId = editorCategoryId(kind);
1052
+ const categoryIndex = this.categories.findIndex((category) => category.id === categoryId);
1053
+ if (categoryIndex >= 0) {
1054
+ this.selectedCategoryIndex = categoryIndex;
1055
+ this.selectedActionIndex = 0;
1056
+ }
1057
+ this.runtimeSnapshot = this.context ? buildAgentWorkspaceRuntimeSnapshot(this.context) : this.runtimeSnapshot;
1058
+ this.status = `Created ${kind}: ${name}.`;
1059
+ this.lastActionResult = {
1060
+ kind: 'refreshed',
1061
+ title: `Created ${kind}`,
1062
+ detail: `${name} (${id}) was saved to the Agent-local ${categoryId} registry.`,
1063
+ safety: 'safe',
1064
+ };
1065
+ this.clampSelection();
1066
+ }
677
1067
  }
678
1068
 
679
1069
  export function handleAgentWorkspaceToken(
@@ -684,6 +1074,21 @@ export function handleAgentWorkspaceToken(
684
1074
  ): boolean {
685
1075
  if (!workspace.active) return false;
686
1076
 
1077
+ if (workspace.localEditor) {
1078
+ if (token.type === 'text') {
1079
+ workspace.appendEditorText(token.value);
1080
+ } else if (token.type === 'key') {
1081
+ if (token.logicalName === 'escape') workspace.cancelLocalEditor();
1082
+ else if (token.logicalName === 'enter') workspace.submitEditorFieldOrForm();
1083
+ else if (token.logicalName === 'tab' || token.logicalName === 'down') workspace.moveEditorField(1);
1084
+ else if (token.logicalName === 'up') workspace.moveEditorField(-1);
1085
+ else if (token.logicalName === 'backspace' || token.logicalName === 'delete') workspace.editorBackspace();
1086
+ else if (token.logicalName === 'j' && token.ctrl === true) workspace.appendEditorNewline();
1087
+ }
1088
+ requestRender();
1089
+ return true;
1090
+ }
1091
+
687
1092
  if (token.type === 'key') {
688
1093
  if (token.logicalName === 'escape') {
689
1094
  handleEscape();
@@ -3,6 +3,7 @@ import type {
3
3
  AgentWorkspaceAction,
4
4
  AgentWorkspaceActionResult,
5
5
  AgentWorkspaceCategory,
6
+ AgentWorkspaceLocalEditor,
6
7
  AgentWorkspaceRuntimeSnapshot,
7
8
  } from '../input/agent-workspace.ts';
8
9
  import type { Line } from '../types/grid.ts';
@@ -68,6 +69,7 @@ function buildLeftRows(workspace: AgentWorkspace, height: number): WorkspaceRow[
68
69
 
69
70
  function actionCommand(action: AgentWorkspaceAction): string {
70
71
  if (action.kind === 'workspace') return action.targetCategoryId ? `open ${action.targetCategoryId}` : '(workspace)';
72
+ if (action.kind === 'editor') return action.editorKind ? `edit ${action.editorKind}` : '(editor)';
71
73
  return action.command ?? '(guidance)';
72
74
  }
73
75
 
@@ -100,6 +102,40 @@ function setupChecklistLines(snapshot: AgentWorkspaceRuntimeSnapshot): ContextLi
100
102
  return lines;
101
103
  }
102
104
 
105
+ function localLibraryLines(
106
+ title: string,
107
+ items: readonly AgentWorkspaceRuntimeSnapshot['localPersonas'][number][],
108
+ emptyText: string,
109
+ ): ContextLine[] {
110
+ const lines: ContextLine[] = [
111
+ { text: title, fg: PALETTE.title, bold: true },
112
+ ];
113
+ if (items.length === 0) {
114
+ lines.push({ text: emptyText, fg: PALETTE.warn });
115
+ return lines;
116
+ }
117
+ for (const item of items.slice(0, 8)) {
118
+ const status = [
119
+ item.active ? 'active' : '',
120
+ item.enabled === true ? 'enabled' : item.enabled === false ? 'disabled' : '',
121
+ item.reviewState,
122
+ item.startCount !== undefined ? `starts ${item.startCount}` : '',
123
+ ].filter(Boolean).join(' / ');
124
+ const tags = item.tags.length > 0 ? ` tags=${item.tags.join(',')}` : '';
125
+ const triggers = item.triggers.length > 0 ? ` triggers=${item.triggers.join(',')}` : '';
126
+ lines.push({
127
+ text: `${item.id}: ${item.name} (${status})`,
128
+ fg: item.reviewState === 'stale' ? PALETTE.warn : PALETTE.info,
129
+ bold: item.active === true,
130
+ });
131
+ lines.push({ text: ` ${item.description}${tags}${triggers}`, fg: PALETTE.muted });
132
+ }
133
+ if (items.length > 8) {
134
+ lines.push({ text: `${items.length - 8} more item(s). Open the library command for the full list.`, fg: PALETTE.dim });
135
+ }
136
+ return lines;
137
+ }
138
+
103
139
  function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspaceRuntimeSnapshot | null): ContextLine[] {
104
140
  if (!snapshot) return [{ text: 'Runtime context is not loaded yet.', fg: PALETTE.warn }];
105
141
  const base: ContextLine[] = [{ text: 'Live Agent Context', fg: PALETTE.title, bold: true }];
@@ -178,6 +214,30 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
178
214
  { text: 'Durable memory, routines, skills, and personas remain Agent-local until shared registry contracts exist.', fg: PALETTE.good },
179
215
  { text: 'Secrets are rejected/redacted; store secret references instead of secret values.', fg: PALETTE.warn },
180
216
  );
217
+ } else if (category.id === 'personas') {
218
+ base.push(
219
+ { text: `Personas: ${snapshot.localPersonaCount}; active: ${snapshot.activePersonaName}`, fg: PALETTE.info },
220
+ { text: 'Personas are local behavior profiles for the serial main-conversation assistant, not spawned agents.', fg: PALETTE.good },
221
+ { text: 'Use them for tone, role, domain constraints, tool posture, and repeatable operating preferences.', fg: PALETTE.muted },
222
+ { text: '' },
223
+ ...localLibraryLines('Persona Library', snapshot.localPersonas, 'No local personas yet. Create one with /personas create ...'),
224
+ );
225
+ } else if (category.id === 'skills') {
226
+ base.push(
227
+ { text: `Skills: ${snapshot.localSkillCount}; enabled: ${snapshot.enabledSkillCount}`, fg: PALETTE.info },
228
+ { text: 'Skills are reusable local procedures the assistant can apply from the main conversation.', fg: PALETTE.good },
229
+ { text: 'Enabled skills are injected as operating guidance; secret-looking content is rejected.', fg: PALETTE.warn },
230
+ { text: '' },
231
+ ...localLibraryLines('Skill Library', snapshot.localSkills, 'No local skills yet. Create one with /agent-skills create ...'),
232
+ );
233
+ } else if (category.id === 'routines') {
234
+ base.push(
235
+ { text: `Routines: ${snapshot.localRoutineCount}; enabled: ${snapshot.enabledRoutineCount}`, fg: PALETTE.info },
236
+ { text: 'Routines are repeatable main-conversation workflows. Starting one does not create hidden jobs.', fg: PALETTE.good },
237
+ { text: 'Scheduling a reviewed routine is explicit and writes to the externally owned daemon only with --yes.', fg: PALETTE.warn },
238
+ { text: '' },
239
+ ...localLibraryLines('Routine Library', snapshot.localRoutines, 'No local routines yet. Create one with /routines create ...'),
240
+ );
181
241
  } else if (category.id === 'work') {
182
242
  base.push(
183
243
  { text: 'Work plan and approvals are read or explicitly confirmed through public operator routes.', fg: PALETTE.info },
@@ -202,6 +262,23 @@ function snapshotLines(category: AgentWorkspaceCategory, snapshot: AgentWorkspac
202
262
  return base;
203
263
  }
204
264
 
265
+ function editorContextLines(editor: AgentWorkspaceLocalEditor): ContextLine[] {
266
+ const selected = editor.fields[editor.selectedFieldIndex];
267
+ const lines: ContextLine[] = [
268
+ { text: editor.title, fg: PALETTE.title, bold: true },
269
+ { text: editor.message, fg: editor.message.includes('required') || editor.message.includes('cannot') || editor.message.includes('Cannot') ? PALETTE.warn : PALETTE.info },
270
+ { text: 'Enter advances fields and saves from the final field. Ctrl-J adds a line inside multiline fields. Esc cancels without writing.', fg: PALETTE.muted },
271
+ ];
272
+ if (selected) {
273
+ lines.push(
274
+ { text: '' },
275
+ { text: `Editing: ${selected.label}${selected.required ? ' (required)' : ''}`, fg: PALETTE.title, bold: true },
276
+ { text: selected.hint, fg: PALETTE.muted },
277
+ );
278
+ }
279
+ return lines;
280
+ }
281
+
205
282
  function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCategory, action: AgentWorkspaceAction | null, width: number): WorkspaceRow[] {
206
283
  const lines: ContextLine[] = [
207
284
  { text: category.label, fg: PALETTE.title, bold: true },
@@ -209,6 +286,8 @@ function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCat
209
286
  { text: '' },
210
287
  { text: category.detail, fg: PALETTE.text },
211
288
  { text: '' },
289
+ ...(workspace.localEditor ? editorContextLines(workspace.localEditor) : []),
290
+ ...(workspace.localEditor ? [{ text: '' }] : []),
212
291
  ...snapshotLines(category, workspace.runtimeSnapshot),
213
292
  ];
214
293
 
@@ -245,7 +324,42 @@ function buildContextRows(workspace: AgentWorkspace, category: AgentWorkspaceCat
245
324
  });
246
325
  }
247
326
 
327
+ function buildEditorRows(editor: AgentWorkspaceLocalEditor, width: number, height: number): WorkspaceRow[] {
328
+ const rows: WorkspaceRow[] = [
329
+ { text: editor.title, fg: PALETTE.title, bold: true },
330
+ { text: editor.message, fg: PALETTE.info },
331
+ { text: '' },
332
+ ];
333
+ for (let index = 0; index < editor.fields.length; index += 1) {
334
+ const field = editor.fields[index]!;
335
+ const selected = index === editor.selectedFieldIndex;
336
+ const marker = selected ? GLYPHS.navigation.selected : ' ';
337
+ const required = field.required ? ' *' : '';
338
+ const value = field.value.length > 0 ? field.value : '(empty)';
339
+ const color = selected ? PALETTE.text : field.value.length > 0 ? PALETTE.info : PALETTE.muted;
340
+ rows.push({
341
+ text: `${marker} ${field.label}${required}`,
342
+ selected,
343
+ fg: color,
344
+ bold: selected,
345
+ });
346
+ const valueLines = value.split('\n');
347
+ for (const valueLine of valueLines.slice(0, 4)) {
348
+ for (const wrapped of wrapText(` ${valueLine}`, Math.max(1, width - 2))) {
349
+ rows.push({ text: wrapped, fg: field.value.length > 0 ? PALETTE.text : PALETTE.dim, dim: field.value.length === 0 });
350
+ }
351
+ }
352
+ if (valueLines.length > 4) rows.push({ text: ` ${valueLines.length - 4} more line(s)`, fg: PALETTE.dim, dim: true });
353
+ rows.push({ text: ` ${field.hint}`, fg: PALETTE.dim, dim: true });
354
+ }
355
+ rows.push({ text: '' });
356
+ rows.push({ text: 'Enter next/save · Up/Down field · Backspace edit · Ctrl-J newline · Esc cancel', fg: PALETTE.muted });
357
+ while (rows.length < height) rows.push({ text: '', kind: 'empty' });
358
+ return rows.slice(0, height);
359
+ }
360
+
248
361
  function buildActionRows(workspace: AgentWorkspace, width: number, height: number): WorkspaceRow[] {
362
+ if (workspace.localEditor) return buildEditorRows(workspace.localEditor, width, height);
249
363
  const rows: WorkspaceRow[] = [];
250
364
  const labelWidth = Math.min(28, Math.max(16, Math.floor(width * 0.30)));
251
365
  const safetyWidth = 10;
@@ -292,6 +406,9 @@ function buildActionRows(workspace: AgentWorkspace, width: number, height: numbe
292
406
  }
293
407
 
294
408
  function footerText(workspace: AgentWorkspace): string {
409
+ if (workspace.localEditor) {
410
+ return `Agent workspace · editing ${workspace.localEditor.kind} · Enter next/save · Ctrl-J newline · Esc cancel`;
411
+ }
295
412
  const focus = workspace.focusPane === 'categories' ? 'categories' : 'actions';
296
413
  return `Agent workspace · focus ${focus} · Up/Down navigate · Left/Right pane · Enter open/action · R refresh · Esc close`;
297
414
  }
@@ -313,7 +430,7 @@ export function renderAgentWorkspace(workspace: AgentWorkspace, width: number, h
313
430
  width,
314
431
  height,
315
432
  title: 'GoodVibes Agent / Operator Workspace',
316
- stateLabel: workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
433
+ stateLabel: workspace.localEditor ? 'Editor' : workspace.focusPane === 'categories' ? 'Categories' : 'Actions',
317
434
  leftHeader: 'Operator Areas',
318
435
  mainHeader: `${category.label} · ${category.actions.length} action(s)`,
319
436
  leftRows: buildLeftRows(workspace, metrics.bodyRows),
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.49';
9
+ let _version = '0.1.51';
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 {