@pellux/goodvibes-agent 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to GoodVibes Agent will be recorded here.
4
4
 
5
+ ## 0.1.91 - 2026-06-01
6
+
7
+ - efbb82a Expand Agent Knowledge CLI management
8
+
9
+ ## 0.1.90 - 2026-06-01
10
+
11
+ - 19f67ea Polish Agent command guidance
12
+
5
13
  ## 0.1.89 - 2026-06-01
6
14
 
7
15
  - 6be201c Fix Agent workspace command targets
package/README.md CHANGED
@@ -17,6 +17,8 @@ goodvibes-agent --help
17
17
  goodvibes-agent status
18
18
  goodvibes-agent profiles templates
19
19
  goodvibes-agent knowledge status
20
+ goodvibes-agent knowledge list --kind sources
21
+ goodvibes-agent knowledge import-urls ./agent-sources.txt --yes
20
22
  ```
21
23
 
22
24
  If `goodvibes-agent` is not found after installation, add Bun's global bin directory to `PATH`:
@@ -95,6 +97,8 @@ GoodVibes Agent owns the operator assistant TUI: serial assistant flow, proactiv
95
97
 
96
98
  Agent Knowledge/Wiki is its own product segment. Agent uses `/api/goodvibes-agent/knowledge/*` and must not fall back to default Knowledge/Wiki or other product-specific knowledge routes.
97
99
 
100
+ Agent Knowledge CLI commands can ask/search, inspect sources/nodes/issues, inspect connectors, ingest a URL, import URL/bookmark files, and reindex the Agent segment. Confirmed mutations require `--yes`.
101
+
98
102
  GoodVibes TUI owns coding execution: file edits, git/worktree workflows, coding panels, execution isolation UX, and WRFC execution. Agent may delegate explicit build/fix/review work to TUI through public runtime/session contracts; normal assistant chat must not use shared coding sessions.
99
103
 
100
104
  ## Package Docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-agent",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "private": false,
5
5
  "description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
6
6
  "type": "module",
@@ -5,6 +5,19 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
  import { SDK_VERSION, VERSION } from '../version.ts';
6
6
  import type { CliCommandOutput } from './types.ts';
7
7
  import type { CliCommandRuntime } from './management.ts';
8
+ import {
9
+ formatAsk,
10
+ formatBatchIngest,
11
+ formatConnectors,
12
+ formatEntityList,
13
+ formatFailure,
14
+ formatIngest,
15
+ formatItem,
16
+ formatMap,
17
+ formatReindex,
18
+ formatSearch,
19
+ formatStatus,
20
+ } from './agent-knowledge-format.ts';
8
21
  import { formatJsonOrText, yesNo } from './management.ts';
9
22
 
10
23
  type JsonRecord = Record<string, unknown>;
@@ -52,10 +65,46 @@ const AGENT_KNOWLEDGE_METHODS = {
52
65
  kind: 'agentKnowledge.search',
53
66
  route: '/api/goodvibes-agent/knowledge/search',
54
67
  },
68
+ sourcesList: {
69
+ kind: 'agentKnowledge.sources.list',
70
+ route: '/api/goodvibes-agent/knowledge/sources',
71
+ },
72
+ nodesList: {
73
+ kind: 'agentKnowledge.nodes.list',
74
+ route: '/api/goodvibes-agent/knowledge/nodes',
75
+ },
76
+ issuesList: {
77
+ kind: 'agentKnowledge.issues.list',
78
+ route: '/api/goodvibes-agent/knowledge/issues',
79
+ },
80
+ itemGet: {
81
+ kind: 'agentKnowledge.item.get',
82
+ route: '/api/goodvibes-agent/knowledge/items/{id}',
83
+ },
84
+ map: {
85
+ kind: 'agentKnowledge.map',
86
+ route: '/api/goodvibes-agent/knowledge/map',
87
+ },
88
+ connectorsList: {
89
+ kind: 'agentKnowledge.connectors.list',
90
+ route: '/api/goodvibes-agent/knowledge/connectors',
91
+ },
55
92
  ingestUrl: {
56
93
  kind: 'agentKnowledge.ingest.url',
57
94
  route: '/api/goodvibes-agent/knowledge/ingest/url',
58
95
  },
96
+ ingestUrls: {
97
+ kind: 'agentKnowledge.ingest.urls',
98
+ route: '/api/goodvibes-agent/knowledge/ingest/urls',
99
+ },
100
+ ingestBookmarks: {
101
+ kind: 'agentKnowledge.ingest.bookmarks',
102
+ route: '/api/goodvibes-agent/knowledge/ingest/bookmarks',
103
+ },
104
+ reindex: {
105
+ kind: 'agentKnowledge.reindex',
106
+ route: '/api/goodvibes-agent/knowledge/reindex',
107
+ },
59
108
  } as const;
60
109
 
61
110
  const DELEGATION_METHOD = {
@@ -79,25 +128,6 @@ function readString(record: JsonRecord | null, key: string): string | null {
79
128
  return typeof value === 'string' ? value : null;
80
129
  }
81
130
 
82
- function readNumber(record: JsonRecord | null, key: string): number | null {
83
- const value = record?.[key];
84
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
85
- }
86
-
87
- function readBoolean(record: JsonRecord | null, key: string): boolean | null {
88
- const value = record?.[key];
89
- return typeof value === 'boolean' ? value : null;
90
- }
91
-
92
- function readArray(record: JsonRecord | null, key: string): readonly unknown[] {
93
- const value = record?.[key];
94
- return Array.isArray(value) ? value : [];
95
- }
96
-
97
- function cleanInline(value: unknown): string {
98
- return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
99
- }
100
-
101
131
  function commandValues(args: readonly string[]): string[] {
102
132
  const values: string[] = [];
103
133
  for (let index = 0; index < args.length; index += 1) {
@@ -265,6 +295,50 @@ async function postAgentKnowledgeJson<TData>(
265
295
  return parsed as TData;
266
296
  }
267
297
 
298
+ function queryRoute(route: string, query: JsonRecord): string {
299
+ const params = new URLSearchParams();
300
+ for (const [key, value] of Object.entries(query)) {
301
+ if (value === undefined || value === null || value === '') continue;
302
+ if (Array.isArray(value)) {
303
+ for (const item of value) {
304
+ if (typeof item === 'string' && item.trim().length > 0) params.append(key, item);
305
+ }
306
+ continue;
307
+ }
308
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
309
+ params.set(key, String(value));
310
+ }
311
+ }
312
+ const suffix = params.toString();
313
+ return suffix ? `${route}?${suffix}` : route;
314
+ }
315
+
316
+ async function getAgentKnowledgeJson<TData>(
317
+ connection: AgentDaemonConnection,
318
+ route: string,
319
+ query: JsonRecord = {},
320
+ ): Promise<TData> {
321
+ const response = await fetch(`${connection.baseUrl}${queryRoute(route, query)}`, {
322
+ headers: {
323
+ authorization: `Bearer ${connection.token ?? ''}`,
324
+ },
325
+ });
326
+ const text = await response.text();
327
+ let parsed: unknown = text;
328
+ if (text.trim()) {
329
+ try {
330
+ parsed = JSON.parse(text) as unknown;
331
+ } catch {
332
+ parsed = text;
333
+ }
334
+ }
335
+ if (!response.ok) {
336
+ const detail = isRecord(parsed) && typeof parsed.error === 'string' ? parsed.error : text;
337
+ throw new Error(`HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ''}`);
338
+ }
339
+ return parsed as TData;
340
+ }
341
+
268
342
  function findDisallowedKnowledgeScopeFlag(args: readonly string[]): string | null {
269
343
  const disallowed = [
270
344
  '--space',
@@ -292,111 +366,6 @@ function formatScopeFlagRejection(flag: string): string {
292
366
  ].join('\n');
293
367
  }
294
368
 
295
- function sourceLine(value: unknown): string {
296
- const record = isRecord(value) ? value : {};
297
- const title = cleanInline(record.title)
298
- || cleanInline(record.canonicalUri)
299
- || cleanInline(record.sourceUri)
300
- || cleanInline(record.url)
301
- || cleanInline(record.id)
302
- || 'untitled';
303
- const type = cleanInline(record.sourceType) || cleanInline(record.type) || 'source';
304
- const url = cleanInline(record.canonicalUri) || cleanInline(record.sourceUri) || cleanInline(record.url);
305
- return url && url !== title ? `[${type}] ${title} (${url})` : `[${type}] ${title}`;
306
- }
307
-
308
- function resultLine(value: unknown): string {
309
- const record = isRecord(value) ? value : {};
310
- const title = cleanInline(record.title) || cleanInline(record.id) || 'untitled';
311
- const id = cleanInline(record.id);
312
- const type = cleanInline(record.type) || cleanInline(record.kind) || cleanInline(record.sourceType) || 'result';
313
- const score = readNumber(record, 'score');
314
- const url = cleanInline(record.url) || cleanInline(record.canonicalUri) || cleanInline(record.sourceUri);
315
- const snippet = cleanInline(record.snippet) || cleanInline(record.summary) || cleanInline(record.text);
316
- const parts = [
317
- `[${type}] ${title}`,
318
- id && id !== title ? `id=${id}` : '',
319
- score !== null ? `score=${score.toFixed(3)}` : '',
320
- url ? `url=${url}` : '',
321
- ].filter((part) => part.length > 0);
322
- return snippet ? `${parts.join(' ')}\n ${snippet}` : parts.join(' ');
323
- }
324
-
325
- function formatStatus(data: unknown): string {
326
- const record = isRecord(data) ? data : {};
327
- const ready = readBoolean(record, 'ready');
328
- const sourceCount = readNumber(record, 'sourceCount');
329
- const nodeCount = readNumber(record, 'nodeCount');
330
- const issueCount = readNumber(record, 'issueCount');
331
- const edgeCount = readNumber(record, 'edgeCount');
332
- const storagePath = readString(record, 'storagePath');
333
- return [
334
- 'Agent Knowledge status',
335
- ` ready: ${ready === null ? 'unknown' : yesNo(ready)}`,
336
- ` sources: ${sourceCount ?? 'unknown'}`,
337
- ` nodes: ${nodeCount ?? 'unknown'}`,
338
- ` edges: ${edgeCount ?? 'unknown'}`,
339
- ` issues: ${issueCount ?? 'unknown'}`,
340
- storagePath ? ` storage: ${storagePath}` : null,
341
- ' route: /api/goodvibes-agent/knowledge/status',
342
- ].filter((line): line is string => Boolean(line)).join('\n');
343
- }
344
-
345
- function formatAsk(data: unknown, query: string): string {
346
- const record = isRecord(data) ? data : {};
347
- const answer = isRecord(record.answer) ? record.answer : record;
348
- const text = cleanInline(answer.text) || cleanInline(record.answer) || 'No answer returned.';
349
- const confidence = readNumber(answer, 'confidence') ?? readNumber(record, 'confidence');
350
- const synthesized = readBoolean(answer, 'synthesized');
351
- const sources = readArray(answer, 'sources');
352
- const facts = readArray(answer, 'facts');
353
- const gaps = readArray(answer, 'gaps');
354
- const lines = [
355
- `Agent Knowledge answer: ${query}`,
356
- text,
357
- '',
358
- `confidence: ${confidence ?? 'unknown'}${synthesized === null ? '' : ` synthesized: ${yesNo(synthesized)}`}`,
359
- ];
360
- if (sources.length > 0) {
361
- lines.push('', 'Sources:', ...sources.slice(0, 8).map((source) => ` - ${sourceLine(source)}`));
362
- }
363
- if (facts.length > 0) lines.push('', `Facts: ${facts.length}`);
364
- if (gaps.length > 0) lines.push('', `Gaps: ${gaps.length}`);
365
- return lines.join('\n');
366
- }
367
-
368
- function formatSearch(data: unknown, query: string): string {
369
- const record = isRecord(data) ? data : {};
370
- const items = readArray(record, 'items');
371
- const results = items.length > 0 ? items : readArray(record, 'results');
372
- if (results.length === 0) {
373
- return [
374
- `Agent Knowledge search: ${query}`,
375
- ' no results',
376
- ' route: /api/goodvibes-agent/knowledge/search',
377
- ].join('\n');
378
- }
379
- return [
380
- `Agent Knowledge search: ${query}`,
381
- ...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
382
- ].join('\n');
383
- }
384
-
385
- function formatIngest(data: unknown, url: string): string {
386
- const record = isRecord(data) ? data : {};
387
- const source = isRecord(record.source) ? record.source : record;
388
- const sourceId = cleanInline(source.id);
389
- const canonicalUri = cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || url;
390
- const artifactId = cleanInline(record.artifactId);
391
- return [
392
- 'Agent Knowledge ingest-url accepted',
393
- ` source: ${sourceId || '(pending)'}`,
394
- ` url: ${canonicalUri}`,
395
- artifactId ? ` artifact: ${artifactId}` : null,
396
- ' route: /api/goodvibes-agent/knowledge/ingest/url',
397
- ].filter((line): line is string => Boolean(line)).join('\n');
398
- }
399
-
400
369
  function buildDelegationBody(task: string, wrfcRequested: boolean): string {
401
370
  return [
402
371
  'GoodVibes Agent explicit build delegation.',
@@ -414,25 +383,6 @@ function buildDelegationBody(task: string, wrfcRequested: boolean): string {
414
383
  ].join('\n');
415
384
  }
416
385
 
417
- function formatFailure(failure: AgentKnowledgeFailure, json: boolean): string {
418
- if (json) return JSON.stringify(failure, null, 2);
419
- return [
420
- `Agent Knowledge error: ${failure.kind}`,
421
- ` ${failure.error}`,
422
- ` runtime: ${failure.baseUrl}`,
423
- ` route: ${failure.route}`,
424
- failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
425
- ? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
426
- : null,
427
- failure.kind === 'version_mismatch'
428
- ? ' next: update/restart the external GoodVibes runtime so /status matches the Agent SDK pin.'
429
- : null,
430
- failure.kind === 'daemon_route_unavailable'
431
- ? ' next: update/restart the external GoodVibes runtime to the SDK version required by this Agent package.'
432
- : null,
433
- ].filter((line): line is string => Boolean(line)).join('\n');
434
- }
435
-
436
386
  async function runKnowledgeCall<TData>(
437
387
  runtime: CliCommandRuntime,
438
388
  method: DaemonCallMethod,
@@ -524,6 +474,63 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
524
474
  };
525
475
  }
526
476
 
477
+ if (normalized === 'list' || normalized === 'sources' || normalized === 'nodes' || normalized === 'issues') {
478
+ const requestedKind = normalized === 'list' ? (readOptionValue(rest, '--kind') ?? 'sources').toLowerCase() : normalized;
479
+ const kind = requestedKind === 'nodes' || requestedKind === 'issues' ? requestedKind : 'sources';
480
+ const limit = readPositiveInt(rest, '--limit', 10);
481
+ const method = kind === 'sources'
482
+ ? AGENT_KNOWLEDGE_METHODS.sourcesList
483
+ : kind === 'nodes'
484
+ ? AGENT_KNOWLEDGE_METHODS.nodesList
485
+ : AGENT_KNOWLEDGE_METHODS.issuesList;
486
+ const result = await runKnowledgeCall(runtime, method, async (connection) => (
487
+ await getAgentKnowledgeJson(connection, method.route, { limit })
488
+ ));
489
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
490
+ return {
491
+ output: formatJsonOrText(runtime.cli)(result, formatEntityList(result.data, kind, limit)),
492
+ exitCode: 0,
493
+ };
494
+ }
495
+
496
+ if (normalized === 'get') {
497
+ const [id] = commandValues(rest);
498
+ if (!id) return { output: 'Usage: goodvibes-agent knowledge get <source|node|issue id>', exitCode: 2 };
499
+ const route = `/api/goodvibes-agent/knowledge/items/${encodeURIComponent(id)}`;
500
+ const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.itemGet, async (connection) => (
501
+ await getAgentKnowledgeJson(connection, route)
502
+ ));
503
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
504
+ return {
505
+ output: formatJsonOrText(runtime.cli)(result, formatItem(result.data, id)),
506
+ exitCode: 0,
507
+ };
508
+ }
509
+
510
+ if (normalized === 'map') {
511
+ const limit = readPositiveInt(rest, '--limit', 50);
512
+ const query = commandValues(rest).join(' ').trim();
513
+ const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.map, async (connection) => (
514
+ await getAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.map.route, { limit, query })
515
+ ));
516
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
517
+ return {
518
+ output: formatJsonOrText(runtime.cli)(result, formatMap(result.data)),
519
+ exitCode: 0,
520
+ };
521
+ }
522
+
523
+ if (normalized === 'connectors') {
524
+ const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorsList, async (connection) => (
525
+ await getAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.connectorsList.route)
526
+ ));
527
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
528
+ return {
529
+ output: formatJsonOrText(runtime.cli)(result, formatConnectors(result.data)),
530
+ exitCode: 0,
531
+ };
532
+ }
533
+
527
534
  if (normalized === 'ingest-url') {
528
535
  const values = commandValues(rest);
529
536
  const url = values[0];
@@ -558,8 +565,69 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
558
565
  };
559
566
  }
560
567
 
568
+ if (normalized === 'import-urls' || normalized === 'import-bookmarks') {
569
+ const values = commandValues(rest);
570
+ const path = values[0];
571
+ const method = normalized === 'import-urls'
572
+ ? AGENT_KNOWLEDGE_METHODS.ingestUrls
573
+ : AGENT_KNOWLEDGE_METHODS.ingestBookmarks;
574
+ if (!path) {
575
+ return {
576
+ output: `Usage: goodvibes-agent knowledge ${normalized} <path> [--allow-private-hosts] --yes`,
577
+ exitCode: 2,
578
+ };
579
+ }
580
+ if (!confirmation.present) {
581
+ const failure = {
582
+ ok: false,
583
+ kind: 'confirmation_required',
584
+ error: `Refusing to import ${path} into Agent Knowledge without --yes.`,
585
+ route: method.route,
586
+ };
587
+ return {
588
+ output: json ? JSON.stringify(failure, null, 2) : `${failure.error}\nUsage: goodvibes-agent knowledge ${normalized} <path> [--allow-private-hosts] --yes`,
589
+ exitCode: 2,
590
+ };
591
+ }
592
+ const result = await runKnowledgeCall(runtime, method, async (connection) => (
593
+ await postAgentKnowledgeJson(connection, method.route, {
594
+ path,
595
+ allowPrivateHosts: hasFlag(rest, '--allow-private-hosts'),
596
+ metadata: { originSurface: 'goodvibes-agent-cli' },
597
+ })
598
+ ));
599
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
600
+ return {
601
+ output: formatJsonOrText(runtime.cli)(result, formatBatchIngest(result.data, normalized)),
602
+ exitCode: 0,
603
+ };
604
+ }
605
+
606
+ if (normalized === 'reindex') {
607
+ if (!confirmation.present) {
608
+ const failure = {
609
+ ok: false,
610
+ kind: 'confirmation_required',
611
+ error: 'Refusing to reindex Agent Knowledge without --yes.',
612
+ route: AGENT_KNOWLEDGE_METHODS.reindex.route,
613
+ };
614
+ return {
615
+ output: json ? JSON.stringify(failure, null, 2) : `${failure.error}\nUsage: goodvibes-agent knowledge reindex --yes`,
616
+ exitCode: 2,
617
+ };
618
+ }
619
+ const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.reindex, async (connection) => (
620
+ await postAgentKnowledgeJson(connection, AGENT_KNOWLEDGE_METHODS.reindex.route, {})
621
+ ));
622
+ if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
623
+ return {
624
+ output: formatJsonOrText(runtime.cli)(result, formatReindex(result.data)),
625
+ exitCode: 0,
626
+ };
627
+ }
628
+
561
629
  return {
562
- output: 'Usage: goodvibes-agent knowledge [status|ask <question>|search <query>|ingest-url <url> --yes]',
630
+ output: 'Usage: goodvibes-agent knowledge [status|ask <question>|search <query>|list|sources|nodes|issues|get <id>|map|connectors|ingest-url <url> --yes|import-urls <path> --yes|import-bookmarks <path> --yes|reindex --yes]',
563
631
  exitCode: 2,
564
632
  };
565
633
  }
@@ -0,0 +1,325 @@
1
+ type JsonRecord = Record<string, unknown>;
2
+
3
+ export interface AgentKnowledgeFailureLike {
4
+ readonly ok: false;
5
+ readonly kind: string;
6
+ readonly error: string;
7
+ readonly baseUrl: string;
8
+ readonly route: string;
9
+ readonly daemonVersion?: string;
10
+ readonly expectedSdkVersion?: string;
11
+ }
12
+
13
+ export function isRecord(value: unknown): value is JsonRecord {
14
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+
17
+ export function readString(record: JsonRecord | null, key: string): string | null {
18
+ const value = record?.[key];
19
+ return typeof value === 'string' ? value : null;
20
+ }
21
+
22
+ function readNumber(record: JsonRecord | null, key: string): number | null {
23
+ const value = record?.[key];
24
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
25
+ }
26
+
27
+ function readBoolean(record: JsonRecord | null, key: string): boolean | null {
28
+ const value = record?.[key];
29
+ return typeof value === 'boolean' ? value : null;
30
+ }
31
+
32
+ function readArray(record: JsonRecord | null, key: string): readonly unknown[] {
33
+ const value = record?.[key];
34
+ return Array.isArray(value) ? value : [];
35
+ }
36
+
37
+ function cleanInline(value: unknown): string {
38
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
39
+ }
40
+
41
+ function yesNo(value: boolean): string {
42
+ return value ? 'yes' : 'no';
43
+ }
44
+
45
+ function sourceLine(value: unknown): string {
46
+ const record = isRecord(value) ? value : {};
47
+ const title = cleanInline(record.title)
48
+ || cleanInline(record.canonicalUri)
49
+ || cleanInline(record.sourceUri)
50
+ || cleanInline(record.url)
51
+ || cleanInline(record.id)
52
+ || 'untitled';
53
+ const type = cleanInline(record.sourceType) || cleanInline(record.type) || 'source';
54
+ const url = cleanInline(record.canonicalUri) || cleanInline(record.sourceUri) || cleanInline(record.url);
55
+ return url && url !== title ? `[${type}] ${title} (${url})` : `[${type}] ${title}`;
56
+ }
57
+
58
+ function resultLine(value: unknown): string {
59
+ const record = isRecord(value) ? value : {};
60
+ const title = cleanInline(record.title) || cleanInline(record.id) || 'untitled';
61
+ const id = cleanInline(record.id);
62
+ const type = cleanInline(record.type) || cleanInline(record.kind) || cleanInline(record.sourceType) || 'result';
63
+ const score = readNumber(record, 'score');
64
+ const url = cleanInline(record.url) || cleanInline(record.canonicalUri) || cleanInline(record.sourceUri);
65
+ const snippet = cleanInline(record.snippet) || cleanInline(record.summary) || cleanInline(record.text);
66
+ const parts = [
67
+ `[${type}] ${title}`,
68
+ id && id !== title ? `id=${id}` : '',
69
+ score !== null ? `score=${score.toFixed(3)}` : '',
70
+ url ? `url=${url}` : '',
71
+ ].filter((part) => part.length > 0);
72
+ return snippet ? `${parts.join(' ')}\n ${snippet}` : parts.join(' ');
73
+ }
74
+
75
+ function nodeLine(value: unknown): string {
76
+ const record = isRecord(value) ? value : {};
77
+ const id = cleanInline(record.id);
78
+ const kind = cleanInline(record.kind) || 'node';
79
+ const title = cleanInline(record.title) || id || 'untitled';
80
+ const confidence = readNumber(record, 'confidence');
81
+ const status = cleanInline(record.status);
82
+ const summary = cleanInline(record.summary);
83
+ const parts = [
84
+ `[${kind}] ${title}`,
85
+ id && id !== title ? `id=${id}` : '',
86
+ status ? `status=${status}` : '',
87
+ confidence !== null ? `confidence=${confidence}` : '',
88
+ ].filter((part) => part.length > 0);
89
+ return summary ? `${parts.join(' ')}\n ${summary}` : parts.join(' ');
90
+ }
91
+
92
+ function issueLine(value: unknown): string {
93
+ const record = isRecord(value) ? value : {};
94
+ const id = cleanInline(record.id) || 'issue';
95
+ const severity = cleanInline(record.severity) || 'unknown';
96
+ const code = cleanInline(record.code) || 'issue';
97
+ const status = cleanInline(record.status);
98
+ const message = cleanInline(record.message);
99
+ const suffix = status ? ` status=${status}` : '';
100
+ return ` - ${id} [${severity}] ${code}${suffix}${message ? ` - ${message}` : ''}`;
101
+ }
102
+
103
+ function connectorLine(value: unknown): string {
104
+ const record = isRecord(value) ? value : {};
105
+ const id = cleanInline(record.id) || 'connector';
106
+ const name = cleanInline(record.displayName) || id;
107
+ const sourceType = cleanInline(record.sourceType);
108
+ const description = cleanInline(record.description);
109
+ const suffix = sourceType ? ` sourceType=${sourceType}` : '';
110
+ return description ? ` - ${id} ${name}${suffix}\n ${description}` : ` - ${id} ${name}${suffix}`;
111
+ }
112
+
113
+ export function formatStatus(data: unknown): string {
114
+ const record = isRecord(data) ? data : {};
115
+ const ready = readBoolean(record, 'ready');
116
+ const sourceCount = readNumber(record, 'sourceCount');
117
+ const nodeCount = readNumber(record, 'nodeCount');
118
+ const issueCount = readNumber(record, 'issueCount');
119
+ const edgeCount = readNumber(record, 'edgeCount');
120
+ const storagePath = readString(record, 'storagePath');
121
+ return [
122
+ 'Agent Knowledge status',
123
+ ` ready: ${ready === null ? 'unknown' : yesNo(ready)}`,
124
+ ` sources: ${sourceCount ?? 'unknown'}`,
125
+ ` nodes: ${nodeCount ?? 'unknown'}`,
126
+ ` edges: ${edgeCount ?? 'unknown'}`,
127
+ ` issues: ${issueCount ?? 'unknown'}`,
128
+ storagePath ? ` storage: ${storagePath}` : null,
129
+ ' route: /api/goodvibes-agent/knowledge/status',
130
+ ].filter((line): line is string => Boolean(line)).join('\n');
131
+ }
132
+
133
+ export function formatEntityList(data: unknown, kind: 'sources' | 'nodes' | 'issues', limit: number): string {
134
+ const record = isRecord(data) ? data : {};
135
+ const values = readArray(record, kind);
136
+ if (values.length === 0) {
137
+ return [
138
+ `Agent Knowledge ${kind}`,
139
+ ' no records',
140
+ ` route: /api/goodvibes-agent/knowledge/${kind}`,
141
+ ].join('\n');
142
+ }
143
+ const format = kind === 'sources'
144
+ ? sourceLine
145
+ : kind === 'nodes'
146
+ ? nodeLine
147
+ : issueLine;
148
+ return [
149
+ `Agent Knowledge ${kind} (${values.length}, limit ${limit})`,
150
+ ...values.slice(0, limit).map((value, index) => (
151
+ kind === 'issues' ? format(value) : ` ${index + 1}. ${format(value)}`
152
+ )),
153
+ ].join('\n');
154
+ }
155
+
156
+ export function formatItem(data: unknown, id: string): string {
157
+ const record = isRecord(data) ? data : {};
158
+ const source = record.source;
159
+ const node = record.node;
160
+ const issue = record.issue;
161
+ const relatedEdges = readArray(record, 'relatedEdges').length;
162
+ const linkedSources = readArray(record, 'linkedSources').length;
163
+ const linkedNodes = readArray(record, 'linkedNodes').length;
164
+ if (source) {
165
+ return [
166
+ `Agent Knowledge item: ${id}`,
167
+ ` ${sourceLine(source)}`,
168
+ ` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
169
+ ].join('\n');
170
+ }
171
+ if (node) {
172
+ return [
173
+ `Agent Knowledge item: ${id}`,
174
+ ` ${nodeLine(node)}`,
175
+ ` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
176
+ ].join('\n');
177
+ }
178
+ if (issue) {
179
+ return [
180
+ `Agent Knowledge item: ${id}`,
181
+ issueLine(issue),
182
+ ` relatedEdges=${relatedEdges} linkedSources=${linkedSources} linkedNodes=${linkedNodes}`,
183
+ ].join('\n');
184
+ }
185
+ return [
186
+ `Agent Knowledge item: ${id}`,
187
+ ' not found',
188
+ ' route: /api/goodvibes-agent/knowledge/items/{id}',
189
+ ].join('\n');
190
+ }
191
+
192
+ export function formatMap(data: unknown): string {
193
+ const record = isRecord(data) ? data : {};
194
+ const sources = readArray(record, 'sources');
195
+ const nodes = readArray(record, 'nodes');
196
+ const edges = readArray(record, 'edges');
197
+ const issues = readArray(record, 'issues');
198
+ return [
199
+ 'Agent Knowledge map',
200
+ ` sources: ${sources.length}`,
201
+ ` nodes: ${nodes.length}`,
202
+ ` edges: ${edges.length}`,
203
+ ` issues: ${issues.length}`,
204
+ ' route: /api/goodvibes-agent/knowledge/map',
205
+ ].join('\n');
206
+ }
207
+
208
+ export function formatConnectors(data: unknown): string {
209
+ const record = isRecord(data) ? data : {};
210
+ const connectors = readArray(record, 'connectors');
211
+ if (connectors.length === 0) {
212
+ return [
213
+ 'Agent Knowledge connectors',
214
+ ' no connectors',
215
+ ' route: /api/goodvibes-agent/knowledge/connectors',
216
+ ].join('\n');
217
+ }
218
+ return [
219
+ `Agent Knowledge connectors (${connectors.length})`,
220
+ ...connectors.map(connectorLine),
221
+ ].join('\n');
222
+ }
223
+
224
+ export function formatAsk(data: unknown, query: string): string {
225
+ const record = isRecord(data) ? data : {};
226
+ const answer = isRecord(record.answer) ? record.answer : record;
227
+ const text = cleanInline(answer.text) || cleanInline(record.answer) || 'No answer returned.';
228
+ const confidence = readNumber(answer, 'confidence') ?? readNumber(record, 'confidence');
229
+ const synthesized = readBoolean(answer, 'synthesized');
230
+ const sources = readArray(answer, 'sources');
231
+ const facts = readArray(answer, 'facts');
232
+ const gaps = readArray(answer, 'gaps');
233
+ const lines = [
234
+ `Agent Knowledge answer: ${query}`,
235
+ text,
236
+ '',
237
+ `confidence: ${confidence ?? 'unknown'}${synthesized === null ? '' : ` synthesized: ${yesNo(synthesized)}`}`,
238
+ ];
239
+ if (sources.length > 0) {
240
+ lines.push('', 'Sources:', ...sources.slice(0, 8).map((source) => ` - ${sourceLine(source)}`));
241
+ }
242
+ if (facts.length > 0) lines.push('', `Facts: ${facts.length}`);
243
+ if (gaps.length > 0) lines.push('', `Gaps: ${gaps.length}`);
244
+ return lines.join('\n');
245
+ }
246
+
247
+ export function formatSearch(data: unknown, query: string): string {
248
+ const record = isRecord(data) ? data : {};
249
+ const items = readArray(record, 'items');
250
+ const results = items.length > 0 ? items : readArray(record, 'results');
251
+ if (results.length === 0) {
252
+ return [
253
+ `Agent Knowledge search: ${query}`,
254
+ ' no results',
255
+ ' route: /api/goodvibes-agent/knowledge/search',
256
+ ].join('\n');
257
+ }
258
+ return [
259
+ `Agent Knowledge search: ${query}`,
260
+ ...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
261
+ ].join('\n');
262
+ }
263
+
264
+ export function formatIngest(data: unknown, url: string): string {
265
+ const record = isRecord(data) ? data : {};
266
+ const source = isRecord(record.source) ? record.source : record;
267
+ const sourceId = cleanInline(source.id);
268
+ const canonicalUri = cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || url;
269
+ const artifactId = cleanInline(record.artifactId);
270
+ return [
271
+ 'Agent Knowledge ingest-url accepted',
272
+ ` source: ${sourceId || '(pending)'}`,
273
+ ` url: ${canonicalUri}`,
274
+ artifactId ? ` artifact: ${artifactId}` : null,
275
+ ' route: /api/goodvibes-agent/knowledge/ingest/url',
276
+ ].filter((line): line is string => Boolean(line)).join('\n');
277
+ }
278
+
279
+ export function formatBatchIngest(data: unknown, label: string): string {
280
+ const record = isRecord(data) ? data : {};
281
+ const imported = readNumber(record, 'imported');
282
+ const failed = readNumber(record, 'failed');
283
+ const sources = readArray(record, 'sources');
284
+ const errors = readArray(record, 'errors');
285
+ return [
286
+ `Agent Knowledge ${label} accepted`,
287
+ ` imported: ${imported ?? sources.length}`,
288
+ ` failed: ${failed ?? errors.length}`,
289
+ ` sources: ${sources.length}`,
290
+ ...sources.slice(0, 5).map((source) => ` - ${sourceLine(source)}`),
291
+ ...(errors.length > 0 ? [' errors:', ...errors.slice(0, 5).map((error) => ` - ${cleanInline(error)}`)] : []),
292
+ ].join('\n');
293
+ }
294
+
295
+ export function formatReindex(data: unknown): string {
296
+ const record = isRecord(data) ? data : {};
297
+ const status = isRecord(record.status) ? record.status : {};
298
+ return [
299
+ 'Agent Knowledge reindex complete',
300
+ ` sources: ${readNumber(status, 'sourceCount') ?? 'unknown'}`,
301
+ ` nodes: ${readNumber(status, 'nodeCount') ?? 'unknown'}`,
302
+ ` edges: ${readNumber(status, 'edgeCount') ?? 'unknown'}`,
303
+ ` issues: ${readNumber(status, 'issueCount') ?? 'unknown'}`,
304
+ ' route: /api/goodvibes-agent/knowledge/reindex',
305
+ ].join('\n');
306
+ }
307
+
308
+ export function formatFailure(failure: AgentKnowledgeFailureLike, json: boolean): string {
309
+ if (json) return JSON.stringify(failure, null, 2);
310
+ return [
311
+ `Agent Knowledge error: ${failure.kind}`,
312
+ ` ${failure.error}`,
313
+ ` runtime: ${failure.baseUrl}`,
314
+ ` route: ${failure.route}`,
315
+ failure.kind === 'version_mismatch' && failure.daemonVersion && failure.expectedSdkVersion
316
+ ? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
317
+ : null,
318
+ failure.kind === 'version_mismatch'
319
+ ? ' next: update/restart the external GoodVibes runtime so /status matches the Agent SDK pin.'
320
+ : null,
321
+ failure.kind === 'daemon_route_unavailable'
322
+ ? ' next: update/restart the external GoodVibes runtime to the SDK version required by this Agent package.'
323
+ : null,
324
+ ].filter((line): line is string => Boolean(line)).join('\n');
325
+ }
package/src/cli/help.ts CHANGED
@@ -184,14 +184,23 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
184
184
  'knowledge status',
185
185
  'knowledge ask <question> [--limit <n>] [--mode concise|standard|detailed]',
186
186
  'knowledge search <query> [--limit <n>]',
187
+ 'knowledge list [--kind sources|nodes|issues] [--limit <n>]',
188
+ 'knowledge get <id>',
189
+ 'knowledge connectors',
187
190
  'knowledge ingest-url <url> [--title <title>] [--tags a,b] --yes',
191
+ 'knowledge import-urls <path> --yes',
192
+ 'knowledge import-bookmarks <path> --yes',
193
+ 'knowledge reindex --yes',
188
194
  ],
189
195
  summary: 'Call isolated Agent Knowledge/Wiki routes under /api/goodvibes-agent/knowledge. No default wiki or non-Agent fallback.',
190
196
  examples: [
191
197
  'knowledge status',
192
198
  'knowledge ask "What is GoodVibes Agent?"',
193
199
  'knowledge search "release checklist"',
200
+ 'knowledge list --kind sources',
201
+ 'knowledge connectors',
194
202
  'knowledge ingest-url https://example.com/page --title "Reference" --yes',
203
+ 'knowledge import-urls ~/agent-links.txt --yes',
195
204
  ],
196
205
  },
197
206
  ask: {
@@ -1,6 +1,8 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { CommandRegistry } from '../input/command-registry.ts';
5
+ import { registerBuiltinCommands } from '../input/commands.ts';
4
6
 
5
7
  export interface PackageCliBinVerification {
6
8
  readonly command: 'goodvibes-agent';
@@ -108,11 +110,40 @@ const PACKAGE_FACING_REQUIRED_TEXT: readonly {
108
110
  { path: 'docs/runtime-connection.md', required: ['/api/goodvibes-agent/knowledge'] },
109
111
  { path: 'docs/release-and-publishing.md', required: ['/api/goodvibes-agent/knowledge'] },
110
112
  ];
113
+ const NON_COMMAND_ROUTE_ROOTS = new Set(['api', 'status']);
111
114
 
112
115
  function readPackageJson(root: string): Record<string, unknown> {
113
116
  return JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')) as Record<string, unknown>;
114
117
  }
115
118
 
119
+ function buildRegisteredSlashCommandNames(): ReadonlySet<string> {
120
+ const registry = new CommandRegistry();
121
+ registerBuiltinCommands(registry);
122
+ const names = new Set<string>();
123
+ for (const command of registry.list()) {
124
+ names.add(command.name);
125
+ for (const alias of command.aliases ?? []) names.add(alias);
126
+ }
127
+ return names;
128
+ }
129
+
130
+ function verifyPackageFacingSlashCommands(path: string, content: string, registeredCommands: ReadonlySet<string>): readonly string[] {
131
+ const failures: string[] = [];
132
+ const commandPattern = /(^|[\s`([])\/([a-z][a-z0-9_-]*)(?=$|[\s`.,;:)\]])/g;
133
+ const lines = content.split(/\r?\n/);
134
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
135
+ const line = lines[lineIndex] ?? '';
136
+ commandPattern.lastIndex = 0;
137
+ for (let match = commandPattern.exec(line); match !== null; match = commandPattern.exec(line)) {
138
+ const root = match[2] ?? '';
139
+ if (NON_COMMAND_ROUTE_ROOTS.has(root)) continue;
140
+ if (registeredCommands.has(root)) continue;
141
+ failures.push(`package-facing text ${path}:${lineIndex + 1} references unknown Agent slash command: /${root}`);
142
+ }
143
+ }
144
+ return failures;
145
+ }
146
+
116
147
  function hasExecutableBit(path: string): boolean {
117
148
  return existsSync(path) && (statSync(path).mode & 0o111) !== 0;
118
149
  }
@@ -146,6 +177,7 @@ function registryPackDryRun(root: string): { readonly files: readonly string[];
146
177
 
147
178
  export function verifyPackageFacingText(root: string): { readonly checkedPaths: readonly string[]; readonly failures: readonly string[] } {
148
179
  const failures: string[] = [];
180
+ const registeredCommands = buildRegisteredSlashCommandNames();
149
181
  for (const path of PACKAGE_FACING_TEXT_PATHS) {
150
182
  const absolutePath = join(root, path);
151
183
  if (!existsSync(absolutePath)) {
@@ -153,6 +185,9 @@ export function verifyPackageFacingText(root: string): { readonly checkedPaths:
153
185
  continue;
154
186
  }
155
187
  const content = readFileSync(absolutePath, 'utf-8');
188
+ if (path !== 'CHANGELOG.md') {
189
+ failures.push(...verifyPackageFacingSlashCommands(path, content, registeredCommands));
190
+ }
156
191
  for (const forbidden of PACKAGE_FACING_FORBIDDEN_TEXT) {
157
192
  if (content.includes(forbidden)) {
158
193
  failures.push(`package-facing text ${path} contains forbidden default/TUI route or policy: ${forbidden}`);
@@ -40,7 +40,7 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
40
40
  label: 'GoodVibes runtime',
41
41
  status: 'ready',
42
42
  detail: `Agent will connect to ${input.daemonBaseUrl}; runtime ownership stays outside this product.`,
43
- command: '/status',
43
+ command: '/health',
44
44
  },
45
45
  {
46
46
  id: 'provider-model',
@@ -238,6 +238,10 @@ function runBundleCommand(args: readonly string[], ctx: CommandContext, skillReg
238
238
 
239
239
  export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx: CommandContext): Promise<void> {
240
240
  const sub = (args[0] ?? 'list').toLowerCase();
241
+ if (sub === 'local' || sub === 'agent') {
242
+ await runAgentSkillsRuntimeCommand(args.slice(1), ctx);
243
+ return;
244
+ }
241
245
  const skillRegistry = registryFromContext(ctx);
242
246
  try {
243
247
  if (sub === 'bundle' || sub === 'bundles') {
@@ -356,7 +360,7 @@ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx:
356
360
  export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): void {
357
361
  registry.register({
358
362
  name: 'agent-skills',
359
- aliases: ['askills', 'local-skills'],
363
+ aliases: ['askills', 'local-skills', 'skills', 'skill'],
360
364
  description: 'Manage local GoodVibes Agent skills',
361
365
  usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes|bundle ...]',
362
366
  handler: runAgentSkillsRuntimeCommand,
@@ -103,9 +103,9 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
103
103
  ` recent failures: ${settings.recentFailureCount}`,
104
104
  ` staged bundle: ${settings.hasStagedManagedBundle ? 'present' : 'none'}`,
105
105
  ...(issues.length > 0 ? issues.map((issue) => ` issue: ${issue}`) : [' no active settings-control issues detected']),
106
- ' next: /settingssync panel',
107
- ' next: /settingssync show <key>',
108
- ' next: /managed staged',
106
+ ' next: /settings',
107
+ ' next: /config <key>',
108
+ ' next: /health repair settings',
109
109
  ].join('\n'));
110
110
  return;
111
111
  }
@@ -131,8 +131,8 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
131
131
  ` active connections: ${snapshot.activeConnections}`,
132
132
  ` degraded: ${snapshot.degradedConnections}`,
133
133
  ...(issues.length > 0 ? issues.map((issue) => ` issue: ${issue}`) : [' no active remote recovery issues detected']),
134
- ' next: /remote supervisor',
135
- ' next: /remote recover <runnerId>',
134
+ ' next: /delegate <build/fix/review task> for explicit TUI build work',
135
+ ' next: use the external runtime host for remote worker repair',
136
136
  ].join('\n'));
137
137
  return;
138
138
  }
@@ -217,14 +217,14 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
217
217
  lines.push(' domain: settings');
218
218
  lines.push(...(
219
219
  settings.conflicts.length > 0
220
- ? [' /settingssync panel', ' /settingssync show <key>', ' /managed staged']
220
+ ? [' /settings', ' /config <key>', ' runtime-owned managed setting repair stays external']
221
221
  : [' no active settings repair actions suggested']
222
222
  ));
223
223
  lines.push(' verify: /health settings');
224
224
  } else if (domain === 'auth') {
225
225
  lines.push(' domain: auth');
226
226
  lines.push(' /auth review');
227
- lines.push(' /providers');
227
+ lines.push(' /provider');
228
228
  lines.push(' /subscription providers');
229
229
  lines.push(' runtime auth users/bootstrap cleanup: use the runtime-owning GoodVibes TUI or host tooling');
230
230
  lines.push(' verify: /health auth');
@@ -237,15 +237,13 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
237
237
  lines.push(' verify: /health accounts');
238
238
  } else if (domain === 'services') {
239
239
  lines.push(' domain: services');
240
- lines.push(' /services doctor');
241
- lines.push(' /services auth-review');
242
240
  lines.push(' /health services');
241
+ lines.push(' runtime service repair belongs to the external GoodVibes runtime host');
243
242
  lines.push(' verify: /health services');
244
243
  } else if (domain === 'remote') {
245
244
  lines.push(' domain: remote');
246
- lines.push(' /remote supervisor');
247
- lines.push(' /remote recover <runnerId>');
248
- lines.push(' /remote setup');
245
+ lines.push(' /delegate <build/fix/review task> for explicit TUI build work');
246
+ lines.push(' remote runner setup and recovery belong to the external runtime host');
249
247
  lines.push(' verify: /health remote');
250
248
  } else if (domain === 'mcp') {
251
249
  lines.push(' domain: mcp');
@@ -262,9 +260,8 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
262
260
  } else if (domain === 'maintenance') {
263
261
  lines.push(' domain: maintenance');
264
262
  lines.push(' /health maintenance');
265
- lines.push(' /guidance review');
263
+ lines.push(' /mode');
266
264
  lines.push(' /compact');
267
- lines.push(' /panel tokens');
268
265
  lines.push(' verify: /health maintenance');
269
266
  } else {
270
267
  lines.push(' domains: settings, auth, accounts, services, remote, mcp, continuity, maintenance');
@@ -329,7 +326,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
329
326
  ' /health remote',
330
327
  ' /health maintenance',
331
328
  ' /health repair <domain>',
332
- ' /setup onboarding',
329
+ ' /onboarding',
333
330
  ].join('\n'));
334
331
  },
335
332
  });
@@ -202,7 +202,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
202
202
  ...needingAttention.map((server) => (
203
203
  ` ${server.name} connected=${server.connected ? 'yes' : 'no'} freshness=${server.schemaFreshness} trust=${server.trustMode}`
204
204
  )),
205
- ' next: /services auth-review',
205
+ ' next: /auth review',
206
206
  ' next: /mcp repair <server>',
207
207
  ].join('\n')
208
208
  : 'MCP Auth Review\n No MCP servers currently need auth or quarantine recovery.');
@@ -223,7 +223,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
223
223
  selected.schemaFreshness === 'quarantined'
224
224
  ? `/mcp quarantine ${selected.name} approve operator --yes`
225
225
  : null,
226
- !selected.connected ? '/services auth-review' : null,
226
+ !selected.connected ? '/auth review' : null,
227
227
  '/mcp review',
228
228
  '/health review',
229
229
  ].filter((entry): entry is string => entry !== null);
@@ -264,7 +264,7 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
264
264
  ctx.print(` Reopened panels: ${reopenedPanels.join(', ')}`);
265
265
  }
266
266
  if ((meta.returnContext.remoteRunners?.length ?? 0) > 0) {
267
- ctx.print(` Remote re-entry: /remote recover ${meta.returnContext.remoteRunners![0]}`);
267
+ ctx.print(' Remote re-entry: use the external runtime host for remote runner recovery; delegate explicit build/fix/review recovery from Agent.');
268
268
  }
269
269
  if ((meta.returnContext.worktreePaths?.length ?? 0) > 0) {
270
270
  ctx.print(' Worktree re-entry: open GoodVibes TUI in the target workspace; delegate explicit build/fix/review recovery from Agent.');
@@ -20,7 +20,7 @@ const APPROVAL_ROWS = [
20
20
  ['network', 'why prompted: external hosts, fetch scope, egress policy', 'review via /approval review network'],
21
21
  ['delegate', 'why prompted: recursive agents, spawn ceilings, write-set inheritance', 'review via /approval review delegate'],
22
22
  ['mcp', 'why prompted: trust escalation, host scope, path scope, coherence mismatch', 'review via /mcp trust and /security'],
23
- ['remote', 'why prompted: runner trust, remote write scope, artifact requirements', 'review via /remote and delegated TUI execution context'],
23
+ ['remote', 'why prompted: runner trust, remote write scope, artifact requirements', 'review delegated TUI execution context'],
24
24
  ['hook', 'why prompted: deny/mutate authority, blocking behavior, runner provenance', 'review via /hooks and /security'],
25
25
  ['plugin', 'why prompted: install/update lifecycle, provenance, capability grants', 'review via /marketplace and /security'],
26
26
  ] as const;
@@ -88,7 +88,7 @@ export class AutomationControlPanel extends ScrollableListPanel<AutomationRun> {
88
88
  protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
89
89
  return [
90
90
  { command: '/schedule list', summary: 'inspect jobs and run history without mutating schedules' },
91
- { command: '/automation jobs', summary: 'review runtime-owned automation jobs from Agent' },
91
+ { command: '/schedule receipts', summary: 'review Agent routine promotion receipts' },
92
92
  ];
93
93
  }
94
94
 
@@ -217,7 +217,7 @@ export async function buildProviderAccountSnapshot(
217
217
  }
218
218
  if (hasServiceOAuth && !serviceOauth?.usable) {
219
219
  issues.push('Service OAuth is configured but missing a usable credential.');
220
- recommendedActions.push(`Repair service OAuth credentials for ${providerId} in /services or /settings.`);
220
+ recommendedActions.push(`Repair service OAuth credentials for ${providerId} in /settings or the external runtime host.`);
221
221
  }
222
222
 
223
223
  return {
@@ -55,8 +55,8 @@ export function buildProviderHealthDomainSummaries(
55
55
  auth.bootstrapCredentialPresent ? 'Runtime bootstrap cleanup must be done from the runtime-owning TUI or host tooling.' : '',
56
56
  ].filter(Boolean),
57
57
  nextSteps: auth.bootstrapCredentialPresent
58
- ? ['/auth review', '/providers', '/subscription providers']
59
- : ['/auth review', '/providers'],
58
+ ? ['/auth review', '/provider', '/subscription providers']
59
+ : ['/auth review', '/provider'],
60
60
  });
61
61
 
62
62
  const settingIssueCount = settings.conflictCount + settings.recentFailureCount + (settings.hasStagedManagedBundle ? 1 : 0);
@@ -68,7 +68,7 @@ export function buildProviderHealthDomainSummaries(
68
68
  : settingIssueCount > 0
69
69
  ? `${settings.conflictCount} conflicts / ${settings.recentFailureCount} failures${settings.hasStagedManagedBundle ? ' / staged bundle' : ''}`
70
70
  : 'settings runtime API clean',
71
- next: settingIssueCount > 0 ? '/settingssync panel' : '/settingssync show <key>',
71
+ next: settingIssueCount > 0 ? '/settings' : '/config <key>',
72
72
  details: [
73
73
  settings.conflictCount > 0 ? `${settings.conflictCount} unresolved import conflict(s)` : '',
74
74
  settings.recentFailureCount > 0 ? `${settings.recentFailureCount} recent sync or managed failure(s)` : '',
@@ -76,8 +76,8 @@ export function buildProviderHealthDomainSummaries(
76
76
  settings.managedLockCount > 0 ? `${settings.managedLockCount} managed lock(s) enforced` : '',
77
77
  ].filter(Boolean),
78
78
  nextSteps: settingIssueCount > 0
79
- ? ['/settingssync panel', '/settingssync show <key>', '/managed staged']
80
- : ['/settingssync show <key>'],
79
+ ? ['/settings', '/config <key>', '/health settings']
80
+ : ['/config <key>'],
81
81
  });
82
82
 
83
83
  summaries.push({
@@ -86,7 +86,7 @@ export function buildProviderHealthDomainSummaries(
86
86
  summary: remote.supervisor.sessions.length === 0
87
87
  ? 'no remote sessions tracked'
88
88
  : `${remote.supervisor.sessions.length} sessions / ${remote.supervisor.degradedConnections} degraded`,
89
- next: remote.supervisor.degradedConnections > 0 ? '/remote recover <workerId>' : '/remote supervisor',
89
+ next: remote.supervisor.degradedConnections > 0 ? '/delegate <build/fix/review task>' : '/health remote',
90
90
  details: remote.supervisor.sessions.length === 0
91
91
  ? ['no remote sessions have been attached yet']
92
92
  : remote.supervisor.sessions
@@ -99,8 +99,8 @@ export function buildProviderHealthDomainSummaries(
99
99
  .slice(0, 3)
100
100
  .map((entry) => `${entry.runnerId}: transport=${entry.transportState} heartbeat=${entry.heartbeat.status}${entry.lastError ? ` error=${entry.lastError}` : ''}`),
101
101
  nextSteps: remote.supervisor.degradedConnections > 0
102
- ? ['/remote supervisor', '/remote recover <workerId>', '/remote support']
103
- : ['/remote supervisor'],
102
+ ? ['/delegate <build/fix/review task>', '/health remote']
103
+ : ['/health remote'],
104
104
  });
105
105
 
106
106
  const degradedServers = security.mcpServers.filter((server) =>
@@ -141,7 +141,7 @@ export function buildProviderHealthDomainSummaries(
141
141
  ? 'warn'
142
142
  : 'good',
143
143
  summary: maintenance.summary,
144
- next: maintenance.nextSteps[0] ?? '/guidance review',
144
+ next: maintenance.nextSteps[0] ?? '/health maintenance',
145
145
  details: maintenance.reasons.slice(0, 3),
146
146
  nextSteps: maintenance.nextSteps,
147
147
  });
@@ -266,7 +266,7 @@ export class SessionBrowserPanel extends BasePanel {
266
266
  ...formatReturnContextLines(selected.returnContext).map((line) =>
267
267
  buildPanelLine(width, [[' ', DEFAULT_PANEL_PALETTE.dim], [truncateDisplay(line, Math.max(0, width - 2)), DEFAULT_PANEL_PALETTE.dim]])
268
268
  ),
269
- buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], [selected.returnContext?.remoteRunners?.length ? `/remote recover ${selected.returnContext.remoteRunners[0]}` : '/session resume', DEFAULT_PANEL_PALETTE.dim]]),
269
+ buildPanelLine(width, [[' Next ', DEFAULT_PANEL_PALETTE.label], ['/session resume', DEFAULT_PANEL_PALETTE.dim]]),
270
270
  ],
271
271
  }
272
272
  : { title: 'Selected', lines: [] };
@@ -76,13 +76,13 @@ export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput):
76
76
  level = 'needs-repair';
77
77
  summary = `Compact now to recover context headroom (${usagePct}% used).`;
78
78
  reasons.push(`Context pressure is high at ${usagePct}% usage.`);
79
- nextSteps.push('/compact', '/panel tokens');
79
+ nextSteps.push('/compact', '/context');
80
80
  compactRecommended = true;
81
81
  } else if (usagePct >= thresholdPct || remainingTokens <= 15_000) {
82
82
  level = 'suggest-compact';
83
83
  summary = `Watch context growth (${usagePct}% used).`;
84
84
  reasons.push(`Context pressure is climbing at ${usagePct}% usage.`);
85
- nextSteps.push('/panel tokens');
85
+ nextSteps.push('/context');
86
86
  compactRecommended = true;
87
87
  } else if (usagePct >= 70 || staleByMessageGrowth) {
88
88
  level = 'watch';
@@ -92,7 +92,7 @@ export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput):
92
92
  reasons.push(staleByMessageGrowth
93
93
  ? `Conversation has grown ${messageCount.toLocaleString()} messages since the last maintenance checkpoint.`
94
94
  : `Context usage is climbing toward the ${thresholdPct}% maintenance threshold.`);
95
- nextSteps.push('/panel tokens');
95
+ nextSteps.push('/context');
96
96
  } else {
97
97
  reasons.push('Context pressure is currently within the stable operating band.');
98
98
  }
@@ -80,7 +80,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
80
80
  return [
81
81
  { command: '/subscription login openai start --yes', summary: 'start the first-class OpenAI subscription flow' },
82
82
  { command: '/login provider <name> start --yes', summary: 'use the front-door auth flow for supported providers' },
83
- { command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
83
+ { command: '/auth review', summary: 'inspect runtime auth posture without exposing token values' },
84
84
  ];
85
85
  }
86
86
 
@@ -69,21 +69,23 @@ export function renderHelpOverlay(
69
69
  // Each entry is [commandName, subcommandOrArgHint, description].
70
70
  // Commands not registered in the live registry are omitted at render time.
71
71
  const FEATURED_COMMANDS: Array<[name: string, argHint: string, desc: string]> = [
72
- ['onboarding', '', 'Open the onboarding wizard with current settings preloaded'],
73
- ['cockpit', '', 'Unified runtime control room'],
74
- ['settings', '', 'Settings and config browser'],
72
+ ['agent', '', 'Open the Agent operator workspace'],
73
+ ['onboarding', '', 'Open Agent setup with current settings preloaded'],
74
+ ['knowledge', 'status', 'Inspect isolated Agent Knowledge readiness'],
75
+ ['memory', '', 'Manage local Agent memory records'],
76
+ ['personas', '', 'Manage serial Agent operating personas'],
77
+ ['agent-skills', '', 'Manage local Agent skills and bundles'],
78
+ ['routines', '', 'Manage reusable main-conversation routines'],
79
+ ['workplan', '', 'Inspect shared work-plan state'],
80
+ ['approval', '', 'Review and explicitly act on approvals'],
81
+ ['schedule', '', 'Inspect schedules and routine promotion receipts'],
82
+ ['delegate', '', 'Explicitly hand build/fix/review work to GoodVibes TUI'],
83
+ ['mcp', '', 'Inspect MCP servers and tool readiness'],
75
84
  ['provider', '', 'Choose provider or model family'],
85
+ ['model', '', 'Select the active model route'],
76
86
  ['subscription', '', 'Review provider logins and subscriptions'],
77
- ['marketplace', 'open', 'Browse plugins, skills, and packs'],
78
- ['remote', 'setup', 'Review remote, bridge, and tunnel flows'],
79
- ['security', '', 'Security review workspace'],
80
- ['policy', '', 'Simulation, lint, and preflight review'],
81
- ['incident', '', 'Incident workspace and export flows'],
82
- ['knowledge', '', 'Durable knowledge and review queue'],
83
- ['hooks', '', 'Hook workbench and runtime activity'],
84
- ['orchestration','', 'Graph and recursive-agent control room'],
85
- ['communication','', 'Structured agent communication workspace'],
86
- ['tasks', '', 'Read-only task view for list/show/pause/resume/output'],
87
+ ['secrets', '', 'Manage secret references without printing values'],
88
+ ['health', '', 'Run Agent runtime and setup diagnostics'],
87
89
  ];
88
90
 
89
91
  // Build command rows from featured list, filtering out unregistered commands.
@@ -115,7 +117,29 @@ export function renderHelpOverlay(
115
117
 
116
118
  if (commands && commands.length > 0) {
117
119
  commandRows.push('', ' Available Slash Commands', ' ' + '\u2500'.repeat(40));
118
- const preferred = ['setup', 'cockpit', 'settings', 'provider', 'subscription', 'marketplace', 'remote', 'security', 'policy', 'incident', 'knowledge', 'hooks', 'orchestration', 'communication', 'tasks'];
120
+ const preferred = [
121
+ 'agent',
122
+ 'onboarding',
123
+ 'knowledge',
124
+ 'memory',
125
+ 'personas',
126
+ 'agent-skills',
127
+ 'routines',
128
+ 'workplan',
129
+ 'approval',
130
+ 'schedule',
131
+ 'delegate',
132
+ 'mcp',
133
+ 'provider',
134
+ 'model',
135
+ 'subscription',
136
+ 'secrets',
137
+ 'health',
138
+ 'settings',
139
+ 'security',
140
+ 'policy',
141
+ 'tasks',
142
+ ];
119
143
  const seen = new Set<string>();
120
144
  for (const name of preferred) {
121
145
  const cmd = commands.find((entry) => entry.name === name);
@@ -148,7 +148,7 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
148
148
  ];
149
149
 
150
150
  if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
151
- if (entry.conflict) lines.push(`Conflict: resolve with /settingssync resolve ${entry.setting.key} local|synced.`);
151
+ if (entry.conflict) lines.push(`Conflict: inspect with /settings and resolve runtime-owned sync state in the external host.`);
152
152
 
153
153
  lines.push('', entry.setting.description);
154
154
 
@@ -74,7 +74,7 @@ export function createResumeSessionHandler(options: ResumeSessionOptions): (sess
74
74
  options.conversation.log(`Resume: Reopened panels: ${reopenedPanels.join(', ')}`, { fg: '244' });
75
75
  }
76
76
  if ((meta.returnContext.remoteRunners?.length ?? 0) > 0) {
77
- options.conversation.log(`Resume: Remote re-entry -> /remote recover ${meta.returnContext.remoteRunners![0]}`, { fg: '244' });
77
+ options.conversation.log('Resume: Remote re-entry belongs to the external runtime host; delegate explicit build/fix/review recovery from Agent.', { fg: '244' });
78
78
  }
79
79
  if (returnContextMode === 'assisted') {
80
80
  const helperModel = new HelperModel({
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.1.89';
9
+ let _version = '0.1.91';
10
10
  let _sdkVersion = '0.33.35';
11
11
  try {
12
12
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {