@pellux/goodvibes-agent 0.1.101 → 0.1.103

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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/docs/README.md +1 -1
  4. package/docs/getting-started.md +17 -3
  5. package/package.json +1 -18
  6. package/src/cli/help.ts +86 -0
  7. package/src/cli/local-library-command.ts +516 -0
  8. package/src/cli/management.ts +17 -0
  9. package/src/cli/memory-command.ts +646 -0
  10. package/src/cli/package-verification.ts +10 -0
  11. package/src/cli/parser.ts +8 -0
  12. package/src/cli/types.ts +3 -0
  13. package/src/input/agent-workspace-setup.ts +2 -2
  14. package/src/input/agent-workspace-snapshot.ts +4 -4
  15. package/src/input/agent-workspace-types.ts +2 -2
  16. package/src/input/command-registry.ts +0 -8
  17. package/src/input/feed-context-factory.ts +1 -3
  18. package/src/input/handler-feed.ts +1 -4
  19. package/src/input/handler-interactions.ts +0 -1
  20. package/src/input/handler-modal-stack.ts +0 -1
  21. package/src/input/handler-modal-token-routes.ts +0 -11
  22. package/src/input/handler-picker-routes.ts +11 -20
  23. package/src/input/handler-ui-state.ts +0 -6
  24. package/src/input/handler.ts +1 -17
  25. package/src/main.ts +0 -6
  26. package/src/panels/builtin/agent.ts +0 -17
  27. package/src/panels/index.ts +0 -2
  28. package/src/renderer/agent-workspace.ts +3 -3
  29. package/src/renderer/conversation-overlays.ts +0 -6
  30. package/src/renderer/live-tail-modal.ts +10 -69
  31. package/src/renderer/process-modal.ts +28 -530
  32. package/src/runtime/bootstrap-command-parts.ts +0 -28
  33. package/src/runtime/bootstrap-core.ts +1 -1
  34. package/src/runtime/bootstrap.ts +3 -12
  35. package/src/runtime/services.ts +3 -4
  36. package/src/tools/{wrfc-agent-guard.ts → agent-tool-policy-guard.ts} +0 -6
  37. package/src/version.ts +1 -1
  38. package/src/panels/agent-inspector-panel.ts +0 -521
  39. package/src/panels/agent-inspector-shared.ts +0 -94
  40. package/src/panels/agent-logs-panel.ts +0 -559
  41. package/src/panels/agent-logs-shared.ts +0 -129
  42. package/src/renderer/agent-detail-modal.ts +0 -331
  43. package/src/renderer/process-summary.ts +0 -67
