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