@pellux/goodvibes-agent 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -4
  3. package/dist/package/main.js +14046 -12557
  4. package/docs/README.md +4 -2
  5. package/docs/channels-remote-and-api.md +4 -0
  6. package/docs/connected-host.md +2 -0
  7. package/docs/getting-started.md +6 -2
  8. package/docs/knowledge-artifacts-and-multimodal.md +5 -3
  9. package/docs/project-planning.md +3 -1
  10. package/docs/providers-and-routing.md +3 -0
  11. package/docs/release-and-publishing.md +7 -6
  12. package/docs/tools-and-commands.md +97 -2
  13. package/docs/voice-and-live-tts.md +2 -0
  14. package/package.json +1 -1
  15. package/src/agent/harness-control.ts +266 -0
  16. package/src/cli/local-library-command.ts +25 -5
  17. package/src/cli/routines-command.ts +15 -3
  18. package/src/config/agent-settings-policy.ts +44 -0
  19. package/src/input/agent-workspace-activation.ts +14 -6
  20. package/src/input/agent-workspace-knowledge-url-editor.ts +4 -11
  21. package/src/input/commands/agent-local-library-args.ts +52 -0
  22. package/src/input/commands/agent-skills-runtime.ts +4 -29
  23. package/src/input/commands/mcp-runtime.ts +62 -15
  24. package/src/input/commands/operator-runtime.ts +139 -3
  25. package/src/input/commands/personas-runtime.ts +4 -29
  26. package/src/input/commands/routines-runtime.ts +4 -29
  27. package/src/input/session-picker-modal.ts +3 -2
  28. package/src/input/settings-modal-agent-policy.ts +5 -44
  29. package/src/runtime/bootstrap.ts +3 -0
  30. package/src/tools/agent-harness-local-operations.ts +233 -0
  31. package/src/tools/agent-harness-metadata.ts +300 -0
  32. package/src/tools/agent-harness-tool.ts +774 -0
  33. package/src/tools/agent-local-registry-requirements.ts +18 -0
  34. package/src/tools/agent-local-registry-tool.ts +88 -34
  35. package/src/version.ts +1 -1
@@ -2,38 +2,13 @@ import { discoverPersonas, type DiscoveredPersonaRecord } from '../../agent/pers
2
2
  import { AgentPersonaRegistry, type AgentPersonaRecord } from '../../agent/persona-registry.ts';
3
3
  import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../../agent/record-labels.ts';
4
4
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
5
+ import { parseAgentLocalLibraryArgs, type ParsedAgentLocalLibraryArgs } from './agent-local-library-args.ts';
5
6
  import { requireShellPaths } from './runtime-services.ts';
6
7
 
7
- interface ParsedPersonaArgs {
8
- readonly rest: readonly string[];
9
- readonly flags: ReadonlyMap<string, string>;
10
- readonly yes: boolean;
11
- }
8
+ const PERSONA_VALUE_FLAGS = ['name', 'description', 'body', 'tags', 'triggers'] as const;
12
9
 
