@pellux/goodvibes-agent 0.1.47 → 0.1.48

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.
@@ -1,1534 +0,0 @@
1
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
2
- import { SDK_VERSION } from '../version.ts';
3
- import type { AgentDaemonConnection } from '../agent/routine-schedule-promotion.ts';
4
-
5
- export const DAEMON_METHOD_CATALOG_ROUTE = '/api/control-plane/methods';
6
- export const AGENT_KNOWLEDGE_STATUS_ROUTE = '/api/goodvibes-agent/knowledge/status';
7
- export const DAEMON_STATUS_ROUTE = '/status';
8
-
9
- export type DaemonCapabilityAuditFailureKind =
10
- | 'auth_required'
11
- | 'daemon_unavailable'
12
- | 'version_mismatch'
13
- | 'daemon_route_unavailable'
14
- | 'daemon_error';
15
-
16
- export type DaemonCapabilityCoverage = 'ready' | 'partial' | 'missing';
17
- export type DaemonCapabilityRouteCoverage = 'ready' | 'missing' | 'not_checked';
18
- export type DaemonCapabilityGapKind =
19
- | 'version_mismatch'
20
- | 'agent_route_missing'
21
- | 'required_method_missing'
22
- | 'route_risk_review'
23
- | 'agent_ux_gap';
24
- export type DaemonCapabilityGapSeverity = 'blocker' | 'high' | 'medium' | 'low';
25
-
26
- export interface DaemonCapabilityRequirement {
27
- readonly id: string;
28
- readonly title: string;
29
- readonly competitorBaseline: string;
30
- readonly agentUse: string;
31
- readonly requiredMethodIds: readonly string[];
32
- readonly optionalMethodIds: readonly string[];
33
- readonly requiredAgentRoutes: readonly string[];
34
- readonly next: readonly string[];
35
- }
36
-
37
- export interface DaemonCapabilityAuditArea {
38
- readonly id: string;
39
- readonly title: string;
40
- readonly coverage: DaemonCapabilityCoverage;
41
- readonly competitorBaseline: string;
42
- readonly agentUse: string;
43
- readonly presentRequiredMethodIds: readonly string[];
44
- readonly missingRequiredMethodIds: readonly string[];
45
- readonly presentOptionalMethodIds: readonly string[];
46
- readonly missingOptionalMethodIds: readonly string[];
47
- readonly agentRoutes: readonly {
48
- readonly route: string;
49
- readonly coverage: DaemonCapabilityRouteCoverage;
50
- }[];
51
- readonly routeRisk: {
52
- readonly readOnlyMethodIds: readonly string[];
53
- readonly mutatingMethodIds: readonly string[];
54
- readonly authenticatedMethodIds: readonly string[];
55
- readonly readOnlyMethodCount: number;
56
- readonly mutatingMethodCount: number;
57
- readonly authenticatedMethodCount: number;
58
- readonly dangerousMethodIds: readonly string[];
59
- };
60
- readonly next: readonly string[];
61
- }
62
-
63
- export interface DaemonCapabilityAuditSuccess {
64
- readonly ok: true;
65
- readonly kind: 'daemon.capabilities.audit';
66
- readonly baseUrl: string;
67
- readonly daemonVersion: string;
68
- readonly expectedSdkVersion: string;
69
- readonly daemonCompatible: boolean;
70
- readonly methodCatalogRoute: typeof DAEMON_METHOD_CATALOG_ROUTE;
71
- readonly methodCount: number;
72
- readonly agentKnowledgeRoute: typeof AGENT_KNOWLEDGE_STATUS_ROUTE;
73
- readonly agentKnowledgeRouteReady: boolean;
74
- readonly defaultKnowledgeFallback: false;
75
- readonly homeGraphFallback: false;
76
- readonly warnings: readonly string[];
77
- readonly areas: readonly DaemonCapabilityAuditArea[];
78
- }
79
-
80
- export interface DaemonCapabilityGap {
81
- readonly id: string;
82
- readonly kind: DaemonCapabilityGapKind;
83
- readonly severity: DaemonCapabilityGapSeverity;
84
- readonly areaId?: string;
85
- readonly title: string;
86
- readonly detail: string;
87
- readonly action: string;
88
- }
89
-
90
- export interface DaemonCapabilityGapReport {
91
- readonly ok: true;
92
- readonly kind: 'daemon.capabilities.gaps';
93
- readonly baseUrl: string;
94
- readonly daemonVersion: string;
95
- readonly expectedSdkVersion: string;
96
- readonly daemonCompatible: boolean;
97
- readonly methodCatalogRoute: typeof DAEMON_METHOD_CATALOG_ROUTE;
98
- readonly agentKnowledgeRoute: typeof AGENT_KNOWLEDGE_STATUS_ROUTE;
99
- readonly agentKnowledgeRouteReady: boolean;
100
- readonly defaultKnowledgeFallback: false;
101
- readonly homeGraphFallback: false;
102
- readonly gapCount: number;
103
- readonly gaps: readonly DaemonCapabilityGap[];
104
- }
105
-
106
- export interface DaemonCapabilityRouteRiskArea {
107
- readonly areaId: string;
108
- readonly title: string;
109
- readonly coverage: DaemonCapabilityCoverage;
110
- readonly readOnlyMethodIds: readonly string[];
111
- readonly mutatingMethodIds: readonly string[];
112
- readonly authenticatedMethodIds: readonly string[];
113
- readonly readOnlyMethodCount: number;
114
- readonly mutatingMethodCount: number;
115
- readonly authenticatedMethodCount: number;
116
- readonly dangerousMethodIds: readonly string[];
117
- }
118
-
119
- export interface DaemonCapabilityRouteRiskReport {
120
- readonly ok: true;
121
- readonly kind: 'daemon.capabilities.route_risk';
122
- readonly baseUrl: string;
123
- readonly daemonVersion: string;
124
- readonly expectedSdkVersion: string;
125
- readonly daemonCompatible: boolean;
126
- readonly methodCatalogRoute: typeof DAEMON_METHOD_CATALOG_ROUTE;
127
- readonly agentKnowledgeRoute: typeof AGENT_KNOWLEDGE_STATUS_ROUTE;
128
- readonly agentKnowledgeRouteReady: boolean;
129
- readonly defaultKnowledgeFallback: false;
130
- readonly homeGraphFallback: false;
131
- readonly totalReadOnlyMethodCount: number;
132
- readonly totalMutatingMethodCount: number;
133
- readonly totalAuthenticatedMethodCount: number;
134
- readonly totalDangerousMethodCount: number;
135
- readonly areas: readonly DaemonCapabilityRouteRiskArea[];
136
- }
137
-
138
- export interface DaemonCapabilityInventoryMethod {
139
- readonly id: string;
140
- readonly title?: string;
141
- readonly category: string;
142
- readonly access: string;
143
- readonly invokable: boolean | null;
144
- readonly dangerous: boolean;
145
- readonly httpMethod: string;
146
- readonly path?: string;
147
- readonly readOnly: boolean;
148
- readonly mutating: boolean;
149
- }
150
-
151
- export interface DaemonCapabilityInventoryGroup {
152
- readonly category: string;
153
- readonly methodCount: number;
154
- readonly readOnlyMethodCount: number;
155
- readonly mutatingMethodCount: number;
156
- readonly authenticatedMethodCount: number;
157
- readonly dangerousMethodCount: number;
158
- readonly methods: readonly DaemonCapabilityInventoryMethod[];
159
- }
160
-
161
- export interface DaemonCapabilityInventoryReport {
162
- readonly ok: true;
163
- readonly kind: 'daemon.capabilities.inventory';
164
- readonly baseUrl: string;
165
- readonly daemonVersion: string;
166
- readonly expectedSdkVersion: string;
167
- readonly daemonCompatible: boolean;
168
- readonly methodCatalogRoute: typeof DAEMON_METHOD_CATALOG_ROUTE;
169
- readonly methodCount: number;
170
- readonly agentKnowledgeRoute: typeof AGENT_KNOWLEDGE_STATUS_ROUTE;
171
- readonly agentKnowledgeRouteReady: boolean;
172
- readonly defaultKnowledgeFallback: false;
173
- readonly homeGraphFallback: false;
174
- readonly readOnlyMethodCount: number;
175
- readonly mutatingMethodCount: number;
176
- readonly authenticatedMethodCount: number;
177
- readonly dangerousMethodCount: number;
178
- readonly accessCounts: readonly {
179
- readonly access: string;
180
- readonly count: number;
181
- }[];
182
- readonly groups: readonly DaemonCapabilityInventoryGroup[];
183
- }
184
-
185
- export type DaemonCapabilityUxCoverage =
186
- | 'usable'
187
- | 'read_only'
188
- | 'explicit_confirmation'
189
- | 'blocked'
190
- | 'not_surfaced';
191
-
192
- export interface DaemonCapabilityUxMethod {
193
- readonly id: string;
194
- readonly title?: string;
195
- readonly category: string;
196
- readonly access: string;
197
- readonly httpMethod: string;
198
- readonly path?: string;
199
- readonly dangerous: boolean;
200
- readonly readOnly: boolean;
201
- readonly mutating: boolean;
202
- readonly uxCoverage: DaemonCapabilityUxCoverage;
203
- readonly surface: string;
204
- readonly next: string;
205
- }
206
-
207
- export interface DaemonCapabilityUxGroup {
208
- readonly category: string;
209
- readonly methodCount: number;
210
- readonly usableMethodCount: number;
211
- readonly readOnlyMethodCount: number;
212
- readonly explicitConfirmationMethodCount: number;
213
- readonly blockedMethodCount: number;
214
- readonly notSurfacedMethodCount: number;
215
- readonly methods: readonly DaemonCapabilityUxMethod[];
216
- }
217
-
218
- export interface DaemonCapabilityUxCoverageReport {
219
- readonly ok: true;
220
- readonly kind: 'daemon.capabilities.ux_coverage';
221
- readonly baseUrl: string;
222
- readonly daemonVersion: string;
223
- readonly expectedSdkVersion: string;
224
- readonly daemonCompatible: boolean;
225
- readonly methodCatalogRoute: typeof DAEMON_METHOD_CATALOG_ROUTE;
226
- readonly methodCount: number;
227
- readonly agentKnowledgeRoute: typeof AGENT_KNOWLEDGE_STATUS_ROUTE;
228
- readonly agentKnowledgeRouteReady: boolean;
229
- readonly defaultKnowledgeFallback: false;
230
- readonly homeGraphFallback: false;
231
- readonly usableMethodCount: number;
232
- readonly readOnlyMethodCount: number;
233
- readonly explicitConfirmationMethodCount: number;
234
- readonly blockedMethodCount: number;
235
- readonly notSurfacedMethodCount: number;
236
- readonly groups: readonly DaemonCapabilityUxGroup[];
237
- }
238
-
239
- export interface DaemonCapabilityAuditFailure {
240
- readonly ok: false;
241
- readonly kind: DaemonCapabilityAuditFailureKind;
242
- readonly error: string;
243
- readonly baseUrl: string;
244
- readonly route: string;
245
- readonly daemonVersion?: string;
246
- readonly expectedSdkVersion?: string;
247
- }
248
-
249
- export type DaemonCapabilityAuditResult =
250
- | DaemonCapabilityAuditSuccess
251
- | DaemonCapabilityAuditFailure;
252
-
253
- export interface DaemonMethodSummary {
254
- readonly id: string;
255
- readonly title?: string;
256
- readonly category?: string;
257
- readonly invokable?: boolean;
258
- readonly access?: string;
259
- readonly dangerous?: boolean;
260
- readonly http?: {
261
- readonly method?: string;
262
- readonly path?: string;
263
- };
264
- }
265
-
266
- interface FetchJsonResult {
267
- readonly ok: boolean;
268
- readonly status: number;
269
- readonly statusText: string;
270
- readonly body: unknown;
271
- }
272
-
273
- export const DAEMON_CAPABILITY_REQUIREMENTS: readonly DaemonCapabilityRequirement[] = [
274
- {
275
- id: 'gateway-control',
276
- title: 'Gateway Control Plane',
277
- competitorBaseline: 'OpenClaw/Hermes expose an always-on gateway with health, auth, method discovery, and events.',
278
- agentUse: 'Agent connects to the existing GoodVibes daemon, inspects posture, and never starts or owns daemon lifecycle.',
279
- requiredMethodIds: [
280
- 'control.status',
281
- 'control.auth.current',
282
- 'control.methods.list',
283
- 'control.contract',
284
- 'control.snapshot',
285
- ],
286
- optionalMethodIds: ['control.clients.list', 'control.events.catalog', 'control.events.stream', 'control.web'],
287
- requiredAgentRoutes: [],
288
- next: ['Add richer first-run guidance when auth or route contract checks fail.'],
289
- },
290
- {
291
- id: 'chat-sessions',
292
- title: 'Companion Chat And Shared Sessions',
293
- competitorBaseline: 'Personal agents keep persistent chat and can route larger work through task/session surfaces.',
294
- agentUse: 'Agent uses companion.chat for normal turns and shared sessions only for explicit TUI build/fix/review delegation.',
295
- requiredMethodIds: [
296
- 'companion.chat.sessions.create',
297
- 'companion.chat.sessions.get',
298
- 'companion.chat.sessions.list',
299
- 'companion.chat.messages.create',
300
- 'companion.chat.messages.list',
301
- 'sessions.create',
302
- 'sessions.messages.create',
303
- 'sessions.list',
304
- ],
305
- optionalMethodIds: ['companion.chat.events.stream', 'sessions.followUp', 'sessions.steer', 'sessions.integration.snapshot'],
306
- requiredAgentRoutes: [],
307
- next: ['Expose delegated session artifacts and status in the operator workspace without making WRFC default.'],
308
- },
309
- {
310
- id: 'channels',
311
- title: 'Channels And Delivery Gateway',
312
- competitorBaseline: 'Gateway products receive and send across Slack, Discord, webhooks, mobile, and companion surfaces.',
313
- agentUse: 'Agent inspects channel readiness and uses explicit delivery targets for scheduled routine promotion.',
314
- requiredMethodIds: [
315
- 'channels.status',
316
- 'channels.capabilities.list',
317
- 'channels.accounts.list',
318
- 'channels.setup.get',
319
- 'channels.doctor.get',
320
- 'channels.actions.list',
321
- 'channels.tools.list',
322
- 'channels.targets.resolve',
323
- 'channels.policies.list',
324
- ],
325
- optionalMethodIds: [
326
- 'channels.directory.query',
327
- 'channels.allowlist.resolve',
328
- 'channels.policies.audit',
329
- 'channels.agent_tools.list',
330
- 'channels.authorize',
331
- ],
332
- requiredAgentRoutes: [],
333
- next: ['Surface live per-account delivery errors and setup repairs in the Agent workspace.'],
334
- },
335
- {
336
- id: 'agent-knowledge',
337
- title: 'Isolated Agent Knowledge',
338
- competitorBaseline: 'Persistent knowledge and memory are core personal-operator features.',
339
- agentUse: 'Agent Knowledge is a separate product segment under /api/goodvibes-agent/knowledge/* with no default wiki or HomeGraph fallback.',
340
- requiredMethodIds: [],
341
- optionalMethodIds: [],
342
- requiredAgentRoutes: [AGENT_KNOWLEDGE_STATUS_ROUTE],
343
- next: ['Add artifact and multimodal ingestion UX only against the isolated Agent Knowledge route family.'],
344
- },
345
- {
346
- id: 'automation-schedules',
347
- title: 'Automation, Schedules, Runs, And Capacity',
348
- competitorBaseline: 'Hermes/OpenClaw-style operators can schedule, run, pause, resume, and inspect recurring work.',
349
- agentUse: 'Agent observes automation and promotes local routines to daemon schedules only through explicit confirmed commands.',
350
- requiredMethodIds: [
351
- 'automation.integration.snapshot',
352
- 'automation.jobs.list',
353
- 'automation.runs.list',
354
- 'automation.heartbeat.list',
355
- 'schedules.list',
356
- 'schedules.create',
357
- 'scheduler.capacity',
358
- ],
359
- optionalMethodIds: [
360
- 'automation.jobs.run',
361
- 'automation.jobs.pause',
362
- 'automation.jobs.resume',
363
- 'automation.runs.cancel',
364
- 'automation.runs.retry',
365
- 'schedules.run',
366
- 'schedules.enable',
367
- 'schedules.disable',
368
- 'schedules.delete',
369
- ],
370
- requiredAgentRoutes: [],
371
- next: ['Add live delivery/run history and failed delivery diagnostics for promoted Agent routines.'],
372
- },
373
- {
374
- id: 'approvals-security',
375
- title: 'Approvals, Policy, And Channel Safety',
376
- competitorBaseline: 'Exposed agents need approval gates, pairing, allowlists, and policy inspection.',
377
- agentUse: 'Agent keeps destructive or external effects behind exact commands plus confirmation and uses daemon approvals.',
378
- requiredMethodIds: [
379
- 'approvals.list',
380
- 'approvals.approve',
381
- 'approvals.deny',
382
- 'approvals.cancel',
383
- 'channels.policies.list',
384
- 'channels.policies.audit',
385
- ],
386
- optionalMethodIds: ['approvals.claim', 'channels.allowlist.edit', 'channels.allowlist.resolve'],
387
- requiredAgentRoutes: [],
388
- next: ['Build a route-risk-aware approval center in the fullscreen Agent workspace.'],
389
- },
390
- {
391
- id: 'mcp-tools-artifacts',
392
- title: 'MCP, Tools, Artifacts, And Web Search',
393
- competitorBaseline: 'Modern personal operators expose managed tools, MCP servers, artifacts, and web/research tools.',
394
- agentUse: 'Agent uses GoodVibes daemon tool surfaces through public SDK contracts and policy-gated model visibility.',
395
- requiredMethodIds: [
396
- 'mcp.config.get',
397
- 'mcp.servers.list',
398
- 'mcp.tools.list',
399
- 'artifacts.create',
400
- 'artifacts.get',
401
- 'artifacts.list',
402
- 'web_search.providers.list',
403
- 'web_search.query',
404
- ],
405
- optionalMethodIds: ['artifacts.content.get', 'mcp.config.reload'],
406
- requiredAgentRoutes: [],
407
- next: ['Add per-turn tool-palette narrowing so broad tool capability does not create noisy model schemas.'],
408
- },
409
- {
410
- id: 'voice-media-nodes',
411
- title: 'Voice, Media, Multimodal, And Remote Nodes',
412
- competitorBaseline: 'OpenClaw/Hermes expose voice, media, mobile/node, and multimodal surfaces.',
413
- agentUse: 'Agent inspects daemon voice/media/remote readiness and keeps execution explicit or read-only until user-selected.',
414
- requiredMethodIds: [
415
- 'voice.status',
416
- 'voice.providers.list',
417
- 'voice.voices.list',
418
- 'voice.tts',
419
- 'voice.stt',
420
- 'media.providers.list',
421
- 'media.analyze',
422
- 'multimodal.providers.list',
423
- 'remote.snapshot',
424
- 'remote.peers.list',
425
- 'remote.work.list',
426
- 'remote.node_host.contract',
427
- ],
428
- optionalMethodIds: [
429
- 'voice.realtime.session',
430
- 'voice.tts.stream',
431
- 'media.generate',
432
- 'media.transform',
433
- 'multimodal.analyze',
434
- 'remote.peers.invoke',
435
- ],
436
- requiredAgentRoutes: [],
437
- next: ['Turn daemon readiness into Agent setup cards for voice, media, browser, and node workflows.'],
438
- },
439
- {
440
- id: 'providers-models',
441
- title: 'Providers, Models, And Usage',
442
- competitorBaseline: 'Personal operators need configurable providers, model routing, and usage posture.',
443
- agentUse: 'Agent reads daemon provider/model state, keeps provider+model routing explicit, and avoids per-message routing hacks.',
444
- requiredMethodIds: ['providers.list', 'providers.get', 'providers.usage.get'],
445
- optionalMethodIds: ['accounts.snapshot'],
446
- requiredAgentRoutes: [],
447
- next: ['Add provider/model readiness remediation directly into onboarding/config workspaces.'],
448
- },
449
- ] as const;
450
-
451
- function isRecord(value: unknown): value is Record<string, unknown> {
452
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
453
- }
454
-
455
- function readString(record: Record<string, unknown>, key: string): string | null {
456
- const value = record[key];
457
- return typeof value === 'string' ? value : null;
458
- }
459
-
460
- function readMethodSummaries(body: unknown): readonly DaemonMethodSummary[] {
461
- const methods = isRecord(body) && Array.isArray(body.methods) ? body.methods : [];
462
- return methods.flatMap((value): DaemonMethodSummary[] => {
463
- if (!isRecord(value)) return [];
464
- const id = readString(value, 'id');
465
- if (!id) return [];
466
- const httpRecord = isRecord(value.http) ? value.http : null;
467
- return [{
468
- id,
469
- title: readString(value, 'title') ?? undefined,
470
- category: readString(value, 'category') ?? undefined,
471
- access: readString(value, 'access') ?? undefined,
472
- invokable: typeof value.invokable === 'boolean' ? value.invokable : undefined,
473
- dangerous: typeof value.dangerous === 'boolean' ? value.dangerous : undefined,
474
- http: httpRecord
475
- ? {
476
- method: readString(httpRecord, 'method') ?? undefined,
477
- path: readString(httpRecord, 'path') ?? undefined,
478
- }
479
- : undefined,
480
- }];
481
- });
482
- }
483
-
484
- function normalizeMethodCategory(method: DaemonMethodSummary): string {
485
- const category = method.category?.trim();
486
- if (category) return category;
487
- const [prefix] = method.id.split('.');
488
- return prefix || 'uncategorized';
489
- }
490
-
491
- function normalizeAccess(method: DaemonMethodSummary): string {
492
- const access = method.access?.trim();
493
- return access || 'unknown';
494
- }
495
-
496
- function normalizeHttpMethod(method: DaemonMethodSummary): string {
497
- return method.http?.method?.trim().toUpperCase() || 'UNKNOWN';
498
- }
499
-
500
- function isReadOnlyHttpMethod(httpMethod: string): boolean {
501
- return httpMethod === 'GET' || httpMethod === 'HEAD';
502
- }
503
-
504
- function compareInventoryMethods(left: DaemonCapabilityInventoryMethod, right: DaemonCapabilityInventoryMethod): number {
505
- return left.id.localeCompare(right.id);
506
- }
507
-
508
- function compareInventoryGroups(left: DaemonCapabilityInventoryGroup, right: DaemonCapabilityInventoryGroup): number {
509
- const countDelta = right.methodCount - left.methodCount;
510
- if (countDelta !== 0) return countDelta;
511
- return left.category.localeCompare(right.category);
512
- }
513
-
514
- function compareUxGroups(left: DaemonCapabilityUxGroup, right: DaemonCapabilityUxGroup): number {
515
- const gapDelta = right.notSurfacedMethodCount + right.blockedMethodCount
516
- - (left.notSurfacedMethodCount + left.blockedMethodCount);
517
- if (gapDelta !== 0) return gapDelta;
518
- const countDelta = right.methodCount - left.methodCount;
519
- if (countDelta !== 0) return countDelta;
520
- return left.category.localeCompare(right.category);
521
- }
522
-
523
- const USABLE_METHOD_SURFACES = new Map<string, string>([
524
- ['control.status', 'status / smoke / compat'],
525
- ['control.auth.current', 'auth / status / smoke'],
526
- ['control.methods.list', 'capabilities daemon inventory / coverage / risk'],
527
- ['control.contract', 'compat / capability checks'],
528
- ['control.snapshot', 'status / capability checks'],
529
- ['companion.chat.sessions.create', 'chat session management'],
530
- ['companion.chat.sessions.get', 'chat session management'],
531
- ['companion.chat.sessions.list', 'chat session management'],
532
- ['companion.chat.sessions.update', 'chat session management'],
533
- ['companion.chat.messages.create', 'normal assistant chat'],
534
- ['companion.chat.messages.list', 'normal assistant chat'],
535
- ['sessions.messages.create', 'explicit build delegation to GoodVibes TUI'],
536
- ['sessions.list', 'delegations status'],
537
- ['tasks.list', 'delegations status'],
538
- ['projectPlanning.workPlan.snapshot', 'workplan'],
539
- ['approvals.list', 'approvals / approval risk'],
540
- ['automation.integration.snapshot', 'automation status'],
541
- ['automation.jobs.list', 'automation jobs'],
542
- ['automation.runs.list', 'automation runs'],
543
- ['automation.heartbeat.list', 'automation heartbeat'],
544
- ['schedules.list', 'schedules'],
545
- ['scheduler.capacity', 'automation capacity'],
546
- ['providers.list', 'provider/model setup'],
547
- ['providers.get', 'provider/model setup'],
548
- ['providers.usage.get', 'provider usage'],
549
- ['accounts.snapshot', 'provider account posture'],
550
- ['channels.status', 'channel onboarding/status'],
551
- ['channels.capabilities.list', 'channel onboarding/status'],
552
- ['channels.accounts.list', 'channel onboarding/status'],
553
- ['channels.setup.get', 'channel onboarding/status'],
554
- ['channels.doctor.get', 'channel diagnostics'],
555
- ['channels.actions.list', 'channel capabilities'],
556
- ['channels.tools.list', 'channel capabilities'],
557
- ['channels.targets.resolve', 'routine delivery planning'],
558
- ['channels.policies.list', 'channel safety posture'],
559
- ['mcp.config.get', 'MCP setup/status'],
560
- ['mcp.servers.list', 'MCP setup/status'],
561
- ['mcp.tools.list', 'MCP setup/status'],
562
- ['web_search.providers.list', 'research/tool readiness'],
563
- ['web_search.query', 'research/tool readiness'],
564
- ['voice.status', 'voice setup/status'],
565
- ['voice.providers.list', 'voice setup/status'],
566
- ['voice.voices.list', 'voice setup/status'],
567
- ['media.providers.list', 'media setup/status'],
568
- ['multimodal.providers.list', 'multimodal setup/status'],
569
- ['remote.snapshot', 'remote/node setup'],
570
- ['remote.peers.list', 'remote/node setup'],
571
- ['remote.work.list', 'remote work status'],
572
- ['remote.node_host.contract', 'remote/node setup'],
573
- ]);
574
-
575
- const EXPLICIT_CONFIRMATION_METHOD_SURFACES = new Map<string, string>([
576
- ['approvals.approve', 'approvals approve --yes'],
577
- ['approvals.deny', 'approvals deny --yes'],
578
- ['approvals.cancel', 'approvals cancel --yes'],
579
- ['automation.jobs.run', 'automation jobs run --yes'],
580
- ['automation.jobs.pause', 'automation jobs pause --yes'],
581
- ['automation.jobs.resume', 'automation jobs resume --yes'],
582
- ['automation.runs.cancel', 'automation runs cancel --yes'],
583
- ['automation.runs.retry', 'automation runs retry --yes'],
584
- ['schedules.run', 'schedules run --yes'],
585
- ]);
586
-
587
- const READ_ONLY_CATEGORY_SURFACES = new Map<string, string>([
588
- ['approvals', 'approvals / approval risk'],
589
- ['artifacts', 'artifact-aware chat and receipts'],
590
- ['automation', 'automation status'],
591
- ['channels', 'channel onboarding/status'],
592
- ['companion', 'chat session management'],
593
- ['control', 'status / capabilities'],
594
- ['mcp', 'MCP setup/status'],
595
- ['media', 'media setup/status'],
596
- ['multimodal', 'multimodal setup/status'],
597
- ['projectPlanning', 'workplan'],
598
- ['providers', 'provider/model setup'],
599
- ['remote', 'remote/node setup'],
600
- ['schedules', 'schedules'],
601
- ['scheduler', 'automation capacity'],
602
- ['sessions', 'delegations status'],
603
- ['tasks', 'delegations status'],
604
- ['voice', 'voice setup/status'],
605
- ['web_search', 'research/tool readiness'],
606
- ]);
607
-
608
- function isDefaultKnowledgeMethod(method: DaemonCapabilityInventoryMethod): boolean {
609
- return method.id.startsWith('knowledge.');
610
- }
611
-
612
- function isHomeGraphMethod(method: DaemonCapabilityInventoryMethod): boolean {
613
- return method.id.startsWith('homeassistant.') || method.id.toLowerCase().includes('homegraph');
614
- }
615
-
616
- function classifyDaemonMethodUx(method: DaemonCapabilityInventoryMethod): DaemonCapabilityUxMethod {
617
- const explicitSurface = EXPLICIT_CONFIRMATION_METHOD_SURFACES.get(method.id);
618
- if (explicitSurface) {
619
- return {
620
- ...method,
621
- uxCoverage: 'explicit_confirmation',
622
- surface: explicitSurface,
623
- next: 'Already exposed only through exact user commands plus confirmation.',
624
- };
625
- }
626
-
627
- const usableSurface = USABLE_METHOD_SURFACES.get(method.id);
628
- if (usableSurface) {
629
- return {
630
- ...method,
631
- uxCoverage: 'usable',
632
- surface: usableSurface,
633
- next: 'Keep this route covered by command/help/workspace smoke as the daemon contract evolves.',
634
- };
635
- }
636
-
637
- if (isDefaultKnowledgeMethod(method)) {
638
- return {
639
- ...method,
640
- uxCoverage: 'blocked',
641
- surface: 'blocked for Agent Knowledge',
642
- next: 'Do not use default Knowledge/Wiki as an Agent fallback; use /api/goodvibes-agent/knowledge/* only.',
643
- };
644
- }
645
-
646
- if (isHomeGraphMethod(method)) {
647
- return {
648
- ...method,
649
- uxCoverage: 'blocked',
650
- surface: 'blocked for Agent product boundary',
651
- next: 'Do not use HomeGraph or Home Assistant routes as Agent Knowledge fallback.',
652
- };
653
- }
654
-
655
- const readOnlySurface = READ_ONLY_CATEGORY_SURFACES.get(method.category);
656
- if (method.readOnly && readOnlySurface) {
657
- return {
658
- ...method,
659
- uxCoverage: 'read_only',
660
- surface: readOnlySurface,
661
- next: 'Visible through read-only posture, inventory, or status surfaces; add focused UX if this becomes user-facing.',
662
- };
663
- }
664
-
665
- return {
666
- ...method,
667
- uxCoverage: 'not_surfaced',
668
- surface: 'daemon inventory only',
669
- next: method.mutating || method.dangerous
670
- ? 'Design an exact command, approval posture, and confirmation flow before exposing this side effect.'
671
- : 'Add a focused Agent command or workspace card if this daemon capability is part of the product surface.',
672
- };
673
- }
674
-
675
- function daemonVersionFromStatus(body: unknown): string {
676
- if (!isRecord(body)) return 'unknown';
677
- return readString(body, 'version')
678
- ?? readString(body, 'sdkVersion')
679
- ?? 'unknown';
680
- }
681
-
682
- function buildHeaders(connection: AgentDaemonConnection): Headers {
683
- const headers = new Headers({ accept: 'application/json' });
684
- if (connection.token) headers.set('authorization', `Bearer ${connection.token}`);
685
- return headers;
686
- }
687
-
688
- async function fetchJson(connection: AgentDaemonConnection, route: string): Promise<FetchJsonResult> {
689
- const response = await fetch(`${connection.baseUrl}${route}`, {
690
- method: 'GET',
691
- headers: buildHeaders(connection),
692
- });
693
- const text = await response.text();
694
- let body: unknown = text;
695
- if (text.trim().length > 0) {
696
- try {
697
- body = JSON.parse(text) as unknown;
698
- } catch {
699
- body = text;
700
- }
701
- }
702
- return {
703
- ok: response.ok,
704
- status: response.status,
705
- statusText: response.statusText,
706
- body,
707
- };
708
- }
709
-
710
- function failureFromResponse(
711
- response: FetchJsonResult,
712
- connection: AgentDaemonConnection,
713
- route: string,
714
- daemonVersion: string,
715
- ): DaemonCapabilityAuditFailure {
716
- const detail = isRecord(response.body) && typeof response.body.error === 'string'
717
- ? response.body.error
718
- : typeof response.body === 'string'
719
- ? response.body
720
- : response.statusText;
721
- const error = `HTTP ${response.status}${detail ? `: ${detail}` : ''}`;
722
- if (response.status === 401 || response.status === 403) {
723
- return { ok: false, kind: 'auth_required', error, baseUrl: connection.baseUrl, route };
724
- }
725
- if (response.status === 404 && daemonVersion !== 'unknown' && daemonVersion !== SDK_VERSION) {
726
- return {
727
- ok: false,
728
- kind: 'version_mismatch',
729
- error: `External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; ${route} is unavailable.`,
730
- baseUrl: connection.baseUrl,
731
- route,
732
- daemonVersion,
733
- expectedSdkVersion: SDK_VERSION,
734
- };
735
- }
736
- if (response.status === 404) {
737
- return { ok: false, kind: 'daemon_route_unavailable', error, baseUrl: connection.baseUrl, route };
738
- }
739
- return { ok: false, kind: 'daemon_error', error, baseUrl: connection.baseUrl, route };
740
- }
741
-
742
- function failureFromThrown(error: unknown, connection: AgentDaemonConnection, route: string): DaemonCapabilityAuditFailure {
743
- const message = summarizeError(error);
744
- const lower = message.toLowerCase();
745
- if (lower.includes('unauthorized') || lower.includes('401') || lower.includes('403')) {
746
- return { ok: false, kind: 'auth_required', error: message, baseUrl: connection.baseUrl, route };
747
- }
748
- if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
749
- return { ok: false, kind: 'daemon_unavailable', error: message, baseUrl: connection.baseUrl, route };
750
- }
751
- if (lower.includes('404') || lower.includes('not found')) {
752
- return { ok: false, kind: 'daemon_route_unavailable', error: message, baseUrl: connection.baseUrl, route };
753
- }
754
- return { ok: false, kind: 'daemon_error', error: message, baseUrl: connection.baseUrl, route };
755
- }
756
-
757
- export function buildDaemonCapabilityAuditAreas(
758
- methodIds: ReadonlySet<string>,
759
- agentKnowledgeRouteReady: boolean | null,
760
- methodSummaries: readonly DaemonMethodSummary[] = [],
761
- ): readonly DaemonCapabilityAuditArea[] {
762
- const methodsById = new Map(methodSummaries.map((method) => [method.id, method]));
763
- return DAEMON_CAPABILITY_REQUIREMENTS.map((requirement) => {
764
- const presentRequiredMethodIds = requirement.requiredMethodIds.filter((methodId) => methodIds.has(methodId));
765
- const missingRequiredMethodIds = requirement.requiredMethodIds.filter((methodId) => !methodIds.has(methodId));
766
- const presentOptionalMethodIds = requirement.optionalMethodIds.filter((methodId) => methodIds.has(methodId));
767
- const missingOptionalMethodIds = requirement.optionalMethodIds.filter((methodId) => !methodIds.has(methodId));
768
- const agentRoutes = requirement.requiredAgentRoutes.map((route) => ({
769
- route,
770
- coverage: agentKnowledgeRouteReady === null
771
- ? 'not_checked'
772
- : agentKnowledgeRouteReady
773
- ? 'ready'
774
- : 'missing',
775
- } satisfies DaemonCapabilityAuditArea['agentRoutes'][number]));
776
- const missingAgentRoutes = agentRoutes.filter((route) => route.coverage === 'missing');
777
- const areaMethodIds = [...requirement.requiredMethodIds, ...requirement.optionalMethodIds];
778
- const areaMethods = areaMethodIds.flatMap((methodId): DaemonMethodSummary[] => {
779
- const method = methodsById.get(methodId);
780
- return method ? [method] : [];
781
- });
782
- const readOnlyMethodIds = areaMethods.filter((method) => {
783
- const verb = method.http?.method?.toUpperCase();
784
- return verb === 'GET' || verb === 'HEAD';
785
- }).map((method) => method.id);
786
- const mutatingMethodIds = areaMethods.filter((method) => {
787
- const verb = method.http?.method?.toUpperCase();
788
- return Boolean(verb) && verb !== 'GET' && verb !== 'HEAD';
789
- }).map((method) => method.id);
790
- const authenticatedMethodIds = areaMethods
791
- .filter((method) => method.access === 'authenticated')
792
- .map((method) => method.id);
793
- const dangerousMethodIds = areaMethods
794
- .filter((method) => method.dangerous === true)
795
- .map((method) => method.id);
796
- const requiredCount = requirement.requiredMethodIds.length + requirement.requiredAgentRoutes.length;
797
- const presentRequiredCount = presentRequiredMethodIds.length
798
- + agentRoutes.filter((route) => route.coverage === 'ready').length;
799
- const coverage: DaemonCapabilityCoverage = requiredCount === presentRequiredCount
800
- ? 'ready'
801
- : presentRequiredCount > 0 && missingRequiredMethodIds.length + missingAgentRoutes.length < requiredCount
802
- ? 'partial'
803
- : 'missing';
804
- return {
805
- id: requirement.id,
806
- title: requirement.title,
807
- coverage,
808
- competitorBaseline: requirement.competitorBaseline,
809
- agentUse: requirement.agentUse,
810
- presentRequiredMethodIds,
811
- missingRequiredMethodIds,
812
- presentOptionalMethodIds,
813
- missingOptionalMethodIds,
814
- agentRoutes,
815
- routeRisk: {
816
- readOnlyMethodIds,
817
- mutatingMethodIds,
818
- authenticatedMethodIds,
819
- readOnlyMethodCount: readOnlyMethodIds.length,
820
- mutatingMethodCount: mutatingMethodIds.length,
821
- authenticatedMethodCount: authenticatedMethodIds.length,
822
- dangerousMethodIds,
823
- },
824
- next: requirement.next,
825
- };
826
- });
827
- }
828
-
829
- export async function fetchLiveDaemonCapabilityAudit(
830
- connection: AgentDaemonConnection,
831
- ): Promise<DaemonCapabilityAuditResult> {
832
- let daemonVersion = 'unknown';
833
- try {
834
- const status = await fetchJson(connection, DAEMON_STATUS_ROUTE);
835
- if (status.ok) {
836
- daemonVersion = daemonVersionFromStatus(status.body);
837
- } else if (status.status === 401 || status.status === 403) {
838
- return failureFromResponse(status, connection, DAEMON_STATUS_ROUTE, daemonVersion);
839
- }
840
-
841
- const methods = await fetchJson(connection, DAEMON_METHOD_CATALOG_ROUTE);
842
- if (!methods.ok) return failureFromResponse(methods, connection, DAEMON_METHOD_CATALOG_ROUTE, daemonVersion);
843
-
844
- const methodSummaries = readMethodSummaries(methods.body);
845
- const methodIds = new Set(methodSummaries.map((method) => method.id));
846
- const warnings: string[] = [];
847
- const daemonCompatible = daemonVersion === SDK_VERSION;
848
- if (daemonVersion !== 'unknown' && !daemonCompatible) {
849
- warnings.push(`External daemon SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}.`);
850
- }
851
-
852
- const agentKnowledge = await fetchJson(connection, AGENT_KNOWLEDGE_STATUS_ROUTE);
853
- if (!agentKnowledge.ok) {
854
- const failure = failureFromResponse(agentKnowledge, connection, AGENT_KNOWLEDGE_STATUS_ROUTE, daemonVersion);
855
- if (failure.kind === 'auth_required') return failure;
856
- warnings.push(`${AGENT_KNOWLEDGE_STATUS_ROUTE} is not ready: ${failure.error}`);
857
- }
858
-
859
- return {
860
- ok: true,
861
- kind: 'daemon.capabilities.audit',
862
- baseUrl: connection.baseUrl,
863
- daemonVersion,
864
- expectedSdkVersion: SDK_VERSION,
865
- daemonCompatible,
866
- methodCatalogRoute: DAEMON_METHOD_CATALOG_ROUTE,
867
- methodCount: methodSummaries.length,
868
- agentKnowledgeRoute: AGENT_KNOWLEDGE_STATUS_ROUTE,
869
- agentKnowledgeRouteReady: agentKnowledge.ok,
870
- defaultKnowledgeFallback: false,
871
- homeGraphFallback: false,
872
- warnings,
873
- areas: buildDaemonCapabilityAuditAreas(methodIds, agentKnowledge.ok, methodSummaries),
874
- };
875
- } catch (error) {
876
- return failureFromThrown(error, connection, daemonVersion === 'unknown' ? DAEMON_STATUS_ROUTE : DAEMON_METHOD_CATALOG_ROUTE);
877
- }
878
- }
879
-
880
- export function buildDaemonCapabilityInventoryReport(
881
- connection: AgentDaemonConnection,
882
- daemonVersion: string,
883
- agentKnowledgeRouteReady: boolean,
884
- methodSummaries: readonly DaemonMethodSummary[],
885
- ): DaemonCapabilityInventoryReport {
886
- const methods = methodSummaries.map((method): DaemonCapabilityInventoryMethod => {
887
- const httpMethod = normalizeHttpMethod(method);
888
- const readOnly = isReadOnlyHttpMethod(httpMethod);
889
- return {
890
- id: method.id,
891
- title: method.title,
892
- category: normalizeMethodCategory(method),
893
- access: normalizeAccess(method),
894
- invokable: typeof method.invokable === 'boolean' ? method.invokable : null,
895
- dangerous: method.dangerous === true,
896
- httpMethod,
897
- path: method.http?.path,
898
- readOnly,
899
- mutating: httpMethod !== 'UNKNOWN' && !readOnly,
900
- };
901
- }).sort(compareInventoryMethods);
902
-
903
- const groupsByCategory = new Map<string, DaemonCapabilityInventoryMethod[]>();
904
- for (const method of methods) {
905
- const existing = groupsByCategory.get(method.category) ?? [];
906
- existing.push(method);
907
- groupsByCategory.set(method.category, existing);
908
- }
909
-
910
- const groups = [...groupsByCategory.entries()].map(([category, categoryMethods]): DaemonCapabilityInventoryGroup => ({
911
- category,
912
- methodCount: categoryMethods.length,
913
- readOnlyMethodCount: categoryMethods.filter((method) => method.readOnly).length,
914
- mutatingMethodCount: categoryMethods.filter((method) => method.mutating).length,
915
- authenticatedMethodCount: categoryMethods.filter((method) => method.access === 'authenticated').length,
916
- dangerousMethodCount: categoryMethods.filter((method) => method.dangerous).length,
917
- methods: categoryMethods,
918
- })).sort(compareInventoryGroups);
919
-
920
- const accessEntries = new Map<string, number>();
921
- for (const method of methods) {
922
- accessEntries.set(method.access, (accessEntries.get(method.access) ?? 0) + 1);
923
- }
924
- const accessCounts = [...accessEntries.entries()]
925
- .map(([access, count]) => ({ access, count }))
926
- .sort((left, right) => {
927
- const countDelta = right.count - left.count;
928
- if (countDelta !== 0) return countDelta;
929
- return left.access.localeCompare(right.access);
930
- });
931
-
932
- return {
933
- ok: true,
934
- kind: 'daemon.capabilities.inventory',
935
- baseUrl: connection.baseUrl,
936
- daemonVersion,
937
- expectedSdkVersion: SDK_VERSION,
938
- daemonCompatible: daemonVersion === SDK_VERSION,
939
- methodCatalogRoute: DAEMON_METHOD_CATALOG_ROUTE,
940
- methodCount: methods.length,
941
- agentKnowledgeRoute: AGENT_KNOWLEDGE_STATUS_ROUTE,
942
- agentKnowledgeRouteReady,
943
- defaultKnowledgeFallback: false,
944
- homeGraphFallback: false,
945
- readOnlyMethodCount: methods.filter((method) => method.readOnly).length,
946
- mutatingMethodCount: methods.filter((method) => method.mutating).length,
947
- authenticatedMethodCount: methods.filter((method) => method.access === 'authenticated').length,
948
- dangerousMethodCount: methods.filter((method) => method.dangerous).length,
949
- accessCounts,
950
- groups,
951
- };
952
- }
953
-
954
- export type DaemonCapabilityInventoryResult =
955
- | DaemonCapabilityInventoryReport
956
- | DaemonCapabilityAuditFailure;
957
-
958
- export type DaemonCapabilityUxCoverageResult =
959
- | DaemonCapabilityUxCoverageReport
960
- | DaemonCapabilityAuditFailure;
961
-
962
- export function buildDaemonCapabilityUxCoverageReport(
963
- inventory: DaemonCapabilityInventoryReport,
964
- ): DaemonCapabilityUxCoverageReport {
965
- const methods = inventory.groups
966
- .flatMap((group) => group.methods)
967
- .map(classifyDaemonMethodUx);
968
-
969
- const groupsByCategory = new Map<string, DaemonCapabilityUxMethod[]>();
970
- for (const method of methods) {
971
- const existing = groupsByCategory.get(method.category) ?? [];
972
- existing.push(method);
973
- groupsByCategory.set(method.category, existing);
974
- }
975
-
976
- const groups = [...groupsByCategory.entries()].map(([category, categoryMethods]): DaemonCapabilityUxGroup => ({
977
- category,
978
- methodCount: categoryMethods.length,
979
- usableMethodCount: categoryMethods.filter((method) => method.uxCoverage === 'usable').length,
980
- readOnlyMethodCount: categoryMethods.filter((method) => method.uxCoverage === 'read_only').length,
981
- explicitConfirmationMethodCount: categoryMethods.filter((method) => method.uxCoverage === 'explicit_confirmation').length,
982
- blockedMethodCount: categoryMethods.filter((method) => method.uxCoverage === 'blocked').length,
983
- notSurfacedMethodCount: categoryMethods.filter((method) => method.uxCoverage === 'not_surfaced').length,
984
- methods: categoryMethods,
985
- })).sort(compareUxGroups);
986
-
987
- return {
988
- ok: true,
989
- kind: 'daemon.capabilities.ux_coverage',
990
- baseUrl: inventory.baseUrl,
991
- daemonVersion: inventory.daemonVersion,
992
- expectedSdkVersion: inventory.expectedSdkVersion,
993
- daemonCompatible: inventory.daemonCompatible,
994
- methodCatalogRoute: inventory.methodCatalogRoute,
995
- methodCount: inventory.methodCount,
996
- agentKnowledgeRoute: inventory.agentKnowledgeRoute,
997
- agentKnowledgeRouteReady: inventory.agentKnowledgeRouteReady,
998
- defaultKnowledgeFallback: false,
999
- homeGraphFallback: false,
1000
- usableMethodCount: methods.filter((method) => method.uxCoverage === 'usable').length,
1001
- readOnlyMethodCount: methods.filter((method) => method.uxCoverage === 'read_only').length,
1002
- explicitConfirmationMethodCount: methods.filter((method) => method.uxCoverage === 'explicit_confirmation').length,
1003
- blockedMethodCount: methods.filter((method) => method.uxCoverage === 'blocked').length,
1004
- notSurfacedMethodCount: methods.filter((method) => method.uxCoverage === 'not_surfaced').length,
1005
- groups,
1006
- };
1007
- }
1008
-
1009
- export function filterDaemonCapabilityUxGroups(
1010
- groups: readonly DaemonCapabilityUxGroup[],
1011
- query: string | undefined,
1012
- ): readonly DaemonCapabilityUxGroup[] {
1013
- const normalized = query?.trim().toLowerCase();
1014
- if (!normalized) return groups;
1015
- return groups.flatMap((group): DaemonCapabilityUxGroup[] => {
1016
- const categoryMatches = group.category.toLowerCase().includes(normalized);
1017
- const methods = categoryMatches
1018
- ? group.methods
1019
- : group.methods.filter((method) => {
1020
- return method.id.toLowerCase().includes(normalized)
1021
- || method.title?.toLowerCase().includes(normalized) === true
1022
- || method.uxCoverage.includes(normalized)
1023
- || method.surface.toLowerCase().includes(normalized)
1024
- || method.next.toLowerCase().includes(normalized)
1025
- || method.path?.toLowerCase().includes(normalized) === true;
1026
- });
1027
- if (methods.length === 0) return [];
1028
- return [{
1029
- category: group.category,
1030
- methodCount: methods.length,
1031
- usableMethodCount: methods.filter((method) => method.uxCoverage === 'usable').length,
1032
- readOnlyMethodCount: methods.filter((method) => method.uxCoverage === 'read_only').length,
1033
- explicitConfirmationMethodCount: methods.filter((method) => method.uxCoverage === 'explicit_confirmation').length,
1034
- blockedMethodCount: methods.filter((method) => method.uxCoverage === 'blocked').length,
1035
- notSurfacedMethodCount: methods.filter((method) => method.uxCoverage === 'not_surfaced').length,
1036
- methods,
1037
- }];
1038
- });
1039
- }
1040
-
1041
- export async function fetchLiveDaemonCapabilityUxCoverage(
1042
- connection: AgentDaemonConnection,
1043
- ): Promise<DaemonCapabilityUxCoverageResult> {
1044
- const inventory = await fetchLiveDaemonCapabilityInventory(connection);
1045
- if (!inventory.ok) return inventory;
1046
- return buildDaemonCapabilityUxCoverageReport(inventory);
1047
- }
1048
-
1049
- export async function fetchLiveDaemonCapabilityInventory(
1050
- connection: AgentDaemonConnection,
1051
- ): Promise<DaemonCapabilityInventoryResult> {
1052
- let daemonVersion = 'unknown';
1053
- try {
1054
- const status = await fetchJson(connection, DAEMON_STATUS_ROUTE);
1055
- if (status.ok) {
1056
- daemonVersion = daemonVersionFromStatus(status.body);
1057
- } else if (status.status === 401 || status.status === 403) {
1058
- return failureFromResponse(status, connection, DAEMON_STATUS_ROUTE, daemonVersion);
1059
- }
1060
-
1061
- const methods = await fetchJson(connection, DAEMON_METHOD_CATALOG_ROUTE);
1062
- if (!methods.ok) return failureFromResponse(methods, connection, DAEMON_METHOD_CATALOG_ROUTE, daemonVersion);
1063
-
1064
- const agentKnowledge = await fetchJson(connection, AGENT_KNOWLEDGE_STATUS_ROUTE);
1065
- if (!agentKnowledge.ok) {
1066
- const failure = failureFromResponse(agentKnowledge, connection, AGENT_KNOWLEDGE_STATUS_ROUTE, daemonVersion);
1067
- if (failure.kind === 'auth_required') return failure;
1068
- }
1069
-
1070
- return buildDaemonCapabilityInventoryReport(
1071
- connection,
1072
- daemonVersion,
1073
- agentKnowledge.ok,
1074
- readMethodSummaries(methods.body),
1075
- );
1076
- } catch (error) {
1077
- return failureFromThrown(error, connection, daemonVersion === 'unknown' ? DAEMON_STATUS_ROUTE : DAEMON_METHOD_CATALOG_ROUTE);
1078
- }
1079
- }
1080
-
1081
- export function filterDaemonCapabilityAuditAreas(
1082
- areas: readonly DaemonCapabilityAuditArea[],
1083
- query: string | undefined,
1084
- ): readonly DaemonCapabilityAuditArea[] {
1085
- const normalized = query?.trim().toLowerCase();
1086
- if (!normalized) return areas;
1087
- return areas.filter((area) => {
1088
- if (area.id.includes(normalized)) return true;
1089
- if (area.title.toLowerCase().includes(normalized)) return true;
1090
- if (area.coverage.includes(normalized)) return true;
1091
- if (area.agentUse.toLowerCase().includes(normalized)) return true;
1092
- return area.presentRequiredMethodIds.some((methodId) => methodId.includes(normalized))
1093
- || area.missingRequiredMethodIds.some((methodId) => methodId.includes(normalized))
1094
- || area.presentOptionalMethodIds.some((methodId) => methodId.includes(normalized))
1095
- || area.missingOptionalMethodIds.some((methodId) => methodId.includes(normalized))
1096
- || area.agentRoutes.some((route) => route.route.toLowerCase().includes(normalized));
1097
- });
1098
- }
1099
-
1100
- function gapToken(value: string): string {
1101
- return value
1102
- .toLowerCase()
1103
- .replace(/[^a-z0-9]+/g, '-')
1104
- .replace(/^-+|-+$/g, '')
1105
- || 'gap';
1106
- }
1107
-
1108
- function gapSeverityRank(severity: DaemonCapabilityGapSeverity): number {
1109
- if (severity === 'blocker') return 0;
1110
- if (severity === 'high') return 1;
1111
- if (severity === 'medium') return 2;
1112
- return 3;
1113
- }
1114
-
1115
- function sortCapabilityGaps(gaps: readonly DaemonCapabilityGap[]): readonly DaemonCapabilityGap[] {
1116
- return [...gaps].sort((left, right) => {
1117
- const severityDelta = gapSeverityRank(left.severity) - gapSeverityRank(right.severity);
1118
- if (severityDelta !== 0) return severityDelta;
1119
- return left.id.localeCompare(right.id);
1120
- });
1121
- }
1122
-
1123
- export function buildDaemonCapabilityGapReport(
1124
- audit: DaemonCapabilityAuditSuccess,
1125
- areas: readonly DaemonCapabilityAuditArea[] = audit.areas,
1126
- ): DaemonCapabilityGapReport {
1127
- const gaps: DaemonCapabilityGap[] = [];
1128
-
1129
- if (!audit.daemonCompatible) {
1130
- gaps.push({
1131
- id: 'daemon-version-mismatch',
1132
- kind: 'version_mismatch',
1133
- severity: audit.agentKnowledgeRouteReady ? 'high' : 'blocker',
1134
- title: 'Daemon SDK version does not match Agent SDK pin',
1135
- detail: `Agent expects ${audit.expectedSdkVersion}; daemon reports ${audit.daemonVersion}.`,
1136
- action: 'Update/restart the externally owned GoodVibes daemon before release validation; Agent will not start it.',
1137
- });
1138
- }
1139
-
1140
- for (const area of areas) {
1141
- if (area.missingRequiredMethodIds.length > 0) {
1142
- gaps.push({
1143
- id: `${area.id}-missing-required-methods`,
1144
- kind: 'required_method_missing',
1145
- severity: 'high',
1146
- areaId: area.id,
1147
- title: `${area.title} missing required daemon methods`,
1148
- detail: area.missingRequiredMethodIds.join(', '),
1149
- action: 'Keep the Agent surface read-only or blocked for this area until the public daemon route contract is present.',
1150
- });
1151
- }
1152
-
1153
- for (const route of area.agentRoutes) {
1154
- if (route.coverage !== 'missing') continue;
1155
- gaps.push({
1156
- id: `${area.id}-missing-${gapToken(route.route)}`,
1157
- kind: 'agent_route_missing',
1158
- severity: route.route === AGENT_KNOWLEDGE_STATUS_ROUTE ? 'blocker' : 'high',
1159
- areaId: area.id,
1160
- title: `${area.title} missing Agent route`,
1161
- detail: route.route,
1162
- action: 'Fail closed for this product segment. Do not query default Knowledge/Wiki, HomeGraph, or Home Assistant routes.',
1163
- });
1164
- }
1165
-
1166
- if (area.routeRisk.dangerousMethodIds.length > 0) {
1167
- gaps.push({
1168
- id: `${area.id}-dangerous-route-review`,
1169
- kind: 'route_risk_review',
1170
- severity: 'medium',
1171
- areaId: area.id,
1172
- title: `${area.title} has dangerous daemon routes`,
1173
- detail: area.routeRisk.dangerousMethodIds.join(', '),
1174
- action: 'Keep these routes behind exact commands, confirmation, and concise approval UX; never trigger them from ordinary chat.',
1175
- });
1176
- }
1177
-
1178
- for (const next of area.next) {
1179
- gaps.push({
1180
- id: `${area.id}-agent-ux-${gapToken(next)}`,
1181
- kind: 'agent_ux_gap',
1182
- severity: area.coverage === 'ready' ? 'medium' : 'low',
1183
- areaId: area.id,
1184
- title: `${area.title} Agent UX gap`,
1185
- detail: next,
1186
- action: 'Build a first-class Agent workspace, command, or setup flow on top of the existing daemon capability.',
1187
- });
1188
- }
1189
- }
1190
-
1191
- const sortedGaps = sortCapabilityGaps(gaps);
1192
- return {
1193
- ok: true,
1194
- kind: 'daemon.capabilities.gaps',
1195
- baseUrl: audit.baseUrl,
1196
- daemonVersion: audit.daemonVersion,
1197
- expectedSdkVersion: audit.expectedSdkVersion,
1198
- daemonCompatible: audit.daemonCompatible,
1199
- methodCatalogRoute: audit.methodCatalogRoute,
1200
- agentKnowledgeRoute: audit.agentKnowledgeRoute,
1201
- agentKnowledgeRouteReady: audit.agentKnowledgeRouteReady,
1202
- defaultKnowledgeFallback: false,
1203
- homeGraphFallback: false,
1204
- gapCount: sortedGaps.length,
1205
- gaps: sortedGaps,
1206
- };
1207
- }
1208
-
1209
- export function filterDaemonCapabilityGaps(
1210
- gaps: readonly DaemonCapabilityGap[],
1211
- query: string | undefined,
1212
- ): readonly DaemonCapabilityGap[] {
1213
- const normalized = query?.trim().toLowerCase();
1214
- if (!normalized) return gaps;
1215
- return gaps.filter((gap) => {
1216
- return gap.id.includes(normalized)
1217
- || gap.kind.includes(normalized)
1218
- || gap.severity.includes(normalized)
1219
- || gap.title.toLowerCase().includes(normalized)
1220
- || gap.detail.toLowerCase().includes(normalized)
1221
- || gap.action.toLowerCase().includes(normalized)
1222
- || Boolean(gap.areaId?.includes(normalized));
1223
- });
1224
- }
1225
-
1226
- export function renderDaemonCapabilityGaps(
1227
- report: DaemonCapabilityGapReport,
1228
- gaps: readonly DaemonCapabilityGap[] = report.gaps,
1229
- ): string {
1230
- const lines: string[] = [
1231
- 'GoodVibes daemon capability gaps',
1232
- ` daemon: ${report.baseUrl}`,
1233
- ` SDK: Agent expects ${report.expectedSdkVersion}; daemon reports ${report.daemonVersion}`,
1234
- ` compatibility: ${report.daemonCompatible ? 'matched' : 'mismatch'}`,
1235
- ` Agent Knowledge: ${report.agentKnowledgeRouteReady ? 'ready' : 'missing'} ${report.agentKnowledgeRoute}`,
1236
- ' isolation: default Knowledge/Wiki fallback no; HomeGraph fallback no',
1237
- ` gaps: ${gaps.length}/${report.gapCount}`,
1238
- '',
1239
- ];
1240
-
1241
- if (gaps.length === 0) {
1242
- lines.push('No daemon capability gaps matched this query.');
1243
- return lines.join('\n');
1244
- }
1245
-
1246
- for (const gap of gaps) {
1247
- lines.push(`${gap.title} [${gap.severity}; ${gap.kind}]`);
1248
- if (gap.areaId) lines.push(` area: ${gap.areaId}`);
1249
- lines.push(` detail: ${gap.detail}`);
1250
- lines.push(` action: ${gap.action}`);
1251
- lines.push('');
1252
- }
1253
-
1254
- return lines.join('\n').trimEnd();
1255
- }
1256
-
1257
- export function buildDaemonCapabilityRouteRiskReport(
1258
- audit: DaemonCapabilityAuditSuccess,
1259
- areas: readonly DaemonCapabilityAuditArea[] = audit.areas,
1260
- ): DaemonCapabilityRouteRiskReport {
1261
- const riskAreas = areas.map((area): DaemonCapabilityRouteRiskArea => ({
1262
- areaId: area.id,
1263
- title: area.title,
1264
- coverage: area.coverage,
1265
- readOnlyMethodIds: area.routeRisk.readOnlyMethodIds,
1266
- mutatingMethodIds: area.routeRisk.mutatingMethodIds,
1267
- authenticatedMethodIds: area.routeRisk.authenticatedMethodIds,
1268
- readOnlyMethodCount: area.routeRisk.readOnlyMethodCount,
1269
- mutatingMethodCount: area.routeRisk.mutatingMethodCount,
1270
- authenticatedMethodCount: area.routeRisk.authenticatedMethodCount,
1271
- dangerousMethodIds: area.routeRisk.dangerousMethodIds,
1272
- }));
1273
- const readOnlyMethodIds = new Set(riskAreas.flatMap((area) => area.readOnlyMethodIds));
1274
- const mutatingMethodIds = new Set(riskAreas.flatMap((area) => area.mutatingMethodIds));
1275
- const authenticatedMethodIds = new Set(riskAreas.flatMap((area) => area.authenticatedMethodIds));
1276
- const dangerousMethodIds = new Set(riskAreas.flatMap((area) => area.dangerousMethodIds));
1277
-
1278
- return {
1279
- ok: true,
1280
- kind: 'daemon.capabilities.route_risk',
1281
- baseUrl: audit.baseUrl,
1282
- daemonVersion: audit.daemonVersion,
1283
- expectedSdkVersion: audit.expectedSdkVersion,
1284
- daemonCompatible: audit.daemonCompatible,
1285
- methodCatalogRoute: audit.methodCatalogRoute,
1286
- agentKnowledgeRoute: audit.agentKnowledgeRoute,
1287
- agentKnowledgeRouteReady: audit.agentKnowledgeRouteReady,
1288
- defaultKnowledgeFallback: false,
1289
- homeGraphFallback: false,
1290
- totalReadOnlyMethodCount: readOnlyMethodIds.size,
1291
- totalMutatingMethodCount: mutatingMethodIds.size,
1292
- totalAuthenticatedMethodCount: authenticatedMethodIds.size,
1293
- totalDangerousMethodCount: dangerousMethodIds.size,
1294
- areas: riskAreas,
1295
- };
1296
- }
1297
-
1298
- export function filterDaemonCapabilityRouteRiskAreas(
1299
- areas: readonly DaemonCapabilityRouteRiskArea[],
1300
- query: string | undefined,
1301
- ): readonly DaemonCapabilityRouteRiskArea[] {
1302
- const normalized = query?.trim().toLowerCase();
1303
- if (!normalized) return areas;
1304
- return areas.filter((area) => {
1305
- return area.areaId.includes(normalized)
1306
- || area.title.toLowerCase().includes(normalized)
1307
- || area.coverage.includes(normalized)
1308
- || area.dangerousMethodIds.some((methodId) => methodId.includes(normalized));
1309
- });
1310
- }
1311
-
1312
- export function renderDaemonCapabilityRouteRisk(
1313
- report: DaemonCapabilityRouteRiskReport,
1314
- areas: readonly DaemonCapabilityRouteRiskArea[] = report.areas,
1315
- ): string {
1316
- const lines: string[] = [
1317
- 'GoodVibes daemon route risk review',
1318
- ` daemon: ${report.baseUrl}`,
1319
- ` SDK: Agent expects ${report.expectedSdkVersion}; daemon reports ${report.daemonVersion}`,
1320
- ` compatibility: ${report.daemonCompatible ? 'matched' : 'mismatch'}`,
1321
- ` method catalog: ${report.methodCatalogRoute}`,
1322
- ` Agent Knowledge: ${report.agentKnowledgeRouteReady ? 'ready' : 'missing'} ${report.agentKnowledgeRoute}`,
1323
- ' isolation: default Knowledge/Wiki fallback no; HomeGraph fallback no',
1324
- ` totals: ${report.totalReadOnlyMethodCount} read-only; ${report.totalMutatingMethodCount} mutating; ${report.totalDangerousMethodCount} dangerous; ${report.totalAuthenticatedMethodCount} authenticated`,
1325
- ' policy: exact command plus confirmation for side effects; ordinary chat never triggers mutating routes',
1326
- '',
1327
- ];
1328
-
1329
- const visibleAreas = areas.filter((area) => {
1330
- return area.readOnlyMethodCount > 0
1331
- || area.mutatingMethodCount > 0
1332
- || area.authenticatedMethodCount > 0
1333
- || area.dangerousMethodIds.length > 0;
1334
- });
1335
- if (visibleAreas.length === 0) {
1336
- lines.push('No route risk metadata matched this query.');
1337
- return lines.join('\n');
1338
- }
1339
-
1340
- for (const area of visibleAreas) {
1341
- lines.push(`${area.title} [${area.coverage}]`);
1342
- lines.push(` methods: ${area.readOnlyMethodCount} read-only; ${area.mutatingMethodCount} mutating; ${area.dangerousMethodIds.length} dangerous; ${area.authenticatedMethodCount} authenticated`);
1343
- if (area.dangerousMethodIds.length > 0) {
1344
- lines.push(` dangerous methods: ${area.dangerousMethodIds.join(', ')}`);
1345
- }
1346
- lines.push(' approval posture: read-only by default; exact command and confirmation required for side effects.');
1347
- lines.push('');
1348
- }
1349
-
1350
- return lines.join('\n').trimEnd();
1351
- }
1352
-
1353
- export function filterDaemonCapabilityInventoryGroups(
1354
- groups: readonly DaemonCapabilityInventoryGroup[],
1355
- query: string | undefined,
1356
- ): readonly DaemonCapabilityInventoryGroup[] {
1357
- const normalized = query?.trim().toLowerCase();
1358
- if (!normalized) return groups;
1359
- return groups.flatMap((group): DaemonCapabilityInventoryGroup[] => {
1360
- const categoryMatches = group.category.toLowerCase().includes(normalized);
1361
- const methods = categoryMatches
1362
- ? group.methods
1363
- : group.methods.filter((method) => {
1364
- return method.id.toLowerCase().includes(normalized)
1365
- || method.title?.toLowerCase().includes(normalized) === true
1366
- || method.access.toLowerCase().includes(normalized)
1367
- || method.httpMethod.toLowerCase().includes(normalized)
1368
- || method.path?.toLowerCase().includes(normalized) === true;
1369
- });
1370
- if (methods.length === 0) return [];
1371
- return [{
1372
- category: group.category,
1373
- methodCount: methods.length,
1374
- readOnlyMethodCount: methods.filter((method) => method.readOnly).length,
1375
- mutatingMethodCount: methods.filter((method) => method.mutating).length,
1376
- authenticatedMethodCount: methods.filter((method) => method.access === 'authenticated').length,
1377
- dangerousMethodCount: methods.filter((method) => method.dangerous).length,
1378
- methods,
1379
- }];
1380
- });
1381
- }
1382
-
1383
- function renderInventoryMethod(method: DaemonCapabilityInventoryMethod): string {
1384
- const risk = method.dangerous
1385
- ? ' dangerous'
1386
- : method.mutating
1387
- ? ' mutating'
1388
- : ' read-only';
1389
- const route = method.path ? ` ${method.httpMethod} ${method.path}` : ` ${method.httpMethod}`;
1390
- return ` ${method.id} [${method.access};${risk}]${route}`;
1391
- }
1392
-
1393
- export function renderDaemonCapabilityInventory(
1394
- report: DaemonCapabilityInventoryReport,
1395
- groups: readonly DaemonCapabilityInventoryGroup[] = report.groups,
1396
- ): string {
1397
- const lines: string[] = [
1398
- 'GoodVibes daemon method inventory',
1399
- ` daemon: ${report.baseUrl}`,
1400
- ` SDK: Agent expects ${report.expectedSdkVersion}; daemon reports ${report.daemonVersion}`,
1401
- ` compatibility: ${report.daemonCompatible ? 'matched' : 'mismatch'}`,
1402
- ` method catalog: ${report.methodCount} methods from ${report.methodCatalogRoute}`,
1403
- ` Agent Knowledge: ${report.agentKnowledgeRouteReady ? 'ready' : 'missing'} ${report.agentKnowledgeRoute}`,
1404
- ' isolation: default Knowledge/Wiki fallback no; HomeGraph fallback no',
1405
- ` totals: ${report.readOnlyMethodCount} read-only; ${report.mutatingMethodCount} mutating; ${report.dangerousMethodCount} dangerous; ${report.authenticatedMethodCount} authenticated`,
1406
- ` access: ${report.accessCounts.map((entry) => `${entry.access} ${entry.count}`).join('; ') || 'none'}`,
1407
- '',
1408
- ];
1409
-
1410
- if (groups.length === 0) {
1411
- lines.push('No daemon methods matched this query.');
1412
- return lines.join('\n');
1413
- }
1414
-
1415
- for (const group of groups) {
1416
- lines.push(`${group.category} (${group.methodCount})`);
1417
- lines.push(` ${group.readOnlyMethodCount} read-only; ${group.mutatingMethodCount} mutating; ${group.dangerousMethodCount} dangerous; ${group.authenticatedMethodCount} authenticated`);
1418
- const visibleMethods = group.methods.slice(0, 12);
1419
- for (const method of visibleMethods) lines.push(renderInventoryMethod(method));
1420
- if (group.methods.length > visibleMethods.length) {
1421
- lines.push(` ... ${group.methods.length - visibleMethods.length} more; use --json or a narrower query for the full list.`);
1422
- }
1423
- lines.push('');
1424
- }
1425
-
1426
- return lines.join('\n').trimEnd();
1427
- }
1428
-
1429
- function renderUxMethod(method: DaemonCapabilityUxMethod): string {
1430
- const route = method.path ? ` ${method.httpMethod} ${method.path}` : ` ${method.httpMethod}`;
1431
- return ` ${method.id} [${method.uxCoverage}]${route} -> ${method.surface}`;
1432
- }
1433
-
1434
- export function renderDaemonCapabilityUxCoverage(
1435
- report: DaemonCapabilityUxCoverageReport,
1436
- groups: readonly DaemonCapabilityUxGroup[] = report.groups,
1437
- ): string {
1438
- const lines: string[] = [
1439
- 'GoodVibes daemon-to-Agent UX coverage',
1440
- ` daemon: ${report.baseUrl}`,
1441
- ` SDK: Agent expects ${report.expectedSdkVersion}; daemon reports ${report.daemonVersion}`,
1442
- ` compatibility: ${report.daemonCompatible ? 'matched' : 'mismatch'}`,
1443
- ` method catalog: ${report.methodCount} methods from ${report.methodCatalogRoute}`,
1444
- ` Agent Knowledge: ${report.agentKnowledgeRouteReady ? 'ready' : 'missing'} ${report.agentKnowledgeRoute}`,
1445
- ' isolation: default Knowledge/Wiki fallback no; HomeGraph fallback no',
1446
- ` totals: ${report.usableMethodCount} usable; ${report.readOnlyMethodCount} read-only observable; ${report.explicitConfirmationMethodCount} explicit-confirmation; ${report.blockedMethodCount} blocked; ${report.notSurfacedMethodCount} not surfaced`,
1447
- '',
1448
- ];
1449
-
1450
- if (groups.length === 0) {
1451
- lines.push('No daemon UX coverage rows matched this query.');
1452
- return lines.join('\n');
1453
- }
1454
-
1455
- for (const group of groups) {
1456
- lines.push(`${group.category} (${group.methodCount})`);
1457
- lines.push(` ${group.usableMethodCount} usable; ${group.readOnlyMethodCount} read-only; ${group.explicitConfirmationMethodCount} explicit-confirmation; ${group.blockedMethodCount} blocked; ${group.notSurfacedMethodCount} not surfaced`);
1458
- const priorityMethods = group.methods
1459
- .filter((method) => method.uxCoverage === 'not_surfaced' || method.uxCoverage === 'blocked')
1460
- .slice(0, 12);
1461
- const visibleMethods = priorityMethods.length > 0 ? priorityMethods : group.methods.slice(0, 12);
1462
- for (const method of visibleMethods) {
1463
- lines.push(renderUxMethod(method));
1464
- if (method.uxCoverage === 'not_surfaced' || method.uxCoverage === 'blocked') {
1465
- lines.push(` next: ${method.next}`);
1466
- }
1467
- }
1468
- if (group.methods.length > visibleMethods.length) {
1469
- lines.push(` ... ${group.methods.length - visibleMethods.length} more; use --json or a narrower query for the full list.`);
1470
- }
1471
- lines.push('');
1472
- }
1473
-
1474
- return lines.join('\n').trimEnd();
1475
- }
1476
-
1477
- export function renderDaemonCapabilityAudit(
1478
- audit: DaemonCapabilityAuditSuccess,
1479
- areas: readonly DaemonCapabilityAuditArea[] = audit.areas,
1480
- ): string {
1481
- const lines: string[] = [
1482
- 'GoodVibes daemon capability audit',
1483
- ` daemon: ${audit.baseUrl}`,
1484
- ` SDK: Agent expects ${audit.expectedSdkVersion}; daemon reports ${audit.daemonVersion}`,
1485
- ` compatibility: ${audit.daemonCompatible ? 'matched' : 'mismatch'}`,
1486
- ` method catalog: ${audit.methodCount} methods from ${audit.methodCatalogRoute}`,
1487
- ` Agent Knowledge: ${audit.agentKnowledgeRouteReady ? 'ready' : 'missing'} ${audit.agentKnowledgeRoute}`,
1488
- ' isolation: default Knowledge/Wiki fallback no; HomeGraph fallback no',
1489
- '',
1490
- ];
1491
-
1492
- for (const warning of audit.warnings) lines.push(` warning: ${warning}`);
1493
- if (audit.warnings.length > 0) lines.push('');
1494
-
1495
- for (const area of areas) {
1496
- const requiredTotal = area.presentRequiredMethodIds.length + area.missingRequiredMethodIds.length;
1497
- const optionalTotal = area.presentOptionalMethodIds.length + area.missingOptionalMethodIds.length;
1498
- lines.push(`${area.title} [${area.coverage}]`);
1499
- lines.push(` baseline: ${area.competitorBaseline}`);
1500
- lines.push(` Agent use: ${area.agentUse}`);
1501
- lines.push(` required methods: ${area.presentRequiredMethodIds.length}/${requiredTotal}`);
1502
- if (area.missingRequiredMethodIds.length > 0) lines.push(` missing required: ${area.missingRequiredMethodIds.join(', ')}`);
1503
- if (optionalTotal > 0) {
1504
- lines.push(` optional methods: ${area.presentOptionalMethodIds.length}/${optionalTotal}`);
1505
- if (area.missingOptionalMethodIds.length > 0) lines.push(` missing optional: ${area.missingOptionalMethodIds.join(', ')}`);
1506
- }
1507
- lines.push(` route risk: ${area.routeRisk.readOnlyMethodCount} read-only; ${area.routeRisk.mutatingMethodCount} mutating; ${area.routeRisk.dangerousMethodIds.length} dangerous; ${area.routeRisk.authenticatedMethodCount} authenticated`);
1508
- if (area.routeRisk.dangerousMethodIds.length > 0) {
1509
- lines.push(` dangerous methods: ${area.routeRisk.dangerousMethodIds.join(', ')}`);
1510
- }
1511
- for (const route of area.agentRoutes) lines.push(` route: ${route.route} [${route.coverage}]`);
1512
- lines.push(` next: ${area.next.join(' | ')}`);
1513
- lines.push('');
1514
- }
1515
-
1516
- return lines.join('\n').trimEnd();
1517
- }
1518
-
1519
- export function renderDaemonCapabilityFailure(failure: DaemonCapabilityAuditFailure): string {
1520
- const details = [
1521
- `GoodVibes daemon capability audit failed [${failure.kind}]`,
1522
- ` daemon: ${failure.baseUrl}`,
1523
- ` route: ${failure.route}`,
1524
- ` error: ${failure.error}`,
1525
- ];
1526
- if (failure.daemonVersion || failure.expectedSdkVersion) {
1527
- details.push(` SDK: Agent expects ${failure.expectedSdkVersion ?? SDK_VERSION}; daemon reports ${failure.daemonVersion ?? 'unknown'}`);
1528
- }
1529
- if (failure.kind === 'auth_required') details.push(' next: authenticate the Agent against the existing GoodVibes daemon; no token value was printed.');
1530
- if (failure.kind === 'daemon_unavailable') details.push(' next: start or reconnect the external GoodVibes daemon; Agent will not start it.');
1531
- if (failure.kind === 'version_mismatch') details.push(' next: update/restart the externally owned daemon to match the Agent SDK pin.');
1532
- if (failure.kind === 'daemon_route_unavailable') details.push(' next: verify the external daemon exposes the published SDK/operator routes.');
1533
- return details.join('\n');
1534
- }