@@ -0,0 +1,646 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { createShellPathService } from '@/runtime/index.ts';
4
+ import {
5
+ MemoryEmbeddingProviderRegistry,
6
+ MemoryRegistry,
7
+ MemoryStore,
8
+ type MemoryBundle,
9
+ type MemoryClass,
10
+ type MemoryLink,
11
+ type MemoryRecord,
12
+ type MemoryReviewState,
13
+ type MemoryScope,
14
+ type MemorySearchFilter,
15
+ type MemorySemanticSearchResult,
16
+ type ProvenanceLink,
17
+ type ProvenanceLinkKind,
18
+ } from '@pellux/goodvibes-sdk/platform/state';
19
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
20
+ import type { CliCommandOutput } from './types.ts';
21
+ import type { CliCommandRuntime } from './management.ts';
22
+
23
+ const VALID_CLASSES: readonly MemoryClass[] = ['decision', 'constraint', 'incident', 'pattern', 'fact', 'risk', 'runbook', 'architecture', 'ownership'];
24
+ const VALID_SCOPES: readonly MemoryScope[] = ['session', 'project', 'team'];
25
+ const VALID_REVIEW_STATES: readonly MemoryReviewState[] = ['fresh', 'reviewed', 'stale', 'contradicted'];
26
+ const VALID_PROVENANCE_KINDS: readonly ProvenanceLinkKind[] = ['session', 'turn', 'task', 'event', 'file'];
27
+ const VALUE_OPTIONS = new Set([
28
+ 'by',
29
+ 'cls',
30
+ 'confidence',
31
+ 'detail',
32
+ 'file',
33
+ 'label',
34
+ 'limit',
35
+ 'reason',
36
+ 'review-state',
37
+ 'scope',
38
+ 'session',
39
+ 'tags',
40
+ 'task',
41
+ 'turn',
42
+ ]);
43
+ const SECRET_PATTERNS: readonly RegExp[] = [
44
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
45
+ /\bsk-[A-Za-z0-9_-]{16,}\b/,
46
+ /\bgh[pousr]_[A-Za-z0-9_]{16,}\b/i,
47
+ /\b(?:password|passwd|api[_-]?key|token|secret)\s*[:=]\s*\S{6,}/i,
48
+ ];
49
+
50
+ interface CommandSuccess<TData> {
51
+ readonly ok: true;
52
+ readonly kind: string;
53
+ readonly data: TData;
54
+ }
55
+
56
+ interface CommandFailure {
57
+ readonly ok: false;
58
+ readonly kind: string;
59
+ readonly error: string;
60
+ }
61
+
62
+ interface ParsedOptions {
63
+ readonly values: ReadonlyMap<string, string>;
64
+ readonly flags: ReadonlySet<string>;
65
+ readonly positionals: readonly string[];
66
+ }
67
+
68
+ interface MemoryContext {
69
+ readonly registry: MemoryRegistry;
70
+ readonly store: MemoryStore;
71
+ readonly path: string;
72
+ }
73
+
74
+ interface MemoryListData {
75
+ readonly path: string;
76
+ readonly records: readonly MemoryRecord[];
77
+ readonly filter: MemorySearchFilter;
78
+ }
79
+
80
+ interface MemorySearchData extends MemoryListData {
81
+ readonly semantic: boolean;
82
+ readonly semanticResults: readonly MemorySemanticSearchResult[];
83
+ }
84
+
85
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
86
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
87
+ }
88
+
89
+ function success<TData>(runtime: CliCommandRuntime, kind: string, data: TData, text: string): CliCommandOutput {
90
+ const value: CommandSuccess<TData> = { ok: true, kind, data };
91
+ return { output: jsonOrText(runtime, value, text), exitCode: 0 };
92
+ }
93
+
94
+ function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCode: number): CliCommandOutput {
95
+ const value: CommandFailure = { ok: false, kind, error };
96
+ return {
97
+ output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : error,
98
+ exitCode,
99
+ };
100
+ }
101
+
102
+ function parseOptions(args: readonly string[]): ParsedOptions {
103
+ const values = new Map<string, string>();
104
+ const flags = new Set<string>();
105
+ const positionals: string[] = [];
106
+ for (let index = 0; index < args.length; index += 1) {
107
+ const arg = args[index] ?? '';
108
+ if (!arg.startsWith('--')) {
109
+ positionals.push(arg);
110
+ continue;
111
+ }
112
+ const equalIndex = arg.indexOf('=');
113
+ const rawName = equalIndex >= 0 ? arg.slice(2, equalIndex) : arg.slice(2);
114
+ const name = rawName.trim();
115
+ if (!name) continue;
116
+ if (equalIndex >= 0) {
117
+ values.set(name, arg.slice(equalIndex + 1));
118
+ continue;
119
+ }
120
+ if (VALUE_OPTIONS.has(name)) {
121
+ const next = args[index + 1];
122
+ if (next !== undefined && !next.startsWith('--')) {
123
+ values.set(name, next);
124
+ index += 1;
125
+ continue;
126
+ }
127
+ }
128
+ flags.add(name);
129
+ }
130
+ return { values, flags, positionals };
131
+ }
132
+
133
+ function optionValue(options: ParsedOptions, name: string): string | undefined {
134
+ const value = options.values.get(name);
135
+ if (value === undefined) return undefined;
136
+ const trimmed = value.trim();
137
+ return trimmed.length > 0 ? trimmed : undefined;
138
+ }
139
+
140
+ function hasFlag(options: ParsedOptions, name: string): boolean {
141
+ return options.flags.has(name);
142
+ }
143
+
144
+ function csvOption(options: ParsedOptions, name: string): readonly string[] | undefined {
145
+ const value = optionValue(options, name);
146
+ if (value === undefined) return undefined;
147
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
148
+ }
149
+
150
+ function parseLimit(value: string | undefined, fallback: number): number {
151
+ if (value === undefined) return fallback;
152
+ const parsed = Number.parseInt(value, 10);
153
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error('--limit must be a positive integer.');
154
+ return Math.min(parsed, 200);
155
+ }
156
+
157
+ function parseConfidence(value: string | undefined): number | undefined {
158
+ if (value === undefined) return undefined;
159
+ const parsed = Number.parseInt(value, 10);
160
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) throw new Error('--confidence must be an integer from 0 to 100.');
161
+ return parsed;
162
+ }
163
+
164
+ function isMemoryClass(value: string): value is MemoryClass {
165
+ return VALID_CLASSES.includes(value as MemoryClass);
166
+ }
167
+
168
+ function isMemoryScope(value: string): value is MemoryScope {
169
+ return VALID_SCOPES.includes(value as MemoryScope);
170
+ }
171
+
172
+ function isReviewState(value: string): value is MemoryReviewState {
173
+ return VALID_REVIEW_STATES.includes(value as MemoryReviewState);
174
+ }
175
+
176
+ function isProvenanceKind(value: string): value is ProvenanceLinkKind {
177
+ return VALID_PROVENANCE_KINDS.includes(value as ProvenanceLinkKind);
178
+ }
179
+
180
+ function requireClass(value: string | undefined): MemoryClass {
181
+ if (!value || !isMemoryClass(value)) throw new Error(`Invalid memory class "${value ?? ''}". Valid: ${VALID_CLASSES.join(', ')}`);
182
+ return value;
183
+ }
184
+
185
+ function requireScope(value: string | undefined): MemoryScope {
186
+ if (!value || !isMemoryScope(value)) throw new Error(`Invalid memory scope "${value ?? ''}". Valid: ${VALID_SCOPES.join(', ')}`);
187
+ return value;
188
+ }
189
+
190
+ function optionalScope(value: string | undefined): MemoryScope | undefined {
191
+ if (value === undefined) return undefined;
192
+ return requireScope(value);
193
+ }
194
+
195
+ function optionalClass(value: string | undefined): MemoryClass | undefined {
196
+ if (value === undefined) return undefined;
197
+ return requireClass(value);
198
+ }
199
+
200
+ function containsSecretLikeText(text: string): boolean {
201
+ return SECRET_PATTERNS.some((pattern) => pattern.test(text));
202
+ }
203
+
204
+ function assertNoSecretLikeText(fields: readonly string[]): void {
205
+ if (fields.some((field) => containsSecretLikeText(field))) {
206
+ throw new Error('Agent memory cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
207
+ }
208
+ }
209
+
210
+ function timestamp(value: number): string {
211
+ return new Date(value).toISOString().slice(0, 19).replace('T', ' ');
212
+ }
213
+
214
+ function memoryDbPath(runtime: CliCommandRuntime): string {
215
+ return createShellPathService({
216
+ workingDirectory: runtime.workingDirectory,
217
+ homeDirectory: runtime.homeDirectory,
218
+ }).resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
219
+ }
220
+
221
+ async function withMemory<T>(runtime: CliCommandRuntime, fn: (context: MemoryContext) => Promise<T> | T): Promise<T> {
222
+ const path = memoryDbPath(runtime);
223
+ const embeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager: runtime.configManager });
224
+ const store = new MemoryStore(path, { embeddingRegistry });
225
+ await store.init();
226
+ const registry = new MemoryRegistry(store);
227
+ try {
228
+ return await fn({ registry, store, path });
229
+ } finally {
230
+ await store.save();
231
+ store.close();
232
+ }
233
+ }
234
+
235
+ function renderRecordLine(record: MemoryRecord, semanticEntry?: MemorySemanticSearchResult): string {
236
+ const tags = record.tags.length > 0 ? ` tags=${record.tags.join(',')}` : '';
237
+ const score = semanticEntry ? ` sim=${Math.round(semanticEntry.similarity * 100)}%` : '';
238
+ return ` ${record.id} ${record.scope}/${record.cls} ${record.reviewState} ${record.confidence}%${score}${tags} ${record.summary}`;
239
+ }
240
+
241
+ function renderRecordList(title: string, path: string, records: readonly MemoryRecord[], semanticResults: readonly MemorySemanticSearchResult[] = []): string {
242
+ if (records.length === 0) {
243
+ return [
244
+ title,
245
+ ` store: ${path}`,
246
+ ' No Agent memory records found.',
247
+ ' Add one with: goodvibes-agent memory add fact "Useful durable fact" --scope project',
248
+ ].join('\n');
249
+ }
250
+ return [
251
+ `${title} (${records.length})`,
252
+ ` store: ${path}`,
253
+ ...records.map((record) => renderRecordLine(record, semanticResults.find((entry) => entry.record.id === record.id))),
254
+ ].join('\n');
255
+ }
256
+
257
+ function renderRecord(record: MemoryRecord, links: readonly MemoryLink[]): string {
258
+ return [
259
+ `Memory ${record.id}`,
260
+ ` scope: ${record.scope}`,
261
+ ` class: ${record.cls}`,
262
+ ` review: ${record.reviewState}`,
263
+ ` confidence: ${record.confidence}`,
264
+ ` tags: ${record.tags.join(', ') || '(none)'}`,
265
+ ` created: ${timestamp(record.createdAt)}`,
266
+ ` updated: ${timestamp(record.updatedAt)}`,
267
+ record.reviewedAt ? ` reviewed: ${timestamp(record.reviewedAt)}${record.reviewedBy ? ` by ${record.reviewedBy}` : ''}` : '',
268
+ record.staleReason ? ` stale reason: ${record.staleReason}` : '',
269
+ '',
270
+ record.summary,
271
+ record.detail ? `\n${record.detail}` : '',
272
+ record.provenance.length > 0 ? '\nProvenance:' : '',
273
+ ...record.provenance.map((entry) => ` ${entry.kind}:${entry.ref}${entry.label ? ` (${entry.label})` : ''}`),
274
+ links.length > 0 ? '\nLinks:' : '',
275
+ ...links.map((link) => ` ${link.fromId} -> ${link.toId} [${link.relation}]`),
276
+ ].filter((line): line is string => Boolean(line)).join('\n');
277
+ }
278
+
279
+ function filterFromOptions(options: ParsedOptions, defaultLimit: number): MemorySearchFilter {
280
+ const filter: MemorySearchFilter = {
281
+ limit: parseLimit(optionValue(options, 'limit'), defaultLimit),
282
+ };
283
+ const scope = optionalScope(optionValue(options, 'scope'));
284
+ const cls = optionalClass(optionValue(options, 'cls'));
285
+ const tags = csvOption(options, 'tags');
286
+ if (scope !== undefined) filter.scope = scope;
287
+ if (cls !== undefined) filter.cls = cls;
288
+ if (tags !== undefined) filter.tags = [...tags];
289
+ return filter;
290
+ }
291
+
292
+ function addProvenance(provenance: ProvenanceLink[], kind: ProvenanceLinkKind, ref: string | undefined): void {
293
+ if (ref !== undefined && ref.trim().length > 0) provenance.push({ kind, ref: ref.trim() });
294
+ }
295
+
296
+ function provenanceFromOptions(options: ParsedOptions): readonly ProvenanceLink[] {
297
+ const provenance: ProvenanceLink[] = [];
298
+ addProvenance(provenance, 'session', optionValue(options, 'session'));
299
+ addProvenance(provenance, 'turn', optionValue(options, 'turn'));
300
+ addProvenance(provenance, 'task', optionValue(options, 'task'));
301
+ addProvenance(provenance, 'file', optionValue(options, 'file'));
302
+ if (provenance.length === 0) provenance.push({ kind: 'event', ref: 'cli' });
303
+ return provenance;
304
+ }
305
+
306
+ function usage(): string {
307
+ return [
308
+ 'Usage: goodvibes-agent memory <subcommand>',
309
+ ' list [class] [--scope <session|project|team>] [--limit <n>]',
310
+ ' search <query> [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>]',
311
+ ' add <class> <summary> [--scope <scope>] [--detail <text>] [--tags a,b] [--confidence <0-100>]',
312
+ ' show <id>',
313
+ ' queue [limit]',
314
+ ' review <id> <fresh|reviewed|stale|contradicted> [--confidence <0-100>] [--by <name>] [--reason <text>]',
315
+ ' stale <id> <reason>',
316
+ ' contradict <id> <reason>',
317
+ ' promote <id> <session|project|team> --yes',
318
+ ' link <fromId> <toId> <relation> --yes',
319
+ ' delete <id> --yes',
320
+ ' export <path> [--scope <scope>] [--cls <class>] --yes',
321
+ ' import <path> --yes',
322
+ ' handoff-inspect <path>',
323
+ ' vector [status|doctor|rebuild]',
324
+ '',
325
+ 'Agent memory is local to GoodVibes Agent and never falls back to default Knowledge/Wiki or non-Agent knowledge segments.',
326
+ ].join('\n');
327
+ }
328
+
329
+ function isRecord(value: unknown): value is Record<string, unknown> {
330
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
331
+ }
332
+
333
+ function isStringArray(value: unknown): value is string[] {
334
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
335
+ }
336
+
337
+ function isProvenanceLink(value: unknown): value is ProvenanceLink {
338
+ if (!isRecord(value)) return false;
339
+ return typeof value.ref === 'string' && typeof value.kind === 'string' && isProvenanceKind(value.kind);
340
+ }
341
+
342
+ function isMemoryRecord(value: unknown): value is MemoryRecord {
343
+ if (!isRecord(value)) return false;
344
+ return (
345
+ typeof value.id === 'string'
346
+ && typeof value.scope === 'string'
347
+ && isMemoryScope(value.scope)
348
+ && typeof value.cls === 'string'
349
+ && isMemoryClass(value.cls)
350
+ && typeof value.summary === 'string'
351
+ && (value.detail === undefined || typeof value.detail === 'string')
352
+ && isStringArray(value.tags)
353
+ && Array.isArray(value.provenance)
354
+ && value.provenance.every(isProvenanceLink)
355
+ && typeof value.reviewState === 'string'
356
+ && isReviewState(value.reviewState)
357
+ && typeof value.confidence === 'number'
358
+ && typeof value.createdAt === 'number'
359
+ && typeof value.updatedAt === 'number'
360
+ );
361
+ }
362
+
363
+ function isMemoryLink(value: unknown): value is MemoryLink {
364
+ if (!isRecord(value)) return false;
365
+ return typeof value.fromId === 'string'
366
+ && typeof value.toId === 'string'
367
+ && typeof value.relation === 'string'
368
+ && typeof value.createdAt === 'number';
369
+ }
370
+
371
+ function isMemoryBundle(value: unknown): value is MemoryBundle {
372
+ if (!isRecord(value)) return false;
373
+ return value.schemaVersion === 'v1'
374
+ && typeof value.exportedAt === 'number'
375
+ && (value.scope === 'all' || (typeof value.scope === 'string' && isMemoryScope(value.scope)))
376
+ && typeof value.recordCount === 'number'
377
+ && typeof value.linkCount === 'number'
378
+ && Array.isArray(value.records)
379
+ && value.records.every(isMemoryRecord)
380
+ && Array.isArray(value.links)
381
+ && value.links.every(isMemoryLink);
382
+ }
383
+
384
+ function readBundle(path: string): MemoryBundle {
385
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'));
386
+ if (!isMemoryBundle(parsed)) throw new Error('Invalid Agent memory bundle.');
387
+ for (const record of parsed.records) {
388
+ assertNoSecretLikeText([
389
+ record.summary,
390
+ record.detail ?? '',
391
+ ...record.tags,
392
+ ...record.provenance.map((entry) => `${entry.kind}:${entry.ref} ${entry.label ?? ''}`),
393
+ ]);
394
+ }
395
+ return parsed;
396
+ }
397
+
398
+ function resolvePath(runtime: CliCommandRuntime, path: string): string {
399
+ return resolve(runtime.workingDirectory, path);
400
+ }
401
+
402
+ function writeBundle(path: string, bundle: MemoryBundle): void {
403
+ mkdirSync(dirname(path), { recursive: true });
404
+ writeFileSync(path, `${JSON.stringify(bundle, null, 2)}\n`);
405
+ }
406
+
407
+ function renderBundleInspection(path: string, bundle: MemoryBundle): string {
408
+ return [
409
+ 'Agent memory handoff bundle',
410
+ ` path: ${path}`,
411
+ ` scope: ${bundle.scope}`,
412
+ ` records: ${bundle.recordCount}`,
413
+ ` links: ${bundle.linkCount}`,
414
+ ...bundle.records.slice(0, 20).map((record) => ` ${record.id} ${record.scope}/${record.cls} ${record.reviewState} ${record.summary}`),
415
+ bundle.records.length > 20 ? ` ... ${bundle.records.length - 20} more` : '',
416
+ ].filter((line): line is string => Boolean(line)).join('\n');
417
+ }
418
+
419
+ function errorOutput(runtime: CliCommandRuntime, error: unknown): CliCommandOutput {
420
+ const message = error instanceof Error ? error.message : String(error);
421
+ const exitCode = message.startsWith('Usage:') || message.startsWith('Invalid ') || message.startsWith('--') ? 2 : 1;
422
+ return failure(runtime, 'agent.memory.error', message, exitCode);
423
+ }
424
+
425
+ async function handleList(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
426
+ const options = parseOptions(args);
427
+ const positionalClass = options.positionals[0];
428
+ const filter = filterFromOptions(options, 50);
429
+ if (positionalClass !== undefined) filter.cls = requireClass(positionalClass);
430
+ const records = context.registry.search(filter);
431
+ const data: MemoryListData = { path: context.path, records, filter };
432
+ return success(runtime, 'agent.memory.list', data, renderRecordList('Agent memory', context.path, records));
433
+ }
434
+
435
+ async function handleSearch(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
436
+ const options = parseOptions(args);
437
+ const query = options.positionals.join(' ').trim();
438
+ if (!query) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory search <query> [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>]', 2);
439
+ const filter = filterFromOptions(options, 20);
440
+ filter.query = query;
441
+ const semantic = hasFlag(options, 'semantic') || hasFlag(options, 'vector');
442
+ if (semantic) filter.semantic = true;
443
+ const semanticResults = semantic ? context.registry.searchSemantic(filter) : [];
444
+ const records = semantic ? semanticResults.map((entry) => entry.record) : context.registry.search(filter);
445
+ const data: MemorySearchData = { path: context.path, records, filter, semantic, semanticResults };
446
+ return success(runtime, 'agent.memory.search', data, renderRecordList(`Agent memory matching "${query}"`, context.path, records, semanticResults));
447
+ }
448
+
449
+ async function handleAdd(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
450
+ const options = parseOptions(args);
451
+ const [classRaw, ...summaryParts] = options.positionals;
452
+ const cls = requireClass(classRaw);
453
+ const summary = summaryParts.join(' ').trim();
454
+ if (!summary) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory add <class> <summary> [--scope <scope>] [--detail <text>] [--tags a,b]', 2);
455
+ const detail = optionValue(options, 'detail');
456
+ const tags = csvOption(options, 'tags');
457
+ assertNoSecretLikeText([summary, detail ?? '', ...(tags ?? [])]);
458
+ const reviewState = optionValue(options, 'review-state');
459
+ if (reviewState !== undefined && !isReviewState(reviewState)) {
460
+ return failure(runtime, 'invalid_memory_command', `Invalid review state "${reviewState}". Valid: ${VALID_REVIEW_STATES.join(', ')}`, 2);
461
+ }
462
+ const record = await context.registry.add({
463
+ scope: optionalScope(optionValue(options, 'scope')) ?? 'project',
464
+ cls,
465
+ summary,
466
+ detail,
467
+ tags: tags === undefined ? undefined : [...tags],
468
+ provenance: [...provenanceFromOptions(options)],
469
+ review: {
470
+ state: reviewState,
471
+ confidence: parseConfidence(optionValue(options, 'confidence')),
472
+ reviewedBy: optionValue(options, 'by'),
473
+ },
474
+ });
475
+ return success(runtime, 'agent.memory.add', record, `Agent memory added: ${record.id}`);
476
+ }
477
+
478
+ async function handleShow(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
479
+ const id = args[0];
480
+ if (!id) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory show <id>', 2);
481
+ const record = context.registry.get(id);
482
+ if (!record) return failure(runtime, 'memory_not_found', `Memory record not found: ${id}`, 1);
483
+ return success(runtime, 'agent.memory.show', { record, links: context.registry.linksFor(id) }, renderRecord(record, context.registry.linksFor(id)));
484
+ }
485
+
486
+ async function handleQueue(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
487
+ const limit = parseLimit(args[0], 10);
488
+ const records = context.registry.reviewQueue(limit);
489
+ return success(runtime, 'agent.memory.queue', { path: context.path, records, limit }, renderRecordList('Agent memory review queue', context.path, records));
490
+ }
491
+
492
+ async function handleReview(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
493
+ const options = parseOptions(args);
494
+ const [id, stateRaw] = options.positionals;
495
+ if (!id || !stateRaw || !isReviewState(stateRaw)) {
496
+ return failure(runtime, 'invalid_memory_command', `Usage: goodvibes-agent memory review <id> <${VALID_REVIEW_STATES.join('|')}> [--confidence <0-100>]`, 2);
497
+ }
498
+ const record = context.registry.review(id, {
499
+ state: stateRaw,
500
+ confidence: parseConfidence(optionValue(options, 'confidence')),
501
+ reviewedBy: optionValue(options, 'by') ?? 'operator',
502
+ staleReason: optionValue(options, 'reason'),
503
+ });
504
+ if (!record) return failure(runtime, 'memory_not_found', `Memory record not found: ${id}`, 1);
505
+ return success(runtime, 'agent.memory.review', record, `Agent memory reviewed: ${record.id} ${record.reviewState} ${record.confidence}%`);
506
+ }
507
+
508
+ async function handleReviewShortcut(runtime: CliCommandRuntime, context: MemoryContext, state: Extract<MemoryReviewState, 'stale' | 'contradicted'>, args: readonly string[]): Promise<CliCommandOutput> {
509
+ const [id, ...reasonParts] = args;
510
+ if (!id || reasonParts.length === 0) {
511
+ return failure(runtime, 'invalid_memory_command', `Usage: goodvibes-agent memory ${state === 'stale' ? 'stale' : 'contradict'} <id> <reason>`, 2);
512
+ }
513
+ const record = context.registry.review(id, {
514
+ state,
515
+ reviewedBy: 'operator',
516
+ staleReason: reasonParts.join(' '),
517
+ });
518
+ if (!record) return failure(runtime, 'memory_not_found', `Memory record not found: ${id}`, 1);
519
+ return success(runtime, `agent.memory.${state}`, record, `Agent memory marked ${state}: ${record.id}`);
520
+ }
521
+
522
+ async function handlePromote(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
523
+ const options = parseOptions(args);
524
+ const [id, scopeRaw] = options.positionals;
525
+ if (!id || !scopeRaw) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory promote <id> <session|project|team> --yes', 2);
526
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to promote memory record ${id} without --yes.`, 2);
527
+ const record = context.registry.update(id, { scope: requireScope(scopeRaw) });
528
+ if (!record) return failure(runtime, 'memory_not_found', `Memory record not found: ${id}`, 1);
529
+ return success(runtime, 'agent.memory.promote', record, `Agent memory promoted: ${record.id} -> ${record.scope}`);
530
+ }
531
+
532
+ async function handleLink(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
533
+ const options = parseOptions(args);
534
+ const [fromId, toId, relation] = options.positionals;
535
+ if (!fromId || !toId || !relation) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory link <fromId> <toId> <relation> --yes', 2);
536
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to link memory records ${fromId} and ${toId} without --yes.`, 2);
537
+ const link = await context.registry.link(fromId, toId, relation);
538
+ if (!link) return failure(runtime, 'memory_link_failed', 'Memory link failed; check that both records exist.', 1);
539
+ return success(runtime, 'agent.memory.link', link, `Agent memory linked: ${fromId} -> ${toId} [${relation}]`);
540
+ }
541
+
542
+ async function handleDelete(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
543
+ const options = parseOptions(args);
544
+ const id = options.positionals[0];
545
+ if (!id) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory delete <id> --yes', 2);
546
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete memory record ${id} without --yes.`, 2);
547
+ if (!context.registry.delete(id)) return failure(runtime, 'memory_not_found', `Memory record not found: ${id}`, 1);
548
+ return success(runtime, 'agent.memory.delete', { id }, `Agent memory deleted: ${id}`);
549
+ }
550
+
551
+ async function handleExport(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
552
+ const options = parseOptions(args);
553
+ const pathArg = options.positionals[0];
554
+ if (!pathArg) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory export <path> [--scope <scope>] [--cls <class>] --yes', 2);
555
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to export Agent memory bundle to ${pathArg} without --yes.`, 2);
556
+ const filter = filterFromOptions(options, 200);
557
+ const path = resolvePath(runtime, pathArg);
558
+ const bundle = context.registry.exportBundle(filter);
559
+ writeBundle(path, bundle);
560
+ return success(runtime, 'agent.memory.export', { path, bundle }, `Agent memory exported: ${bundle.recordCount} record(s), ${bundle.linkCount} link(s) -> ${path}`);
561
+ }
562
+
563
+ async function handleImport(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
564
+ const options = parseOptions(args);
565
+ const pathArg = options.positionals[0];
566
+ if (!pathArg) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory import <path> --yes', 2);
567
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to import Agent memory bundle from ${pathArg} without --yes.`, 2);
568
+ const path = resolvePath(runtime, pathArg);
569
+ const bundle = readBundle(path);
570
+ const result = await context.registry.importBundle(bundle);
571
+ return success(runtime, 'agent.memory.import', { path, result }, `Agent memory imported: ${result.importedRecords} record(s), ${result.importedLinks} link(s); skipped ${result.skippedRecords}`);
572
+ }
573
+
574
+ async function handleInspect(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
575
+ const pathArg = args[0];
576
+ if (!pathArg) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory handoff-inspect <path>', 2);
577
+ const path = resolvePath(runtime, pathArg);
578
+ const bundle = readBundle(path);
579
+ return success(runtime, 'agent.memory.handoffInspect', { path, bundle }, renderBundleInspection(path, bundle));
580
+ }
581
+
582
+ async function handleVector(runtime: CliCommandRuntime, context: MemoryContext, args: readonly string[]): Promise<CliCommandOutput> {
583
+ const sub = (args[0] ?? 'status').toLowerCase();
584
+ if (sub === 'status') {
585
+ const stats = context.registry.vectorStats();
586
+ return success(runtime, 'agent.memory.vector.status', stats, [
587
+ 'Agent memory vector index',
588
+ ` backend: ${stats.backend}`,
589
+ ` enabled: ${stats.enabled ? 'yes' : 'no'}`,
590
+ ` available: ${stats.available ? 'yes' : 'no'}`,
591
+ ` dimensions: ${stats.dimensions}`,
592
+ ` indexed records: ${stats.indexedRecords}`,
593
+ ...(stats.path ? [` path: ${stats.path}`] : []),
594
+ ...(stats.error ? [` error: ${stats.error}`] : []),
595
+ ].join('\n'));
596
+ }
597
+ if (sub === 'doctor') {
598
+ const report = await context.registry.doctor();
599
+ return success(runtime, 'agent.memory.vector.doctor', report, [
600
+ 'Agent memory vector doctor',
601
+ ` vector backend: ${report.vector.backend}`,
602
+ ` vector available: ${report.vector.available ? 'yes' : 'no'}`,
603
+ ` indexed records: ${report.vector.indexedRecords}`,
604
+ ` active embedding provider: ${report.embeddings.activeProviderId || '(none)'}`,
605
+ ` embedding providers: ${report.embeddings.providers.length}`,
606
+ ...report.embeddings.warnings.map((warning) => ` warning: ${warning}`),
607
+ ].join('\n'));
608
+ }
609
+ if (sub === 'rebuild') {
610
+ const stats = await context.registry.rebuildVectorsAsync();
611
+ return success(runtime, 'agent.memory.vector.rebuild', stats, [
612
+ 'Agent memory vector rebuild complete',
613
+ ` backend: ${stats.backend}`,
614
+ ` indexed records: ${stats.indexedRecords}`,
615
+ ...(stats.error ? [` error: ${stats.error}`] : []),
616
+ ].join('\n'));
617
+ }
618
+ return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory vector [status|doctor|rebuild]', 2);
619
+ }
620
+
621
+ export async function handleMemoryCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
622
+ try {
623
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
624
+ const normalized = sub.toLowerCase();
625
+ if (normalized === 'handoff-inspect' || normalized === 'inspect') return handleInspect(runtime, rest);
626
+ return await withMemory(runtime, async (context) => {
627
+ if (normalized === 'list' || normalized === 'ls') return handleList(runtime, context, rest);
628
+ if (normalized === 'search' || normalized === 'find') return handleSearch(runtime, context, rest);
629
+ if (normalized === 'add' || normalized === 'create') return handleAdd(runtime, context, rest);
630
+ if (normalized === 'show' || normalized === 'get') return handleShow(runtime, context, rest);
631
+ if (normalized === 'queue') return handleQueue(runtime, context, rest);
632
+ if (normalized === 'review') return handleReview(runtime, context, rest);
633
+ if (normalized === 'stale') return handleReviewShortcut(runtime, context, 'stale', rest);
634
+ if (normalized === 'contradict' || normalized === 'contradicted') return handleReviewShortcut(runtime, context, 'contradicted', rest);
635
+ if (normalized === 'promote') return handlePromote(runtime, context, rest);
636
+ if (normalized === 'link') return handleLink(runtime, context, rest);
637
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') return handleDelete(runtime, context, rest);
638
+ if (normalized === 'export' || normalized === 'handoff-export' || normalized === 'share') return handleExport(runtime, context, rest);
639
+ if (normalized === 'import' || normalized === 'handoff-import') return handleImport(runtime, context, rest);
640
+ if (normalized === 'vector' || normalized === 'vectors') return handleVector(runtime, context, rest);
641
+ return failure(runtime, 'invalid_memory_command', usage(), 2);
642
+ });
643
+ } catch (error) {
644
+ return errorOutput(runtime, error);
645
+ }
646
+ }
@@ -50,6 +50,15 @@ const FORBIDDEN_TARBALL_DOCS = [
50
50
  ['docs/home', 'assistant-surface.md'].join(''),
51
51
  'docs/wrfc/',
52
52
  ] as const;
53
+ const FORBIDDEN_TARBALL_FILES = new Set([
54
+ 'src/panels/agent-inspector-panel.ts',
55
+ 'src/panels/agent-inspector-shared.ts',
56
+ 'src/panels/agent-logs-panel.ts',
57
+ 'src/panels/agent-logs-shared.ts',
58
+ 'src/tools/wrfc-agent-guard.ts',
59
+ 'src/renderer/agent-detail-modal.ts',
60
+ 'src/renderer/process-summary.ts',
61
+ ]);
53
62
  const PACKAGE_FACING_TEXT_PATHS = [
54
63
  'README.md',
55
64
  'CHANGELOG.md',
@@ -216,6 +225,7 @@ export function verifyPackageCliInstall(root: string): PackageCliVerificationRep
216
225
  const requiredPathsPresent = REQUIRED_TARBALL_PATHS.filter((path) => pack.files.includes(path));
217
226
  const forbiddenPaths = pack.files.filter((path) => {
218
227
  if (FORBIDDEN_TARBALL_PREFIXES.some((prefix) => path.startsWith(prefix))) return true;
228
+ if (FORBIDDEN_TARBALL_FILES.has(path)) return true;
219
229
  return FORBIDDEN_TARBALL_DOCS.some((docPath) => path === docPath || path.startsWith(docPath));
220
230
  });
221
231
  const issues: string[] = [];