13
- function parsePersonaArgs(args: readonly string[]): ParsedPersonaArgs {
14
- const flags = new Map<string, string>();
15
- const rest: string[] = [];
16
- let yes = false;
17
- for (let index = 0; index < args.length; index += 1) {
18
- const token = args[index] ?? '';
19
- if (token === '--yes') {
20
- yes = true;
21
- continue;
22
- }
23
- if (token.startsWith('--')) {
24
- const key = token.slice(2);
25
- const next = args[index + 1];
26
- if (next !== undefined && !next.startsWith('--')) {
27
- flags.set(key, next);
28
- index += 1;
29
- } else {
30
- flags.set(key, 'true');
31
- }
32
- continue;
33
- }
34
- rest.push(token);
35
- }
36
- return { rest, flags, yes };
10
+ function parsePersonaArgs(args: readonly string[]): ParsedAgentLocalLibraryArgs {
11
+ return parseAgentLocalLibraryArgs(args, { valueFlags: PERSONA_VALUE_FLAGS });
37
12
  }
38
13
 
39
14
  function splitList(value: string | undefined): readonly string[] {
@@ -21,38 +21,13 @@ import {
21
21
  RoutineScheduleReceiptStore,
22
22
  } from '../../agent/routine-schedule-receipts.ts';
23
23
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
24
+ import { parseAgentLocalLibraryArgs, type ParsedAgentLocalLibraryArgs } from './agent-local-library-args.ts';
24
25
  import { requireShellPaths } from './runtime-services.ts';
25
26
 
26
- interface ParsedRoutineArgs {
27
- readonly rest: readonly string[];
28
- readonly flags: ReadonlyMap<string, string>;
29
- readonly yes: boolean;
30
- }
27
+ const ROUTINE_VALUE_FLAGS = ['name', 'description', 'steps', 'tags', 'triggers', 'requires-env', 'requires-command', 'requires-commands'] as const;
31
28
 
32
- function parseRoutineArgs(args: readonly string[]): ParsedRoutineArgs {
33
- const flags = new Map<string, string>();
34
- const rest: string[] = [];
35
- let yes = false;
36
- for (let index = 0; index < args.length; index += 1) {
37
- const token = args[index] ?? '';
38
- if (token === '--yes') {
39
- yes = true;
40
- continue;
41
- }
42
- if (token.startsWith('--')) {
43
- const key = token.slice(2);
44
- const next = args[index + 1];
45
- if (next !== undefined && !next.startsWith('--')) {
46
- flags.set(key, next);
47
- index += 1;
48
- } else {
49
- flags.set(key, 'true');
50
- }
51
- continue;
52
- }
53
- rest.push(token);
54
- }
55
- return { rest, flags, yes };
29
+ function parseRoutineArgs(args: readonly string[]): ParsedAgentLocalLibraryArgs {
30
+ return parseAgentLocalLibraryArgs(args, { valueFlags: ROUTINE_VALUE_FLAGS });
56
31
  }
57
32
 
58
33
  function splitList(value: string | undefined): readonly string[] {
@@ -9,20 +9,21 @@ import type { SessionInfo, SessionManager } from '@pellux/goodvibes-sdk/platform
9
9
  import type { ConversationManager } from '../core/conversation';
10
10
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
11
11
  import { readConversationMessageSnapshots } from '../core/conversation-message-snapshot.ts';
12
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
12
13
 
13
14
  function sessionLoadedMessage(name: string, messageCount: number): string {
14
15
  return `Loaded session ${name} (${messageCount} messages)`;
15
16
  }
16
17
 
17
18
  function sessionDeletionCommandRequiredMessage(name: string): string {
18
- return `Deletion requires an explicit command: /session delete ${name} --yes`;
19
+ return `Deletion requires an explicit command: /session delete ${quoteSlashCommandArg(name)} --yes`;
19
20
  }
20
21
 
21
22
  export function renderSessionPickerStatePackageText(): string {
22
23
  return [
23
24
  'Loaded session <session> (<count> messages)',
24
25
  'Error',
25
- sessionDeletionCommandRequiredMessage('<session>'),
26
+ 'Deletion requires an explicit command: /session delete <session> --yes',
26
27
  ].join('\n');
27
28
  }
28
29
 
@@ -1,44 +1,5 @@
1
- export const AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON = 'GoodVibes Agent uses a connected GoodVibes host. Change host lifecycle and bind posture from the owning host; Agent settings are read-only for those controls.';
2
-
3
- const AGENT_HIDDEN_SETTING_PREFIXES = [
4
- ['cloud', 'flare.'].join(''),
5
- ['surfaces.', 'home', 'assistant.'].join(''),
6
- 'batch.',
7
- 'controlPlane.',
8
- 'danger.',
9
- 'httpListener.',
10
- 'network.',
11
- 'orchestration.',
12
- 'runtime.',
13
- 'service.',
14
- 'sandbox.',
15
- 'web.',
16
- 'watchers.',
17
- 'wrfc.',
18
- ] as const;
19
-
20
- const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
21
- 'ui.wrfcMessages',
22
- ]);
23
-
24
- const EXTERNAL_HOST_SETTING_PREFIXES = [
25
- 'service.',
26
- 'controlPlane.',
27
- 'httpListener.',
28
- 'web.',
29
- ] as const;
30
-
31
- const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
32
- 'danger.daemon',
33
- 'danger.httpListener',
34
- ]);
35
-
36
- export function isExternalHostOwnedSettingKey(key: string): boolean {
37
- return EXTERNAL_HOST_SETTING_KEYS.has(key)
38
- || EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
39
- }
40
-
41
- export function isAgentHiddenSettingKey(key: string): boolean {
42
- return AGENT_HIDDEN_SETTING_KEYS.has(key)
43
- || AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
44
- }
1
+ export {
2
+ AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON,
3
+ isAgentHiddenSettingKey,
4
+ isExternalHostOwnedSettingKey,
5
+ } from '../config/agent-settings-policy.ts';
@@ -42,6 +42,7 @@ import { buildActivePersonaPrompt } from '../agent/persona-registry.ts';
42
42
  import { buildEnabledSkillsPrompt } from '../agent/skill-registry.ts';
