@pellux/goodvibes-agent 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +7 -5
  3. package/dist/package/main.js +7346 -2349
  4. package/docs/README.md +2 -2
  5. package/docs/channels-remote-and-api.md +6 -2
  6. package/docs/connected-host.md +26 -5
  7. package/docs/getting-started.md +29 -9
  8. package/docs/knowledge-artifacts-and-multimodal.md +16 -4
  9. package/docs/project-planning.md +2 -2
  10. package/docs/providers-and-routing.md +3 -2
  11. package/docs/release-and-publishing.md +15 -11
  12. package/docs/tools-and-commands.md +20 -8
  13. package/package.json +8 -3
  14. package/release/live-verification/live-verification.json +148 -0
  15. package/release/live-verification/live-verification.md +187 -0
  16. package/release/performance-snapshot.json +57 -0
  17. package/release/release-notes.md +28 -0
  18. package/release/release-readiness.json +581 -0
  19. package/src/cli/agent-knowledge-command.ts +5 -5
  20. package/src/cli/agent-knowledge-format.ts +11 -0
  21. package/src/cli/agent-knowledge-runtime.ts +92 -13
  22. package/src/cli/bundle-command.ts +5 -4
  23. package/src/cli/entrypoint.ts +5 -2
  24. package/src/cli/external-runtime.ts +2 -15
  25. package/src/cli/management.ts +4 -3
  26. package/src/input/commands/guidance-runtime.ts +1 -1
  27. package/src/input/commands/knowledge.ts +2 -2
  28. package/src/runtime/bootstrap.ts +2 -2
  29. package/src/runtime/connected-host-auth.ts +16 -0
  30. package/src/tools/agent-channel-send-tool.ts +5 -7
  31. package/src/tools/agent-harness-channel-metadata.ts +177 -0
  32. package/src/tools/agent-harness-connected-host-status.ts +1 -1
  33. package/src/tools/agent-harness-delegation-posture.ts +216 -0
  34. package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
  35. package/src/tools/agent-harness-mcp-metadata.ts +246 -0
  36. package/src/tools/agent-harness-media-posture.ts +282 -0
  37. package/src/tools/agent-harness-metadata.ts +21 -4
  38. package/src/tools/agent-harness-model-routing.ts +501 -0
  39. package/src/tools/agent-harness-notification-metadata.ts +217 -0
  40. package/src/tools/agent-harness-operator-methods.ts +285 -0
  41. package/src/tools/agent-harness-pairing-posture.ts +265 -0
  42. package/src/tools/agent-harness-provider-account-metadata.ts +203 -0
  43. package/src/tools/agent-harness-release-evidence.ts +364 -0
  44. package/src/tools/agent-harness-release-readiness.ts +298 -0
  45. package/src/tools/agent-harness-security-posture.ts +646 -0
  46. package/src/tools/agent-harness-service-posture.ts +201 -0
  47. package/src/tools/agent-harness-session-metadata.ts +282 -0
  48. package/src/tools/agent-harness-setup-posture.ts +295 -0
  49. package/src/tools/agent-harness-tool-schema.ts +103 -27
  50. package/src/tools/agent-harness-tool.ts +209 -236
  51. package/src/tools/agent-harness-ui-surface-metadata.ts +1 -1
  52. package/src/tools/agent-harness-workspace-actions.ts +232 -0
  53. package/src/tools/agent-knowledge-ingest-tool.ts +6 -8
  54. package/src/tools/agent-knowledge-tool.ts +122 -23
  55. package/src/tools/agent-local-registry-tool.ts +4 -5
  56. package/src/tools/agent-media-generate-tool.ts +4 -6
  57. package/src/tools/agent-notify-tool.ts +5 -6
  58. package/src/tools/agent-operator-action-tool.ts +6 -8
  59. package/src/tools/agent-operator-briefing-tool.ts +3 -4
  60. package/src/tools/agent-reminder-schedule-tool.ts +6 -7
  61. package/src/tools/agent-tool-policy-guard.ts +8 -17
  62. package/src/tools/agent-work-plan-tool.ts +3 -4
  63. package/src/version.ts +2 -2
@@ -135,7 +135,7 @@ export async function connectedHostStatusSummary(
135
135
  ],
136
136
  findings: connectedHostFindings(runtime, tokenUsable),
137
137
  modelAccess: {
138
- diagnostics: 'Use mode:"connected_host_status" for live read-only host readiness. Use mode:"connected_host" for capability and boundary inventory.',
138
+ diagnostics: 'Use mode:"connected_host_status" for live read-only host readiness, mode:"service_posture" for endpoint posture, mode:"service_endpoint" for one endpoint, and mode:"connected_host" for capability and boundary inventory.',
139
139
  cliMirrors: ['goodvibes-agent status --json', 'goodvibes-agent doctor', 'goodvibes-agent compat'],
140
140
  tuiMirrors: ['Agent Workspace -> Home -> Host compatibility', 'Agent Workspace -> Home -> Doctor diagnostics', 'Agent Workspace -> Home -> Review health'],
141
141
  },