43
43
  import { buildEnabledRoutinesPrompt } from '../agent/routine-registry.ts';
44
44
  import { buildReviewedMemoryPrompt } from '../agent/memory-prompt.ts';
45
+ import { registerAgentHarnessTool } from '../tools/agent-harness-tool.ts';
45
46
 
46
47
  const GOODVIBES_AGENT_OPERATOR_POLICY = [
47
48
  '## GoodVibes Agent Operator Policy',
@@ -50,6 +51,7 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
50
51
  '- Use the `agent_operator_briefing` tool for read-only main-conversation summaries of connected work plan, approvals, automation, schedules, and scheduler capacity. This tool must not call mutation routes, connected-host lifecycle routes, default knowledge, non-Agent knowledge spaces, separate Agent job creation, or GoodVibes TUI delegation.',
51
52
  '- When the user explicitly asks Agent to approve, deny, or cancel a specific approval, run/pause/resume a specific automation job, cancel/retry a specific automation run, or run a specific schedule, use the `agent_operator_action` tool with confirm:true and the original user request. It can call only its allowlisted public operator routes and must not create, edit, delete, or discover automation definitions.',
52
53
  '- Use the `agent_work_plan` tool to keep the visible Agent-local work plan current while working in the main conversation. Create, inspect, and update local work items proactively when useful. Removing items or clearing completed items requires an explicit user request and confirm:true.',
54
+ '- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
53
55
  '- Use the `agent_knowledge` tool for Agent Knowledge status, ask, and search from the main conversation. It must use only /api/goodvibes-agent/knowledge/* and must fail closed instead of falling back to default knowledge or non-Agent knowledge spaces.',
54
56
  '- When the user explicitly asks Agent to add, import, remember, or ingest a URL, URL-list file, local file, bookmarks file, browser history, or connector input into Agent Knowledge, use the `agent_knowledge_ingest` tool with confirm:true and the original user request. It must write only to /api/goodvibes-agent/knowledge/* ingest routes and never to default knowledge or non-Agent knowledge spaces.',
55
57
  '- Use the `agent_local_registry` tool when a scratchpad note, durable memory, reusable persona, skill, skill bundle, or routine would improve later work. Keep those records Agent-local, non-secret, source/provenance tagged, and reviewable. Notes are for temporary/source-triage context; promote them explicitly into memory, skills, personas, routines, or Agent Knowledge only when that is the correct durable home. Review memory with a confidence score when it should shape later turns. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
@@ -285,6 +287,7 @@ export async function bootstrapRuntime(
285
287
  const commandRegistry = shell.commandRegistry;
286
288
  const commandContext = shell.commandContext;
287
289
  const inputHistory = shell.inputHistory;
290
+ registerAgentHarnessTool(toolRegistry, commandRegistry, commandContext);
288
291
  const pluginCommandRegistry = {
289
292
  register(command: {
290
293
  readonly name: string;
@@ -0,0 +1,233 @@
1
+ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
2
+ import { AgentNoteRegistry } from '../agent/note-registry.ts';
3
+ import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
4
+ import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
5
+ import { AgentSkillRegistry } from '../agent/skill-registry.ts';
6
+ import type { CommandContext } from '../input/command-registry.ts';
7
+ import {
8
+ createKnowledgeUrlEditorFromNote,
9
+ createMemoryEditorFromNote,
10
+ createMemoryUpdateEditor,
11
+ createNoteUpdateEditor,
12
+ createPersonaEditorFromNote,
13
+ createPersonaUpdateEditor,
14
+ createRoutineEditorFromNote,
15
+ createRoutineUpdateEditor,
16
+ createSkillEditorFromNote,
17
+ createSkillUpdateEditor,
18
+ isAffirmative,
19
+ splitList,
20
+ } from '../input/agent-workspace-editors.ts';
21
+ import type { AgentWorkspaceAction, AgentWorkspaceLocalEditor, AgentWorkspaceLocalOperation } from '../input/agent-workspace-types.ts';
22
+
23
+ export interface AgentHarnessLocalOperationArgs {
24
+ readonly fields?: unknown;
25
+ readonly recordId?: unknown;
26
+ readonly id?: unknown;
27
+ readonly confirm?: unknown;
28
+ readonly explicitUserRequest?: unknown;
29
+ }
30
+
31
+ export interface AgentHarnessLocalOperationDeps {
32
+ readonly commandContext: CommandContext;
33
+ readonly toolRegistry: ToolRegistry;
34
+ }
35
+
36
+ type HarnessResult =
37
+ | { readonly success: true; readonly output: string }
38
+ | { readonly success: false; readonly error: string };
39
+
40
+ type RegistryDomain = 'memory' | 'note' | 'persona' | 'skill' | 'routine';
41
+ type RegistryAction = 'update' | 'review' | 'stale' | 'delete' | 'use' | 'clear_active' | 'enable' | 'disable' | 'start';
42
+
43
+ interface RegistryTarget {
44
+ readonly domain: RegistryDomain;
45
+ readonly action: RegistryAction;
46
+ readonly requiresRecordId: boolean;
47
+ }
48
+
49
+ function output(value: unknown): HarnessResult {
50
+ return { success: true, output: typeof value === 'string' ? value : JSON.stringify(value, null, 2) };
51
+ }
52
+
53
+ function error(message: string): HarnessResult {
54
+ return { success: false, error: message };
55
+ }
56
+
57
+ function readString(value: unknown): string {
58
+ return typeof value === 'string' ? value.trim() : '';
59
+ }
60
+
61
+ function readFieldMap(value: unknown): Readonly<Record<string, string>> {
62
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return {};
63
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, typeof entry === 'string' ? entry : String(entry)]));
64
+ }
65
+
66
+ function describeEditor(editor: AgentWorkspaceLocalEditor): Record<string, unknown> {
67
+ return {
68
+ kind: editor.kind,
69
+ mode: editor.mode,
70
+ recordId: editor.recordId,
71
+ title: editor.title,
72
+ message: editor.message,
73
+ fields: editor.fields.map((field) => ({
74
+ id: field.id,
75
+ label: field.label,
76
+ required: field.required,
77
+ multiline: field.multiline,
78
+ hint: field.hint,
79
+ default: field.redact ? '<redacted>' : field.value,
80
+ })),
81
+ };
82
+ }
83
+
84
+ function readRecordId(args: AgentHarnessLocalOperationArgs): string {
85
+ const fields = readFieldMap(args.fields);
86
+ return readString(args.recordId ?? args.id ?? fields.recordId ?? fields.id);
87
+ }
88
+
89
+ function requireConfirmed(args: AgentHarnessLocalOperationArgs, label: string): string | null {
90
+ if (!readString(args.explicitUserRequest)) return `${label} requires explicitUserRequest with the user's exact request or a short faithful summary.`;
91
+ if (args.confirm !== true) return `${label} requires confirm:true after an explicit user request.`;
92
+ return null;
93
+ }
94
+
95
+ function fieldReader(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>): (id: string) => string {
96
+ return (id: string) => fields[id] ?? editor.fields.find((field) => field.id === id)?.value ?? '';
97
+ }
98
+
99
+ function hasExecutionFields(args: AgentHarnessLocalOperationArgs): boolean {
100
+ return Object.keys(readFieldMap(args.fields)).some((key) => key !== 'id' && key !== 'recordId');
101
+ }
102
+
103
+ function missingRequiredFields(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>): readonly string[] {
104
+ const read = fieldReader(editor, fields);
105
+ return editor.fields.filter((field) => field.required && !read(field.id).trim()).map((field) => field.id);
106
+ }
107
+
108
+ function registryTarget(operation: AgentWorkspaceLocalOperation): RegistryTarget | null {
109
+ if (operation === 'memory-edit') return { domain: 'memory', action: 'update', requiresRecordId: true };
110
+ if (operation === 'memory-review') return { domain: 'memory', action: 'review', requiresRecordId: true };
111
+ if (operation === 'memory-stale') return { domain: 'memory', action: 'stale', requiresRecordId: true };
112
+ if (operation === 'memory-delete') return { domain: 'memory', action: 'delete', requiresRecordId: true };
113
+ if (operation === 'note-edit') return { domain: 'note', action: 'update', requiresRecordId: true };
114
+ if (operation === 'note-review') return { domain: 'note', action: 'review', requiresRecordId: true };
115
+ if (operation === 'note-stale') return { domain: 'note', action: 'stale', requiresRecordId: true };
116
+ if (operation === 'note-delete') return { domain: 'note', action: 'delete', requiresRecordId: true };
117
+ if (operation === 'persona-edit') return { domain: 'persona', action: 'update', requiresRecordId: true };
118
+ if (operation === 'persona-use') return { domain: 'persona', action: 'use', requiresRecordId: true };
119
+ if (operation === 'persona-review') return { domain: 'persona', action: 'review', requiresRecordId: true };
120
+ if (operation === 'persona-clear') return { domain: 'persona', action: 'clear_active', requiresRecordId: false };
121
+ if (operation === 'persona-delete') return { domain: 'persona', action: 'delete', requiresRecordId: true };
122
+ if (operation === 'skill-edit') return { domain: 'skill', action: 'update', requiresRecordId: true };
123
+ if (operation === 'skill-enable') return { domain: 'skill', action: 'enable', requiresRecordId: true };
124
+ if (operation === 'skill-disable') return { domain: 'skill', action: 'disable', requiresRecordId: true };
125
+ if (operation === 'skill-review') return { domain: 'skill', action: 'review', requiresRecordId: true };
126
+ if (operation === 'skill-delete') return { domain: 'skill', action: 'delete', requiresRecordId: true };
127
+ if (operation === 'routine-edit') return { domain: 'routine', action: 'update', requiresRecordId: true };
128
+ if (operation === 'routine-start') return { domain: 'routine', action: 'start', requiresRecordId: true };
129
+ if (operation === 'routine-enable') return { domain: 'routine', action: 'enable', requiresRecordId: true };
130
+ if (operation === 'routine-disable') return { domain: 'routine', action: 'disable', requiresRecordId: true };
131
+ if (operation === 'routine-review') return { domain: 'routine', action: 'review', requiresRecordId: true };
132
+ if (operation === 'routine-delete') return { domain: 'routine', action: 'delete', requiresRecordId: true };
133
+ return null;
134
+ }
135
+
136
+ function editorForOperation(context: CommandContext, operation: AgentWorkspaceLocalOperation, recordId: string): AgentWorkspaceLocalEditor | null {
137
+ const shellPaths = context.workspace.shellPaths;
138
+ if (operation === 'memory-edit') {
139
+ const record = context.clients?.agentKnowledgeApi?.memory?.get(recordId);
140
+ return record ? createMemoryUpdateEditor(record) : null;
141
+ }
142
+ if (!shellPaths) return null;
143
+ if (operation === 'note-edit') {
144
+ const note = AgentNoteRegistry.fromShellPaths(shellPaths).get(recordId);
145
+ return note ? createNoteUpdateEditor(note) : null;
146
+ }
147
+ if (operation === 'note-promote-memory' || operation === 'note-promote-persona' || operation === 'note-promote-skill' || operation === 'note-promote-routine' || operation === 'note-promote-knowledge-url') {
148
+ const note = AgentNoteRegistry.fromShellPaths(shellPaths).get(recordId);
149
+ if (!note) return null;
150
+ if (operation === 'note-promote-memory') return createMemoryEditorFromNote(note);
151
+ if (operation === 'note-promote-persona') return createPersonaEditorFromNote(note);
152
+ if (operation === 'note-promote-skill') return createSkillEditorFromNote(note);
153
+ if (operation === 'note-promote-routine') return createRoutineEditorFromNote(note);
154
+ return note.sourceUrl ? createKnowledgeUrlEditorFromNote(note) : null;
155
+ }
156
+ if (operation === 'persona-edit') {
157
+ const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
158
+ const persona = registry.get(recordId);
159
+ return persona ? createPersonaUpdateEditor(persona, registry.snapshot().activePersonaId === persona.id) : null;
160
+ }
161
+ if (operation === 'skill-edit') {
162
+ const skill = AgentSkillRegistry.fromShellPaths(shellPaths).get(recordId);
163
+ return skill ? createSkillUpdateEditor(skill) : null;
164
+ }
165
+ if (operation === 'routine-edit') {
166
+ const routine = AgentRoutineRegistry.fromShellPaths(shellPaths).get(recordId);
167
+ return routine ? createRoutineUpdateEditor(routine) : null;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function localRegistryArgsFromEditor(editor: AgentWorkspaceLocalEditor, fields: Readonly<Record<string, string>>, id?: string): Record<string, unknown> {
173
+ const read = fieldReader(editor, fields);
174
+ if (editor.kind === 'memory') return { domain: 'memory', action: editor.mode === 'update' ? 'update' : 'create', id, cls: read('cls'), scope: read('scope'), summary: read('summary'), detail: read('detail'), tags: splitList(read('tags')), confidence: read('confidence'), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
175
+ if (editor.kind === 'note') return { domain: 'note', action: 'update', id, title: read('title'), body: read('body'), sourceUrl: read('sourceUrl'), tags: splitList(read('tags')), provenance: 'agent-harness-local-operation' };
176
+ if (editor.kind === 'persona') return { domain: 'persona', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), body: read('body'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), activate: read('activate'), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
177
+ if (editor.kind === 'skill') return { domain: 'skill', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), procedure: read('procedure'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), requiresEnv: splitList(read('requiresEnv')), requiresCommands: splitList(read('requiresCommands')), enabled: isAffirmative(read('enabled')), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
178
+ return { domain: 'routine', action: editor.mode === 'update' ? 'update' : 'create', id, name: read('name'), description: read('description'), steps: read('steps'), tags: splitList(read('tags')), triggers: splitList(read('triggers')), requiresEnv: splitList(read('requiresEnv')), requiresCommands: splitList(read('requiresCommands')), enabled: isAffirmative(read('enabled')), provenance: editor.recordId ? 'agent-harness-local-operation' : 'agent-harness-note-promotion' };
179
+ }
180
+
181
+ function localRegistryArgsForTarget(target: RegistryTarget, args: AgentHarnessLocalOperationArgs, recordId: string): Record<string, unknown> {
182
+ const fields = readFieldMap(args.fields);
183
+ const base = { domain: target.domain, action: target.action, ...(recordId ? { id: recordId } : {}) };
184
+ const edit = target.action === 'update' ? localRegistryArgsFromEditor({ kind: target.domain, mode: 'update', recordId, title: '', selectedFieldIndex: 0, message: '', fields: [] } as AgentWorkspaceLocalEditor, fields, recordId) : {};
185
+ return {
186
+ ...base,
187
+ ...edit,
188
+ ...(target.action === 'stale' && fields.reason ? { reason: fields.reason } : {}),
189
+ ...(target.action === 'delete' ? { confirm: args.confirm, explicitUserRequest: readString(args.explicitUserRequest) } : {}),
190
+ };
191
+ }
192
+
193
+ async function executeTool(toolRegistry: ToolRegistry, name: string, toolArgs: Record<string, unknown>): Promise<HarnessResult> {
194
+ if (!toolRegistry.has(name)) return output({ status: 'model_tool_required', modelExecution: { tool: name, args: toolArgs } });
195
+ const result = await toolRegistry.execute(`agent-harness-${name}-${Date.now()}`, name, toolArgs) as { readonly success: boolean; readonly output?: string; readonly error?: string };
196
+ if (!result.success) return error(result.error ?? `${name} failed.`);
197
+ return output({ status: 'executed_model_tool', tool: name, output: result.output ?? '' });
198
+ }
199
+
200
+ export function describeLocalWorkspaceModelExecution(action: AgentWorkspaceAction): Record<string, unknown> | null {
201
+ if (action.kind === 'local-selection') return { tool: 'agent_local_registry', domain: action.localKind, actions: ['list', 'search', 'get'], note: 'TUI selection maps to recordId for model-run local-operation actions.' };
202
+ if (!action.localOperation) return null;
203
+ const target = registryTarget(action.localOperation);
204
+ if (target) return { tool: 'agent_local_registry', domain: target.domain, action: target.action, requiresRecordId: target.requiresRecordId };
205
+ if (action.localOperation === 'note-promote-knowledge-url') return { tool: 'agent_knowledge_ingest', sourceKind: 'url', requiresRecordId: true, selectedRecordDomain: 'note' };
206
+ return { tool: 'agent_local_registry', selectedRecordDomain: 'note', action: 'create', requiresRecordId: true };
207
+ }
208
+
209
+ export async function runLocalWorkspaceAction(
210
+ deps: AgentHarnessLocalOperationDeps,
211
+ action: AgentWorkspaceAction,
212
+ args: AgentHarnessLocalOperationArgs,
213
+ ): Promise<HarnessResult> {
214
+ if (action.kind === 'local-selection') return output({ status: 'local_selection', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
215
+ const operation = action.localOperation;
216
+ if (!operation) return output({ status: 'local_registry_action', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
217
+ const target = registryTarget(operation);
218
+ const recordId = readRecordId(args);
219
+ if (target?.requiresRecordId && !recordId) return output({ status: 'needs_record_id', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
220
+ const editor = recordId ? editorForOperation(deps.commandContext, operation, recordId) : null;
221
+ if (editor && !hasExecutionFields(args) && args.confirm !== true) return output({ status: 'editor', action: action.id, editor: describeEditor(editor), modelExecution: describeLocalWorkspaceModelExecution(action) });
222
+ const confirmationError = requireConfirmed(args, 'Workspace local registry action');
223
+ if (confirmationError) return error(confirmationError);
224
+ if (editor) {
225
+ const fields = readFieldMap(args.fields);
226
+ const missing = missingRequiredFields(editor, fields);
227
+ if (missing.length > 0) return output({ status: 'missing_required_fields', missing, action: action.id, editor: describeEditor(editor) });
228
+ if (editor.kind === 'knowledge-url') return executeTool(deps.toolRegistry, 'agent_knowledge_ingest', { sourceKind: 'url', url: fieldReader(editor, fields)('url'), tags: splitList(fieldReader(editor, fields)('tags')), folderPath: fieldReader(editor, fields)('folder'), confirm: args.confirm, explicitUserRequest: readString(args.explicitUserRequest) });
229
+ return executeTool(deps.toolRegistry, 'agent_local_registry', localRegistryArgsFromEditor(editor, fields, recordId));
230
+ }
231
+ if (!target) return output({ status: 'local_registry_action', action: action.id, modelExecution: describeLocalWorkspaceModelExecution(action) });
232
+ return executeTool(deps.toolRegistry, 'agent_local_registry', localRegistryArgsForTarget(target, args, recordId));
233
+ }