@@ -0,0 +1,216 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+
3
+ export interface AgentHarnessDelegationArgs {
4
+ readonly delegationRouteId?: unknown;
5
+ readonly target?: unknown;
6
+ readonly query?: unknown;
7
+ readonly includeParameters?: unknown;
8
+ readonly limit?: unknown;
9
+ }
10
+
11
+ type DelegationResolution =
12
+ | { readonly status: 'found'; readonly route: Record<string, unknown> }
13
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
14
+ | { readonly status: 'missing_lookup'; readonly usage: string };
15
+
16
+ type DelegationLookupSource = 'delegationRouteId' | 'target' | 'query';
17
+
18
+ interface DelegationRoute {
19
+ readonly id: string;
20
+ readonly label: string;
21
+ readonly detail: string;
22
+ readonly effect: 'read-only' | 'main-conversation' | 'delegated-work' | 'blocked';
23
+ readonly command?: string;
24
+ readonly commandTemplate?: string;
25
+ readonly workspaceActionId?: string;
26
+ readonly confirmationRequired?: boolean;
27
+ readonly reviewPolicy?: 'explicit-only' | 'not-applicable';
28
+ }
29
+
30
+ function readString(value: unknown): string {
31
+ return typeof value === 'string' ? value.trim() : '';
32
+ }
33
+
34
+ function readLimit(value: unknown, fallback: number): number {
35
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
36
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
37
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
38
+ }
39
+
40
+ function lookupFromArgs(args: AgentHarnessDelegationArgs): { readonly source: DelegationLookupSource; readonly input: string } | null {
41
+ const delegationRouteId = readString(args.delegationRouteId);
42
+ if (delegationRouteId) return { source: 'delegationRouteId', input: delegationRouteId };
43
+ const target = readString(args.target);
44
+ if (target) return { source: 'target', input: target };
45
+ const query = readString(args.query);
46
+ return query ? { source: 'query', input: query } : null;
47
+ }
48
+
49
+ function routes(): readonly DelegationRoute[] {
50
+ return [
51
+ {
52
+ id: 'delegate-build-task',
53
+ label: 'Delegate build/fix/review task',
54
+ detail: 'Send one explicit build, fix, implementation, patch, or review task to GoodVibes TUI/shared-session routes with the full original ask.',
55
+ effect: 'delegated-work',
56
+ commandTemplate: '/delegate <full original user ask>',
57
+ workspaceActionId: 'delegate-task',
58
+ confirmationRequired: true,
59
+ reviewPolicy: 'explicit-only',
60
+ },
61
+ {
62
+ id: 'delegate-build-task-with-review',
63
+ label: 'Delegate with requested review',
64
+ detail: 'Request delegated review only when the user explicitly asks for review or explicitly requests review as part of a build/fix/review handoff.',
65
+ effect: 'delegated-work',
66
+ commandTemplate: '/delegate --review <full original user ask>',
67
+ workspaceActionId: 'delegate-task',
68
+ confirmationRequired: true,
69
+ reviewPolicy: 'explicit-only',
70
+ },
71
+ {
72
+ id: 'delegation-status',
73
+ label: 'Delegation status',
74
+ detail: 'Inspect build-delegation receipts and shared-session status without starting coding work.',
75
+ effect: 'read-only',
76
+ command: '/delegate status',
77
+ workspaceActionId: 'delegation-status',
78
+ reviewPolicy: 'not-applicable',
79
+ },
80
+ {
81
+ id: 'ordinary-agent-work',
82
+ label: 'Ordinary Agent work',
83
+ detail: 'Planning, research, operations, memory, configuration, approvals, automation observability, media generation, and ordinary assistant work stay in the main Agent conversation or first-class Agent tools.',
84
+ effect: 'main-conversation',
85
+ reviewPolicy: 'not-applicable',
86
+ },
87
+ {
88
+ id: 'local-coding-blocked',
89
+ label: 'Local coding execution blocked in Agent',
90
+ detail: 'Agent does not own file edits, git/worktree workflows, execution isolation UX, coding panels, or local delegated review chains.',
91
+ effect: 'blocked',
92
+ reviewPolicy: 'not-applicable',
93
+ },
94
+ ];
95
+ }
96
+
97
+ function routeSearchText(route: DelegationRoute): string {
98
+ return [
99
+ route.id,
100
+ route.label,
101
+ route.detail,
102
+ route.effect,
103
+ route.command ?? '',
104
+ route.commandTemplate ?? '',
105
+ route.workspaceActionId ?? '',
106
+ route.reviewPolicy ?? '',
107
+ ].join('\n').toLowerCase();
108
+ }
109
+
110
+ function describeCandidate(route: DelegationRoute): Record<string, unknown> {
111
+ return {
112
+ delegationRouteId: route.id,
113
+ label: route.label,
114
+ effect: route.effect,
115
+ confirmationRequired: route.confirmationRequired === true,
116
+ };
117
+ }
118
+
119
+ function describeRoute(
120
+ context: CommandContext,
121
+ route: DelegationRoute,
122
+ options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
123
+ ): Record<string, unknown> {
124
+ return {
125
+ delegationRouteId: route.id,
126
+ label: route.label,
127
+ detail: route.detail,
128
+ effect: route.effect,
129
+ ...(route.command ? { command: route.command } : {}),
130
+ ...(route.commandTemplate ? { commandTemplate: route.commandTemplate } : {}),
131
+ ...(route.workspaceActionId ? { workspaceActionId: route.workspaceActionId } : {}),
132
+ confirmationRequired: route.confirmationRequired === true,
133
+ reviewPolicy: route.reviewPolicy ?? 'not-applicable',
134
+ runtime: {
135
+ sessionId: context.session.runtime.sessionId,
136
+ operatorClientAttached: Boolean(context.clients?.operator),
137
+ },
138
+ ...(options.lookup ? { lookup: options.lookup } : {}),
139
+ policy: {
140
+ effect: route.effect,
141
+ values: 'Delegation posture returns policy, route, confirmation, and runtime availability metadata only; it does not submit delegated work.',
142
+ mutation: 'Delegated work submission stays a visible confirmed workspace or slash-command flow and must preserve the full original user ask.',
143
+ },
144
+ ...(options.includeParameters ? {
145
+ modelAccess: {
146
+ inspectDelegation: 'agent_harness mode:"delegation_posture"',
147
+ inspectRoute: 'agent_harness mode:"delegation_route"',
148
+ workspaceAction: route.workspaceActionId ? `agent_harness mode:"workspace_action" actionId:"${route.workspaceActionId}"` : null,
149
+ runWorkspaceAction: route.workspaceActionId ? `agent_harness mode:"run_workspace_action" actionId:"${route.workspaceActionId}" confirm:true explicitUserRequest:"..."` : null,
150
+ runCommand: route.command ? `agent_harness mode:"run_command" command:"${route.command}" confirm:true explicitUserRequest:"..."` : null,
151
+ runCommandTemplate: route.commandTemplate ? `agent_harness mode:"run_command" command:"${route.commandTemplate}" confirm:true explicitUserRequest:"..."` : null,
152
+ },
153
+ } : {}),
154
+ };
155
+ }
156
+
157
+ export function delegationPostureCatalogStatus(context: CommandContext): Record<string, unknown> {
158
+ return {
159
+ modes: ['delegation_posture', 'delegation_route'],
160
+ routes: routes().length,
161
+ operatorClientAttached: Boolean(context.clients?.operator),
162
+ readOnly: true,
163
+ };
164
+ }
165
+
166
+ export function delegationPostureSummary(context: CommandContext, args: AgentHarnessDelegationArgs): Record<string, unknown> {
167
+ const query = readString(args.query).toLowerCase();
168
+ const includeParameters = args.includeParameters === true;
169
+ const filtered = routes()
170
+ .filter((route) => !query || routeSearchText(route).includes(query))
171
+ .slice(0, readLimit(args.limit, 100));
172
+ return {
173
+ status: 'available',
174
+ summary: {
175
+ routes: routes().length,
176
+ operatorClientAttached: Boolean(context.clients?.operator),
177
+ sessionId: context.session.runtime.sessionId,
178
+ delegatedReviewPolicy: 'explicit-build-delegation-only',
179
+ normalChatPolicy: 'ordinary assistant work stays in the main Agent conversation',
180
+ codingOwnership: 'GoodVibes TUI owns file edits, git/worktree workflows, execution isolation UX, coding panels, and delegated review coordination',
181
+ },
182
+ routes: filtered.map((route) => describeRoute(context, route, { includeParameters })),
183
+ returned: filtered.length,
184
+ total: routes().length,
185
+ policy: 'Read-only delegation posture. Delegation submission requires an explicit build/fix/review/implementation user request, visible confirmation, and preservation of the full original ask.',
186
+ };
187
+ }
188
+
189
+ export function describeHarnessDelegationRoute(context: CommandContext, args: AgentHarnessDelegationArgs): DelegationResolution {
190
+ const lookup = lookupFromArgs(args);
191
+ if (!lookup) {
192
+ return {
193
+ status: 'missing_lookup',
194
+ usage: 'delegation_route requires delegationRouteId, target, or query. Use mode:"delegation_posture" to inspect delegation route ids.',
195
+ };
196
+ }
197
+ const all = routes();
198
+ const normalized = lookup.input.toLowerCase();
199
+ const exact = all.find((route) => route.id === lookup.input);
200
+ if (exact) return { status: 'found', route: describeRoute(context, exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
201
+ const insensitive = all.find((route) => route.id.toLowerCase() === normalized);
202
+ if (insensitive) return { status: 'found', route: describeRoute(context, insensitive, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
203
+ const searched = all.filter((route) => routeSearchText(route).includes(normalized));
204
+ if (searched.length === 1) return { status: 'found', route: describeRoute(context, searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
205
+ if (searched.length > 1) {
206
+ return {
207
+ status: 'ambiguous',
208
+ input: lookup.input,
209
+ candidates: searched.slice(0, 8).map(describeCandidate),
210
+ };
211
+ }
212
+ return {
213
+ status: 'missing_lookup',
214
+ usage: `Unknown delegation route ${lookup.input}. Use mode:"delegation_posture" to inspect delegation route ids.`,
215
+ };
216
+ }
@@ -53,6 +53,7 @@ interface KeybindingOperationRoute {
53
53
  }
54
54
 
55
55
  type KeybindingsOverrideFile = Record<string, unknown>;
56
+ type KeybindingEntry = { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] };
56
57
 
57
58
  interface KeybindingLookup {
58
59
  readonly source: 'actionId' | 'target' | 'key' | 'query';
@@ -93,6 +94,19 @@ function requireKeybindingsManager(context: CommandContext): KeybindingsManager
93
94
  return manager;
94
95
  }
95
96
 
97
+ function readKeybindingsManager(context: CommandContext): KeybindingsManager | null {
98
+ return context.workspace.keybindingsManager ?? null;
99
+ }
100
+
101
+ function defaultKeybindingEntries(): KeybindingEntry[] {
102
+ return (Object.entries(DEFAULT_KEYBINDINGS) as [KeyAction, KeyCombo[]][])
103
+ .map(([action, combos]) => ({
104
+ action,
105
+ description: ACTION_DESCRIPTIONS[action],
106
+ combos: combos.map((combo) => ({ ...combo })),
107
+ }));
108
+ }
109
+
96
110
  function isKeyAction(action: string): action is KeyAction {
97
111
  return Object.hasOwn(ACTION_DESCRIPTIONS, action);
98
112
  }
@@ -162,13 +176,23 @@ function combosEqual(left: readonly KeyCombo[], right: readonly KeyCombo[]): boo
162
176
  return left.every((combo, index) => comboFingerprint(combo) === comboFingerprint(right[index]));
163
177
  }
164
178
 
165
- function describeCombo(manager: KeybindingsManager, combo: KeyCombo): Record<string, unknown> {
179
+ function formatCombo(manager: KeybindingsManager | null, combo: KeyCombo): string {
180
+ if (manager) return manager.formatCombo(combo);
181
+ const parts: string[] = [];
182
+ if (combo.ctrl) parts.push('Ctrl');
183
+ if (combo.alt) parts.push('Alt');
184
+ if (combo.shift) parts.push('Shift');
185
+ parts.push(combo.key.length === 1 ? combo.key.toUpperCase() : combo.key);
186
+ return parts.join('+');
187
+ }
188
+
189
+ function describeCombo(manager: KeybindingsManager | null, combo: KeyCombo): Record<string, unknown> {
166
190
  return {
167
191
  key: combo.key,
168
192
  ctrl: combo.ctrl === true,
169
193
  shift: combo.shift === true,
170
194
  alt: combo.alt === true,
171
- label: manager.formatCombo(combo),
195
+ label: formatCombo(manager, combo),
172
196
  };
173
197
  }
174
198
 
@@ -184,33 +208,33 @@ function keybindingLookupFromArgs(args: HarnessKeybindingArgs): { readonly sourc
184
208
  return null;
185
209
  }
186
210
 
187
- function bindingSearchText(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }): string {
211
+ function bindingSearchText(manager: KeybindingsManager | null, entry: KeybindingEntry): string {
188
212
  return [
189
213
  entry.action,
190
214
  entry.description,
191
215
  ...entry.combos.map((combo) => comboFingerprint(combo)),
192
- ...entry.combos.map((combo) => manager.formatCombo(combo)),
216
+ ...entry.combos.map((combo) => formatCombo(manager, combo)),
193
217
  ].join('\n').toLowerCase();
194
218
  }
195
219
 
196
- function bindingCandidate(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }): Record<string, unknown> {
220
+ function bindingCandidate(manager: KeybindingsManager | null, entry: KeybindingEntry): Record<string, unknown> {
197
221
  return {
198
222
  action: entry.action,
199
223
  description: entry.description,
200
- labels: entry.combos.map((combo) => manager.formatCombo(combo)),
224
+ labels: entry.combos.map((combo) => formatCombo(manager, combo)),
201
225
  };
202
226
  }
203
227
 
204
- function bindingMatches(manager: KeybindingsManager, entry: { readonly action: KeyAction; readonly description: string; readonly combos: KeyCombo[] }, query: string): boolean {
228
+ function bindingMatches(manager: KeybindingsManager | null, entry: KeybindingEntry, query: string): boolean {
205
229
  if (!query) return true;
206
230
  return bindingSearchText(manager, entry).includes(query.toLowerCase());
207
231
  }
208
232
 
209
233
  function resolveHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): KeybindingResolution | null {
210
- const manager = requireKeybindingsManager(context);
234
+ const manager = readKeybindingsManager(context);
211
235
  const lookup = keybindingLookupFromArgs(args);
212
236
  if (!lookup) return null;
213
- const entries = manager.getAll();
237
+ const entries = manager?.getAll() ?? defaultKeybindingEntries();
214
238
  if (isKeyAction(lookup.input)) return { status: 'found', action: lookup.input, lookup: { ...lookup, resolvedBy: 'action' } };
215
239
  const inputLower = lookup.input.toLowerCase();
216
240
  const ciActions = entries.filter((entry) => entry.action.toLowerCase() === inputLower);
@@ -222,7 +246,7 @@ function resolveHarnessKeybinding(context: CommandContext, args: HarnessKeybindi
222
246
  return null;
223
247
  }
224
248
 
225
- function describeBinding(manager: KeybindingsManager, action: KeyAction, combos: KeyCombo[], lookup?: KeybindingLookup): Record<string, unknown> {
249
+ function describeBinding(manager: KeybindingsManager | null, action: KeyAction, combos: KeyCombo[], lookup?: KeybindingLookup): Record<string, unknown> {
226
250
  const defaults = DEFAULT_KEYBINDINGS[action];
227
251
  const customized = !combosEqual(combos, defaults);
228
252
  return {
@@ -230,10 +254,10 @@ function describeBinding(manager: KeybindingsManager, action: KeyAction, combos:
230
254
  description: ACTION_DESCRIPTIONS[action],
231
255
  ...(lookup ? { lookup } : {}),
232
256
  bindings: combos.map((combo) => describeCombo(manager, combo)),
233
- labels: combos.map((combo) => manager.formatCombo(combo)),
257
+ labels: combos.map((combo) => formatCombo(manager, combo)),
234
258
  defaultBindings: defaults.map((combo) => describeCombo(manager, combo)),
235
259
  customized,
236
- source: customized ? 'custom' : 'default',
260
+ source: manager ? (customized ? 'custom' : 'default') : 'default-fallback',
237
261
  modelOperation: keybindingOperationRoute(action),
238
262
  };
239
263
  }
@@ -377,7 +401,7 @@ function writeOverrideFile(configPath: string, overrides: KeybindingsOverrideFil
377
401
  }
378
402
 
379
403
  export function totalHarnessKeybindings(context: CommandContext): number {
380
- return context.workspace.keybindingsManager?.getAll().length ?? 0;
404
+ return context.workspace.keybindingsManager?.getAll().length ?? Object.keys(DEFAULT_KEYBINDINGS).length;
381
405
  }
382
406
 
383
407
  export function totalHarnessShortcuts(context: CommandContext): number {
@@ -391,41 +415,52 @@ export function listHarnessShortcuts(context: CommandContext, args: HarnessKeybi
391
415
  .filter((shortcut) => !query || `${shortcut.key}\n${shortcut.description}`.toLowerCase().includes(query))
392
416
  .slice(0, readLimit(args.limit, 200))
393
417
  .map((shortcut) => ({ ...shortcut, source: 'fixed', userEditable: false }));
418
+ const degraded = keybindings.status === 'degraded';
394
419
  return {
420
+ status: degraded ? 'degraded' : 'available',
395
421
  configPath: keybindings.configPath,
396
422
  fixedShortcuts: fixed,
397
423
  configurableKeybindings: keybindings.keybindings,
398
424
  returned: fixed.length + Number(keybindings.returned ?? 0),
399
425
  total: totalHarnessShortcuts(context),
400
- policy: 'Fixed shortcuts are runtime/editor controls. Configurable keybindings can be inspected, supported shell-safe actions can be run with run_keybinding, and bindings can be changed with set_keybinding/reset_keybinding.',
426
+ policy: degraded
427
+ ? 'Fixed shortcuts and default keybindings are available for discovery. Live keybinding execution and mutation require the runtime keybinding manager.'
428
+ : 'Fixed shortcuts are runtime/editor controls. Configurable keybindings can be inspected, supported shell-safe actions can be run with run_keybinding, and bindings can be changed with set_keybinding/reset_keybinding.',
401
429
  };
402
430
  }
403
431
 
404
432
  export function listHarnessKeybindings(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> {
405
- const manager = requireKeybindingsManager(context);
433
+ const manager = readKeybindingsManager(context);
406
434
  const query = readString(args.query);
407
- const entries = manager.getAll()
435
+ const allEntries = manager?.getAll() ?? defaultKeybindingEntries();
436
+ const entries = allEntries
408
437
  .filter((entry) => bindingMatches(manager, entry, query))
409
438
  .slice(0, readLimit(args.limit, 200))
410
439
  .map((entry) => describeBinding(manager, entry.action, entry.combos));
411
440
  return {
412
- configPath: manager.getConfigPath(),
441
+ status: manager ? 'available' : 'degraded',
442
+ configPath: manager?.getConfigPath() ?? null,
413
443
  keybindings: entries,
414
444
  returned: entries.length,
415
- total: manager.getAll().length,
416
- policy: 'Reads the live resolved keybindings, including modelOperation route metadata. run_keybinding executes only supported shell-safe actions. set_keybinding/reset_keybinding write the same keybindings.json file the user can edit and reload the runtime manager.',
445
+ total: allEntries.length,
446
+ policy: manager
447
+ ? 'Reads the live resolved keybindings, including modelOperation route metadata. run_keybinding executes only supported shell-safe actions. set_keybinding/reset_keybinding write the same keybindings.json file the user can edit and reload the runtime manager.'
448
+ : 'Live keybinding manager is unavailable; default keybindings are shown for discovery only. run_keybinding, set_keybinding, and reset_keybinding require a live manager.',
417
449
  };
418
450
  }
419
451
 
420
452
  export function describeHarnessKeybinding(context: CommandContext, args: HarnessKeybindingArgs): Record<string, unknown> | null {
421
- const manager = requireKeybindingsManager(context);
453
+ const manager = readKeybindingsManager(context);
422
454
  const resolved = resolveHarnessKeybinding(context, args);
423
455
  if (resolved?.status === 'ambiguous') return { status: 'ambiguous', input: resolved.input, candidates: resolved.candidates };
424
456
  if (!resolved) return null;
425
- const entry = manager.getAll().find((candidate) => candidate.action === resolved.action);
457
+ const entries = manager?.getAll() ?? defaultKeybindingEntries();
458
+ const entry = entries.find((candidate) => candidate.action === resolved.action);
426
459
  return entry ? {
427
- configPath: manager.getConfigPath(),
460
+ status: manager ? 'available' : 'degraded',
461
+ configPath: manager?.getConfigPath() ?? null,
428
462
  ...describeBinding(manager, entry.action, entry.combos, resolved.lookup),
463
+ ...(manager ? {} : { note: 'Default keybinding descriptor only; live run/set/reset operations require the runtime keybinding manager.' }),
429
464
  } : null;
430
465
  }
431
466
 
@@ -0,0 +1,246 @@
1
+ import type { CommandContext } from '../input/command-registry.ts';
2
+
3
+ export interface AgentHarnessMcpArgs {
4
+ readonly mcpServerId?: unknown;
5
+ readonly target?: unknown;
6
+ readonly query?: unknown;
7
+ readonly includeParameters?: unknown;
8
+ readonly limit?: unknown;
9
+ }
10
+
11
+ type McpServerLookupSource = 'mcpServerId' | 'target' | 'query';
12
+
13
+ type McpServerResolution =
14
+ | { readonly status: 'found'; readonly server: Record<string, unknown> }
15
+ | { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
16
+ | { readonly status: 'missing_lookup'; readonly usage: string };
17
+
18
+ type McpServerRecord = ReturnType<NonNullable<NonNullable<CommandContext['clients']>['mcpApi']>['listServerSecurity']>[number];
19
+
20
+ interface McpToolRecord {
21
+ readonly serverName: string;
22
+ readonly toolName: string;
23
+ readonly description?: string;
24
+ }
25
+
26
+ function readString(value: unknown): string {
27
+ return typeof value === 'string' ? value.trim() : '';
28
+ }
29
+
30
+ function readLimit(value: unknown, fallback: number): number {
31
+ const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
32
+ if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
33
+ return Math.max(1, Math.min(500, Math.trunc(parsed)));
34
+ }
35
+
36
+ function resolveMcpApi(context: CommandContext): {
37
+ readonly listServerSecurity: () => readonly McpServerRecord[];
38
+ readonly listAllTools?: (() => Promise<readonly McpToolRecord[]>) | undefined;
39
+ } | null {
40
+ const api = context.clients?.mcpApi ?? context.extensions?.mcpRegistry;
41
+ if (!api || typeof api.listServerSecurity !== 'function') return null;
42
+ return {
43
+ listServerSecurity: () => api.listServerSecurity(),
44
+ ...(typeof (api as { readonly listAllTools?: unknown }).listAllTools === 'function'
45
+ ? { listAllTools: () => (api as { listAllTools: () => Promise<readonly McpToolRecord[]> }).listAllTools() }
46
+ : {}),
47
+ };
48
+ }
49
+
50
+ function lookupFromArgs(args: AgentHarnessMcpArgs): { readonly source: McpServerLookupSource; readonly input: string } | null {
51
+ const mcpServerId = readString(args.mcpServerId);
52
+ if (mcpServerId) return { source: 'mcpServerId', input: mcpServerId };
53
+ const target = readString(args.target);
54
+ if (target) return { source: 'target', input: target };
55
+ const query = readString(args.query);
56
+ return query ? { source: 'query', input: query } : null;
57
+ }
58
+
59
+ function serverSearchText(server: McpServerRecord): string {
60
+ return [
61
+ server.name,
62
+ server.connected ? 'connected' : 'disconnected',
63
+ server.trustMode,
64
+ server.role,
65
+ server.schemaFreshness,
66
+ server.quarantineReason ?? '',
67
+ server.quarantineDetail ?? '',
68
+ ...server.allowedHosts,
69
+ ].join('\n').toLowerCase();
70
+ }
71
+
72
+ function describeCandidate(server: McpServerRecord): Record<string, unknown> {
73
+ return {
74
+ mcpServerId: server.name,
75
+ connected: server.connected,
76
+ trustMode: server.trustMode,
77
+ role: server.role,
78
+ schemaFreshness: server.schemaFreshness,
79
+ };
80
+ }
81
+
82
+ function toolsByServer(tools: readonly McpToolRecord[]): ReadonlyMap<string, readonly McpToolRecord[]> {
83
+ const grouped = new Map<string, McpToolRecord[]>();
84
+ for (const tool of tools) {
85
+ const entries = grouped.get(tool.serverName) ?? [];
86
+ entries.push(tool);
87
+ grouped.set(tool.serverName, entries);
88
+ }
89
+ return grouped;
90
+ }
91
+
92
+ function describeServer(
93
+ server: McpServerRecord,
94
+ options: {
95
+ readonly includeParameters?: boolean;
96
+ readonly tools?: readonly McpToolRecord[];
97
+ readonly lookup?: Record<string, unknown>;
98
+ } = {},
99
+ ): Record<string, unknown> {
100
+ const tools = options.tools ?? [];
101
+ return {
102
+ name: server.name,
103
+ connected: server.connected,
104
+ trustMode: server.trustMode,
105
+ role: server.role,
106
+ schemaFreshness: server.schemaFreshness,
107
+ allowedPathCount: server.allowedPaths.length,
108
+ allowedHostCount: server.allowedHosts.length,
109
+ ...(server.quarantineReason ? { quarantineReason: server.quarantineReason } : {}),
110
+ ...(server.quarantineDetail ? { quarantineDetail: server.quarantineDetail } : {}),
111
+ ...(options.lookup ? { lookup: options.lookup } : {}),
112
+ ...(options.includeParameters ? {
113
+ tools: tools.map((tool) => ({
114
+ name: tool.toolName,
115
+ ...(tool.description ? { description: tool.description } : {}),
116
+ })),
117
+ toolCount: tools.length,
118
+ } : { toolCount: tools.length }),
119
+ policy: {
120
+ effect: 'read-only',
121
+ values: 'Server posture returns trust, role, connection, quarantine, and tool metadata; env values and secret config values are never returned.',
122
+ mutation: 'MCP add/remove/reload/trust/role/quarantine operations stay explicit confirmation-gated workspace or slash-command flows.',
123
+ allowAll: 'allow-all trust decisions remain routed through Settings -> MCP, not direct command escalation.',
124
+ },
125
+ modelAccess: {
126
+ reviewCommand: '/mcp review',
127
+ serversCommand: '/mcp servers',
128
+ toolsCommand: `/mcp tools ${server.name}`,
129
+ repairCommand: `/mcp repair ${server.name}`,
130
+ authReviewCommand: '/mcp auth-review',
131
+ configCommand: '/mcp config',
132
+ workspaceActionIds: [
133
+ 'mcp-review',
134
+ 'mcp-tools-server',
135
+ 'mcp-repair',
136
+ 'mcp-config',
137
+ 'mcp-add-server',
138
+ 'mcp-settings',
139
+ ],
140
+ confirmationGatedCommands: [
141
+ `/mcp trust ${server.name} <constrained|ask-on-risk|blocked> --yes`,
142
+ `/mcp role ${server.name} <role> --yes`,
143
+ `/mcp quarantine ${server.name} approve <operatorId> --yes`,
144
+ `/mcp remove ${server.name} --yes`,
145
+ ],
146
+ },
147
+ };
148
+ }
149
+
150
+ async function readMcpTools(api: ReturnType<typeof resolveMcpApi>, includeParameters: boolean): Promise<readonly McpToolRecord[]> {
151
+ if (!includeParameters || !api?.listAllTools) return [];
152
+ try {
153
+ return await api.listAllTools();
154
+ } catch {
155
+ return [];
156
+ }
157
+ }
158
+
159
+ export function mcpServerCatalogStatus(context: CommandContext): Record<string, unknown> {
160
+ const api = resolveMcpApi(context);
161
+ if (!api) {
162
+ return {
163
+ modes: ['mcp_servers', 'mcp_server'],
164
+ status: 'unavailable',
165
+ readOnly: true,
166
+ };
167
+ }
168
+ const servers = api.listServerSecurity();
169
+ return {
170
+ modes: ['mcp_servers', 'mcp_server'],
171
+ servers: servers.length,
172
+ connected: servers.filter((server) => server.connected).length,
173
+ attention: servers.filter((server) => !server.connected || server.schemaFreshness === 'quarantined').length,
174
+ readOnly: true,
175
+ };
176
+ }
177
+
178
+ export async function mcpServerSummary(context: CommandContext, args: AgentHarnessMcpArgs): Promise<Record<string, unknown>> {
179
+ const api = resolveMcpApi(context);
180
+ if (!api) {
181
+ return {
182
+ status: 'unavailable',
183
+ servers: [],
184
+ returned: 0,
185
+ total: 0,
186
+ policy: 'MCP runtime API is unavailable in this Agent context.',
187
+ };
188
+ }
189
+ const query = readString(args.query).toLowerCase();
190
+ const includeParameters = args.includeParameters === true;
191
+ const servers = api.listServerSecurity();
192
+ const tools = toolsByServer(await readMcpTools(api, includeParameters));
193
+ const filtered = servers
194
+ .filter((server) => !query || serverSearchText(server).includes(query))
195
+ .slice(0, readLimit(args.limit, 100));
196
+ return {
197
+ servers: filtered.map((server) => describeServer(server, {
198
+ includeParameters,
199
+ tools: tools.get(server.name) ?? [],
200
+ })),
201
+ returned: filtered.length,
202
+ total: servers.length,
203
+ connected: servers.filter((server) => server.connected).length,
204
+ attention: servers.filter((server) => !server.connected || server.schemaFreshness === 'quarantined').length,
205
+ policy: 'Read-only MCP server posture. Use confirmed workspace actions or slash-command mirrors for add/remove/reload/trust/role/quarantine mutations.',
206
+ };
207
+ }
208
+
209
+ export async function describeHarnessMcpServer(context: CommandContext, args: AgentHarnessMcpArgs): Promise<McpServerResolution> {
210
+ const lookup = lookupFromArgs(args);
211
+ if (!lookup) {
212
+ return {
213
+ status: 'missing_lookup',
214
+ usage: 'mcp_server requires mcpServerId, target, or query. Use mode:"mcp_servers" to inspect MCP server ids.',
215
+ };
216
+ }
217
+ const api = resolveMcpApi(context);
218
+ if (!api) {
219
+ return {
220
+ status: 'missing_lookup',
221
+ usage: 'MCP runtime API is unavailable in this Agent context.',
222
+ };
223
+ }
224
+ const servers = api.listServerSecurity();
225
+ const normalized = lookup.input.toLowerCase();
226
+ const exact = servers.find((server) => server.name === lookup.input);
227
+ const tools = toolsByServer(await readMcpTools(api, true));
228
+ if (exact) return { status: 'found', server: describeServer(exact, { includeParameters: true, tools: tools.get(exact.name) ?? [], lookup: { ...lookup, resolvedBy: 'id' } }) };
229
+ const insensitive = servers.find((server) => server.name.toLowerCase() === normalized);
230
+ if (insensitive) return { status: 'found', server: describeServer(insensitive, { includeParameters: true, tools: tools.get(insensitive.name) ?? [], lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
231
+ const searched = servers.filter((server) => serverSearchText(server).includes(normalized));
232
+ if (searched.length === 1) {
233
+ return { status: 'found', server: describeServer(searched[0]!, { includeParameters: true, tools: tools.get(searched[0]!.name) ?? [], lookup: { ...lookup, resolvedBy: 'search' } }) };
234
+ }
235
+ if (searched.length > 1) {
236
+ return {
237
+ status: 'ambiguous',
238
+ input: lookup.input,
239
+ candidates: searched.slice(0, 8).map(describeCandidate),
240
+ };
241
+ }
242
+ return {
243
+ status: 'missing_lookup',
244
+ usage: `Unknown MCP server ${lookup.input}. Use mode:"mcp_servers" to inspect MCP server ids.`,
245
+ };
246
+ }