@pellux/goodvibes-agent 1.8.1 → 1.8.2

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 (88) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +1 -1
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
  5. package/dist/package/main.js +135337 -118733
  6. package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
  7. package/docs/tools-and-commands.md +9 -0
  8. package/package.json +3 -2
  9. package/src/agent/capability-registry.ts +300 -0
  10. package/src/agent/email/style-reply-lane.ts +13 -4
  11. package/src/agent/memory-consolidation-receipts.ts +79 -0
  12. package/src/agent/memory-prompt.ts +3 -1
  13. package/src/agent/operator-gateway-call.ts +134 -0
  14. package/src/agent/principal-attribution.ts +97 -0
  15. package/src/agent/prompt-context-receipts.ts +5 -1
  16. package/src/agent/unified-inbox.ts +17 -1
  17. package/src/cli/bundle-command.ts +2 -1
  18. package/src/cli/channel-profiles-command.ts +168 -0
  19. package/src/cli/ci-command.ts +214 -0
  20. package/src/cli/completion.ts +5 -0
  21. package/src/cli/connected-host-metrics.ts +165 -0
  22. package/src/cli/entrypoint.ts +25 -1
  23. package/src/cli/fleet-command.ts +178 -0
  24. package/src/cli/help.ts +115 -0
  25. package/src/cli/import-command.ts +208 -0
  26. package/src/cli/index.ts +1 -0
  27. package/src/cli/management-commands.ts +9 -8
  28. package/src/cli/management.ts +42 -0
  29. package/src/cli/memory-command.ts +4 -3
  30. package/src/cli/openclaw-import.ts +382 -0
  31. package/src/cli/operator-command-args.ts +88 -0
  32. package/src/cli/parser.ts +12 -0
  33. package/src/cli/personas-command.ts +3 -2
  34. package/src/cli/principals-command.ts +230 -0
  35. package/src/cli/profiles-command.ts +2 -1
  36. package/src/cli/relay-command.ts +100 -0
  37. package/src/cli/routines-command.ts +4 -3
  38. package/src/cli/skill-bundle-command.ts +3 -2
  39. package/src/cli/skills-command.ts +3 -2
  40. package/src/cli/status.ts +114 -2
  41. package/src/cli/temporal-label.ts +89 -0
  42. package/src/cli/types.ts +7 -0
  43. package/src/cli/workspaces-command.ts +135 -0
  44. package/src/config/agent-settings-policy.ts +8 -0
  45. package/src/config/checkpoint-settings.ts +113 -0
  46. package/src/config/workspace-registration.ts +299 -0
  47. package/src/core/rewind-turn-anchors.ts +82 -0
  48. package/src/input/agent-workspace-categories.ts +1 -0
  49. package/src/input/agent-workspace-channel-triage.ts +27 -8
  50. package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
  51. package/src/input/agent-workspace-onboarding-categories.ts +17 -1
  52. package/src/input/agent-workspace-snapshot.ts +25 -3
  53. package/src/input/agent-workspace-types.ts +5 -5
  54. package/src/input/commands/connected-host-admin-runtime.ts +75 -0
  55. package/src/input/commands.ts +2 -0
  56. package/src/input/settings-modal-types.ts +16 -5
  57. package/src/main.ts +17 -12
  58. package/src/permissions/approval-posture.ts +40 -5
  59. package/src/permissions/prompt.ts +35 -1
  60. package/src/renderer/settings-modal-helpers.ts +11 -0
  61. package/src/renderer/settings-modal.ts +21 -0
  62. package/src/renderer/terminal-escapes.ts +65 -12
  63. package/src/runtime/agent-runtime-events.ts +102 -3
  64. package/src/runtime/bootstrap-core.ts +111 -10
  65. package/src/runtime/bootstrap-external-services.ts +156 -0
  66. package/src/runtime/bootstrap.ts +73 -69
  67. package/src/runtime/context-accounting-source.ts +114 -0
  68. package/src/runtime/conversation-rewind-port.ts +133 -0
  69. package/src/runtime/index.ts +8 -28
  70. package/src/runtime/memory-consolidation-scheduler.ts +67 -0
  71. package/src/runtime/memory-consolidation-wiring.ts +56 -0
  72. package/src/runtime/memory-usage-wiring.ts +75 -0
  73. package/src/runtime/services.ts +250 -14
  74. package/src/runtime/tool-permission-safety.ts +18 -1
  75. package/src/shell/blocking-input.ts +38 -8
  76. package/src/shell/startup-wiring.ts +26 -1
  77. package/src/tools/agent-context-policy.ts +1 -1
  78. package/src/tools/agent-harness-cli-command-policy.ts +29 -3
  79. package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
  80. package/src/tools/agent-harness-memory-posture.ts +57 -2
  81. package/src/tools/agent-harness-metadata.ts +12 -0
  82. package/src/tools/agent-harness-mode-catalog.ts +1 -1
  83. package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
  84. package/src/tools/agent-harness-prompt-context.ts +3 -1
  85. package/src/tools/agent-harness-tool.ts +3 -2
  86. package/src/tools/agent-policy-explanation.ts +40 -0
  87. package/src/tools/tool-definition-compaction.ts +2 -0
  88. package/src/version.ts +1 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * principal-attribution.ts
3
+ *
4
+ * Shared "honest inbound attribution" helper. Anywhere the Agent displays who a
5
+ * session or message came from, it should show the RESOLVED principal when the
6
+ * sender's channel identity maps to one, and plainly say "unknown principal"
7
+ * when it does not — never a blank field, never a guess.
8
+ *
9
+ * Resolution goes through the connected host's `principals.resolve` operator
10
+ * method: POST /api/principals/resolve with `{ channel, value }`, returning
11
+ * `{ principal, known }`. An unmapped identity resolves to `known: false` —
12
+ * the registry never guesses, and neither does this helper.
13
+ *
14
+ * Every failure mode (empty channel identity, no connected-host token, a
15
+ * failed/unreachable resolve call, or an explicit `known: false`) collapses to
16
+ * the same human-facing label, `UNKNOWN_PRINCIPAL_LABEL`, so the display never
17
+ * over-claims certainty about which failure occurred. Callers that want the
18
+ * underlying detail can read the optional `error` field separately.
19
+ */
20
+
21
+ import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
22
+ import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
23
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
24
+
25
+ export const UNKNOWN_PRINCIPAL_LABEL = 'unknown principal';
26
+
27
+ export interface PrincipalAttributionConnection {
28
+ readonly baseUrl: string;
29
+ readonly token: string | null;
30
+ }
31
+
32
+ export interface ResolvedPrincipalSummary {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly kind: string;
36
+ }
37
+
38
+ export interface ResolvedPrincipalAttribution {
39
+ readonly known: boolean;
40
+ readonly label: string;
41
+ readonly principal: ResolvedPrincipalSummary | null;
42
+ /** Diagnostic detail only — never surfaced in `label` itself. */
43
+ readonly error?: string;
44
+ }
45
+
46
+ type PrincipalsResolveInput = OperatorMethodInput<'principals.resolve'>;
47
+ type PrincipalsResolveOutput = OperatorMethodOutput<'principals.resolve'>;
48
+
49
+ const UNKNOWN_ATTRIBUTION: ResolvedPrincipalAttribution = {
50
+ known: false,
51
+ label: UNKNOWN_PRINCIPAL_LABEL,
52
+ principal: null,
53
+ };
54
+
55
+ export function formatPrincipalLabel(principal: ResolvedPrincipalSummary): string {
56
+ return `${principal.name} (${principal.id})`;
57
+ }
58
+
59
+ /**
60
+ * Resolve a channel identity ({channel, value}) to its named principal for
61
+ * honest inbound-attribution display.
62
+ *
63
+ * Never fabricates a name. Returns the shared "unknown principal" label
64
+ * (never a blank string) when: the identity is empty, no connected-host
65
+ * operator token is available, or the resolve call fails or returns
66
+ * `known: false`.
67
+ */
68
+ export async function resolveChannelPrincipalAttribution(
69
+ connection: PrincipalAttributionConnection,
70
+ channel: string,
71
+ value: string,
72
+ ): Promise<ResolvedPrincipalAttribution> {
73
+ if (!channel.trim() || !value.trim()) return UNKNOWN_ATTRIBUTION;
74
+ if (!connection.token) {
75
+ return { ...UNKNOWN_ATTRIBUTION, error: 'No connected-host operator token available for principal resolution.' };
76
+ }
77
+ try {
78
+ const sdk = createBrowserGoodVibesSdk({
79
+ baseUrl: connection.baseUrl,
80
+ authToken: connection.token,
81
+ // A single attempt is enough for a display-only lookup — this must not
82
+ // stall triage/inbox rendering behind retry backoff.
83
+ retry: { maxAttempts: 1, baseDelayMs: 0, maxDelayMs: 0 },
84
+ });
85
+ const payload: PrincipalsResolveInput = { channel, value };
86
+ const result = await sdk.operator.invoke('principals.resolve', payload) as PrincipalsResolveOutput;
87
+ if (!result.known || !result.principal) return UNKNOWN_ATTRIBUTION;
88
+ const principal: ResolvedPrincipalSummary = {
89
+ id: result.principal.id,
90
+ name: result.principal.name,
91
+ kind: result.principal.kind,
92
+ };
93
+ return { known: true, label: formatPrincipalLabel(principal), principal };
94
+ } catch (error) {
95
+ return { ...UNKNOWN_ATTRIBUTION, error: summarizeError(error) };
96
+ }
97
+ }
@@ -202,7 +202,11 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
202
202
  const projectContext = discoverProjectContextFiles(input.shellPaths);
203
203
  const projectContextPrompt = buildProjectContextPrompt(input.shellPaths) ?? '';
204
204
  const memoryRecords = resolveMemoryRecords(input);
205
- const eligibleMemory = memoryRecords.filter(isPromptActiveMemory);
205
+ // Bound to one arg: Array.filter passes (element, index, array), and
206
+ // isPromptActiveMemory's second param is `now` — passing the callback bare
207
+ // would leak the array index in as `now`, breaking the temporal-validity
208
+ // check for every record past index 0.
209
+ const eligibleMemory = memoryRecords.filter((record) => isPromptActiveMemory(record));
206
210
  const memoryRanking = rankMemoryForTurn(input.memoryRegistry, eligibleMemory, input.turnText);
207
211
  const activeMemory = memoryRanking.records.slice(0, 10);
208
212
  const activeMemoryIds = new Set(activeMemory.map((record) => record.id));
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { AgentWorkspaceChannelTriage } from '../input/agent-workspace-channel-triage.ts';
20
+ import { UNKNOWN_PRINCIPAL_LABEL } from './principal-attribution.ts';
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Item kinds
@@ -62,6 +63,10 @@ export interface UnifiedInboxRouteBindingItem {
62
63
  readonly surfaceId: string | null;
63
64
  /** External id is digested — never the raw identifier. */
64
65
  readonly externalIdDigest: string | null;
66
+ /** Resolved sender principal, when the channel identity maps to one. Never a guess. */
67
+ readonly principal: { readonly id: string; readonly name: string; readonly kind: string } | null;
68
+ /** Human-facing sender label — the resolved principal's "name (id)", or "unknown principal". */
69
+ readonly principalLabel: string;
65
70
  readonly sessionPolicy: string | null;
66
71
  readonly threadPolicy: string | null;
67
72
  readonly deliveryGuarantee: string | null;
@@ -202,6 +207,15 @@ function toSurfaceMessageItem(raw: Record<string, unknown>): UnifiedInboxSurface
202
207
  };
203
208
  }
204
209
 
210
+ function toPrincipalOrNull(raw: Record<string, unknown>): { readonly id: string; readonly name: string; readonly kind: string } | null {
211
+ const principalRaw = isRecord(raw.principal) ? raw.principal : null;
212
+ if (!principalRaw) return null;
213
+ const id = readString(principalRaw, 'id');
214
+ const name = readString(principalRaw, 'name');
215
+ const kind = readString(principalRaw, 'kind');
216
+ return id && name && kind ? { id, name, kind } : null;
217
+ }
218
+
205
219
  function toRouteBindingItem(raw: Record<string, unknown>): UnifiedInboxRouteBindingItem {
206
220
  return {
207
221
  kind: 'route_binding',
@@ -210,6 +224,8 @@ function toRouteBindingItem(raw: Record<string, unknown>): UnifiedInboxRouteBind
210
224
  surfaceKind: readString(raw, 'surfaceKind', 'unknown'),
211
225
  surfaceId: readStringOrNull(raw, 'surfaceId'),
212
226
  externalIdDigest: readStringOrNull(raw, 'externalIdDigest'),
227
+ principal: toPrincipalOrNull(raw),
228
+ principalLabel: readString(raw, 'principalLabel', UNKNOWN_PRINCIPAL_LABEL),
213
229
  sessionPolicy: readStringOrNull(raw, 'sessionPolicy'),
214
230
  threadPolicy: readStringOrNull(raw, 'threadPolicy'),
215
231
  deliveryGuarantee: readStringOrNull(raw, 'deliveryGuarantee'),
@@ -385,7 +401,7 @@ export function formatUnifiedInbox(inbox: UnifiedInbox): string {
385
401
  if (routeBindingItems.length > 0) {
386
402
  lines.push('', ' Route Bindings');
387
403
  for (const item of routeBindingItems.slice(0, 10)) {
388
- lines.push(` - ${item.id}: ${item.surfaceKind} ${item.bindingKind} ext=${item.externalIdDigest ?? 'none'}`);
404
+ lines.push(` - ${item.id}: ${item.surfaceKind} ${item.bindingKind} ext=${item.externalIdDigest ?? 'none'} sender=${item.principalLabel}`);
389
405
  }
390
406
  }
391
407
 
@@ -16,6 +16,7 @@ import { classifyProviderSetup } from './provider-classification.ts';
16
16
  import { buildCliServicePosture } from './service-posture.ts';
17
17
  import { REDACTED_VALUE, collectSensitiveConfigValues, isRedactedValue, redactConfig, redactSerializedSecrets } from './redaction.ts';
18
18
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
19
+ import { appendTemporalLabel } from './temporal-label.ts';
19
20
 
20
21
  interface BundleInspectSummary {
21
22
  readonly type: string;
@@ -142,7 +143,7 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
142
143
  ` type: ${summary.type}`,
143
144
  ` version: ${summary.version}`,
144
145
  ` path: ${summary.path}`,
145
- ` capturedAt: ${summary.capturedAt === null ? 'n/a' : new Date(summary.capturedAt).toISOString()}`,
146
+ ` capturedAt: ${summary.capturedAt === null ? 'n/a' : appendTemporalLabel(new Date(summary.capturedAt).toISOString(), summary.capturedAt)}`,
146
147
  ` configKeys: ${summary.configKeys}`,
147
148
  ` redactedConfigKeys: ${summary.redactedConfigPaths.length}`,
148
149
  ` diagnostics: ${summary.hasDiagnostics ? 'present' : 'missing'}`,
@@ -0,0 +1,168 @@
1
+ import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
2
+ import {
3
+ formatOperatorGatewayFailure,
4
+ invokeOperatorGatewayMethod,
5
+ } from '../agent/operator-gateway-call.ts';
6
+ import { resolveAgentConnectedHostConnection } from '../agent/routine-schedule-promotion.ts';
7
+ import {
8
+ operatorFlagValue,
9
+ parseOperatorCommandArgs,
10
+ } from './operator-command-args.ts';
11
+ import type { CliCommandOutput } from './types.ts';
12
+ import type { CliCommandRuntime } from './management.ts';
13
+ import { appendTemporalLabel } from './temporal-label.ts';
14
+
15
+ const CHANNEL_PROFILES_ROUTE = '/api/channels/profiles';
16
+ const CHANNEL_PROFILE_ROUTE = '/api/channels/profiles/{surfaceKind}';
17
+
18
+ const CHANNEL_PROFILES_GET_USAGE = 'Usage: goodvibes-agent channel-profiles get <surfaceKind> [--channel-id <id>]';
19
+ const CHANNEL_PROFILES_SET_USAGE = 'Usage: goodvibes-agent channel-profiles set <surfaceKind> [--channel-id <id>] [--model <model>] [--provider <provider>] [--permission-mode <plan|normal|accept-edits|auto>] --yes';
20
+ const CHANNEL_PROFILES_DELETE_USAGE = 'Usage: goodvibes-agent channel-profiles delete <surfaceKind> [--channel-id <id>] --yes';
21
+ const CHANNEL_PROFILES_USAGE = 'Usage: goodvibes-agent channel-profiles [list|get <surfaceKind> [--channel-id <id>]|set <surfaceKind> [--channel-id <id>] [--model <model>] [--provider <provider>] [--permission-mode <mode>] --yes|delete <surfaceKind> [--channel-id <id>] --yes]';
22
+
23
+ const PERMISSION_MODES = new Set(['plan', 'normal', 'accept-edits', 'auto']);
24
+
25
+ type ChannelProfile = OperatorMethodOutput<'channels.profiles.list'>['bindings'][number];
26
+ type PermissionMode = NonNullable<OperatorMethodInput<'channels.profiles.set'>['permissionMode']>;
27
+
28
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
29
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
30
+ }
31
+
32
+ function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
33
+ return {
34
+ output: jsonOrText(runtime, { ok: false, kind: 'invalid_channel_profiles_command', error }, error),
35
+ exitCode: 2,
36
+ };
37
+ }
38
+
39
+ function parsePermissionMode(value: string | undefined, usage: string): PermissionMode | undefined {
40
+ if (value === undefined) return undefined;
41
+ const normalized = value.trim().toLowerCase();
42
+ if (!PERMISSION_MODES.has(normalized)) throw new Error(`${usage}\n--permission-mode must be one of: plan, normal, accept-edits, auto.`);
43
+ return normalized as PermissionMode;
44
+ }
45
+
46
+ function renderChannelProfile(binding: ChannelProfile): string {
47
+ return [
48
+ `Channel profile ${binding.id}`,
49
+ ` surface ${binding.surfaceKind}${binding.channelId ? ` channel ${binding.channelId}` : ''}`,
50
+ ` model ${binding.model ?? '(default)'}`,
51
+ ` provider ${binding.provider ?? '(default)'}`,
52
+ ` permission mode ${binding.permissionMode ?? '(default)'}`,
53
+ ` updated ${appendTemporalLabel(new Date(binding.updatedAt).toISOString(), binding.updatedAt)}`,
54
+ ].join('\n');
55
+ }
56
+
57
+ function renderChannelProfileLine(binding: ChannelProfile): string {
58
+ const surface = binding.channelId ? `${binding.surfaceKind}:${binding.channelId}` : binding.surfaceKind;
59
+ return ` ${surface.padEnd(24)} model ${(binding.model ?? '(default)').padEnd(20)} provider ${(binding.provider ?? '(default)').padEnd(14)} permission ${binding.permissionMode ?? '(default)'}`;
60
+ }
61
+
62
+ function renderChannelProfileList(bindings: readonly ChannelProfile[]): string {
63
+ if (bindings.length === 0) {
64
+ return [
65
+ 'Channel profiles',
66
+ ' No per-channel profile bindings configured yet.',
67
+ ' next goodvibes-agent channel-profiles set <surfaceKind> --model <model> --yes',
68
+ ].join('\n');
69
+ }
70
+ return [`Channel profiles (${bindings.length})`, ...bindings.map(renderChannelProfileLine)].join('\n');
71
+ }
72
+
73
+ async function handleChannelProfilesList(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
74
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
75
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.list'>>(connection, 'channels.profiles.list', CHANNEL_PROFILES_ROUTE, {});
76
+ if (!result.ok) {
77
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
78
+ }
79
+ return { output: jsonOrText(runtime, result, renderChannelProfileList(result.data.bindings)), exitCode: 0 };
80
+ }
81
+
82
+ async function handleChannelProfilesGet(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
83
+ const parsed = parseOperatorCommandArgs(args, ['channel-id']);
84
+ const surfaceKind = parsed.positionals[0];
85
+ if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_GET_USAGE);
86
+ const payload: OperatorMethodInput<'channels.profiles.get'> = {
87
+ surfaceKind,
88
+ channelId: operatorFlagValue(parsed, 'channel-id'),
89
+ };
90
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
91
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.get'>>(connection, 'channels.profiles.get', CHANNEL_PROFILE_ROUTE, payload);
92
+ if (!result.ok) {
93
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
94
+ }
95
+ return { output: jsonOrText(runtime, result, renderChannelProfile(result.data.binding)), exitCode: 0 };
96
+ }
97
+
98
+ async function handleChannelProfilesSet(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
99
+ const parsed = parseOperatorCommandArgs(args, ['channel-id', 'model', 'provider', 'permission-mode']);
100
+ const surfaceKind = parsed.positionals[0];
101
+ if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_SET_USAGE);
102
+ let permissionMode: PermissionMode | undefined;
103
+ try {
104
+ permissionMode = parsePermissionMode(operatorFlagValue(parsed, 'permission-mode'), CHANNEL_PROFILES_SET_USAGE);
105
+ } catch (error) {
106
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
107
+ }
108
+ if (!parsed.yes) {
109
+ return { output: `Refusing to set channel profile ${surfaceKind} without --yes.`, exitCode: 2 };
110
+ }
111
+ const payload: OperatorMethodInput<'channels.profiles.set'> = {
112
+ surfaceKind,
113
+ channelId: operatorFlagValue(parsed, 'channel-id'),
114
+ model: operatorFlagValue(parsed, 'model'),
115
+ provider: operatorFlagValue(parsed, 'provider'),
116
+ permissionMode,
117
+ };
118
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
119
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.set'>>(connection, 'channels.profiles.set', CHANNEL_PROFILES_ROUTE, payload);
120
+ if (!result.ok) {
121
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
122
+ }
123
+ return {
124
+ output: jsonOrText(runtime, result, [`Set channel profile ${result.data.binding.id}`, renderChannelProfile(result.data.binding)].join('\n')),
125
+ exitCode: 0,
126
+ };
127
+ }
128
+
129
+ async function handleChannelProfilesDelete(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
130
+ const parsed = parseOperatorCommandArgs(args, ['channel-id']);
131
+ const surfaceKind = parsed.positionals[0];
132
+ if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_DELETE_USAGE);
133
+ if (!parsed.yes) {
134
+ return { output: `Refusing to delete channel profile ${surfaceKind} without --yes.`, exitCode: 2 };
135
+ }
136
+ const payload: OperatorMethodInput<'channels.profiles.delete'> = {
137
+ surfaceKind,
138
+ channelId: operatorFlagValue(parsed, 'channel-id'),
139
+ };
140
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
141
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.delete'>>(connection, 'channels.profiles.delete', CHANNEL_PROFILE_ROUTE, payload);
142
+ if (!result.ok) {
143
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
144
+ }
145
+ return {
146
+ output: jsonOrText(runtime, result, `Deleted channel profile ${result.data.surfaceKind}${result.data.channelId ? `:${result.data.channelId}` : ''}: ${result.data.deleted ? 'ok' : 'not found'}`),
147
+ exitCode: result.data.deleted ? 0 : 1,
148
+ };
149
+ }
150
+
151
+ export async function handleChannelProfilesCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
152
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
153
+ switch (sub.toLowerCase()) {
154
+ case 'list':
155
+ return handleChannelProfilesList(runtime);
156
+ case 'get':
157
+ case 'show':
158
+ return handleChannelProfilesGet(runtime, rest);
159
+ case 'set':
160
+ case 'update':
161
+ return handleChannelProfilesSet(runtime, rest);
162
+ case 'delete':
163
+ case 'remove':
164
+ return handleChannelProfilesDelete(runtime, rest);
165
+ default:
166
+ return usageFailure(runtime, CHANNEL_PROFILES_USAGE);
167
+ }
168
+ }
@@ -0,0 +1,214 @@
1
+ import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
2
+ import {
3
+ formatOperatorGatewayFailure,
4
+ invokeOperatorGatewayMethod,
5
+ } from '../agent/operator-gateway-call.ts';
6
+ import { resolveAgentConnectedHostConnection } from '../agent/routine-schedule-promotion.ts';
7
+ import {
8
+ operatorFlagValue,
9
+ operatorHasFlag,
10
+ operatorParseIntFlag,
11
+ operatorRequiredFlag,
12
+ parseOperatorCommandArgs,
13
+ } from './operator-command-args.ts';
14
+ import type { CliCommandOutput } from './types.ts';
15
+ import type { CliCommandRuntime } from './management.ts';
16
+ import { appendTemporalLabel } from './temporal-label.ts';
17
+
18
+ const CI_STATUS_ROUTE = '/api/ci/status';
19
+ const CI_WATCHES_ROUTE = '/api/ci/watches';
20
+ const CI_WATCH_ROUTE = '/api/ci/watches/{watchId}';
21
+ const CI_WATCH_RUN_ROUTE = '/api/ci/watches/{watchId}/run';
22
+
23
+ const CI_STATUS_USAGE = 'Usage: goodvibes-agent ci status <repo> [--ref <ref>] [--pr <number>]';
24
+ const CI_WATCHES_CREATE_USAGE = 'Usage: goodvibes-agent ci watches create <repo> --delivery-channel <channel> [--ref <ref>] [--pr <number>] [--trigger-fix-session] --yes';
25
+ const CI_WATCHES_DELETE_USAGE = 'Usage: goodvibes-agent ci watches delete <watchId> --yes';
26
+ const CI_WATCHES_RUN_USAGE = 'Usage: goodvibes-agent ci watches run <watchId>';
27
+ const CI_USAGE = 'Usage: goodvibes-agent ci [status <repo> [--ref <ref>] [--pr <number>]|watches list|watches create <repo> --delivery-channel <channel> [--ref <ref>] [--pr <number>] [--trigger-fix-session] --yes|watches delete <watchId> --yes|watches run <watchId>]';
28
+
29
+ type CiReport = OperatorMethodOutput<'ci.status'>['report'];
30
+ type CiWatch = OperatorMethodOutput<'ci.watches.list'>['watches'][number];
31
+
32
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
33
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
34
+ }
35
+
36
+ function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
37
+ return {
38
+ output: jsonOrText(runtime, { ok: false, kind: 'invalid_ci_command', error }, error),
39
+ exitCode: 2,
40
+ };
41
+ }
42
+
43
+ function renderCiJobs(report: CiReport): readonly string[] {
44
+ if (report.jobs.length === 0) return [' jobs (none reported)'];
45
+ return [
46
+ ` jobs (${report.jobs.length})`,
47
+ ...report.jobs.map((job) => {
48
+ const conclusion = job.conclusion ?? 'pending';
49
+ const continueOnError = job.continueOnError ? ' continue-on-error' : '';
50
+ return ` ${job.name.padEnd(32)} status ${job.status.padEnd(12)} conclusion ${conclusion}${continueOnError}`;
51
+ }),
52
+ ];
53
+ }
54
+
55
+ function renderCiReport(report: CiReport): string {
56
+ return [
57
+ `CI report ${report.repo}${report.ref ? ` @ ${report.ref}` : ''}${report.prNumber !== undefined ? ` PR #${report.prNumber}` : ''}`,
58
+ ` overall ${report.overall}`,
59
+ ...renderCiJobs(report),
60
+ report.violations.length > 0 ? ` violations: ${report.violations.join('; ')}` : ' violations: none',
61
+ ` checked at ${appendTemporalLabel(new Date(report.checkedAt).toISOString(), report.checkedAt)}`,
62
+ ].join('\n');
63
+ }
64
+
65
+ function renderCiWatch(watch: CiWatch): string {
66
+ return [
67
+ ` ${watch.id} ${watch.repo}${watch.ref ? ` @ ${watch.ref}` : ''}${watch.prNumber !== undefined ? ` PR #${watch.prNumber}` : ''}`,
68
+ ` delivery ${watch.deliveryChannel} trigger-fix-session ${watch.triggerFixSession ? 'yes' : 'no'} last overall ${watch.lastOverall ?? 'unknown'}`,
69
+ ].join('\n');
70
+ }
71
+
72
+ function renderCiWatchList(watches: readonly CiWatch[]): string {
73
+ if (watches.length === 0) {
74
+ return [
75
+ 'CI watches',
76
+ ' No CI watches configured yet.',
77
+ ' next goodvibes-agent ci watches create <repo> --delivery-channel <channel> --yes',
78
+ ].join('\n');
79
+ }
80
+ return [`CI watches (${watches.length})`, ...watches.map(renderCiWatch)].join('\n');
81
+ }
82
+
83
+ async function handleCiStatus(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
84
+ const parsed = parseOperatorCommandArgs(args, ['ref', 'pr']);
85
+ const repo = parsed.positionals[0];
86
+ if (!repo) return usageFailure(runtime, CI_STATUS_USAGE);
87
+ let prNumber: number | undefined;
88
+ try {
89
+ prNumber = operatorParseIntFlag(parsed, 'pr', CI_STATUS_USAGE);
90
+ } catch (error) {
91
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
92
+ }
93
+ const payload: OperatorMethodInput<'ci.status'> = {
94
+ repo,
95
+ ref: operatorFlagValue(parsed, 'ref'),
96
+ prNumber,
97
+ };
98
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
99
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.status'>>(connection, 'ci.status', CI_STATUS_ROUTE, payload);
100
+ if (!result.ok) {
101
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
102
+ }
103
+ return { output: jsonOrText(runtime, result, renderCiReport(result.data.report)), exitCode: 0 };
104
+ }
105
+
106
+ async function handleCiWatchesList(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
107
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
108
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.list'>>(connection, 'ci.watches.list', CI_WATCHES_ROUTE, {});
109
+ if (!result.ok) {
110
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
111
+ }
112
+ return { output: jsonOrText(runtime, result, renderCiWatchList(result.data.watches)), exitCode: 0 };
113
+ }
114
+
115
+ async function handleCiWatchesCreate(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
116
+ const parsed = parseOperatorCommandArgs(args, ['ref', 'pr', 'delivery-channel']);
117
+ const repo = parsed.positionals[0];
118
+ if (!repo) return usageFailure(runtime, CI_WATCHES_CREATE_USAGE);
119
+ let deliveryChannel: string;
120
+ let prNumber: number | undefined;
121
+ try {
122
+ deliveryChannel = operatorRequiredFlag(parsed, 'delivery-channel', CI_WATCHES_CREATE_USAGE);
123
+ prNumber = operatorParseIntFlag(parsed, 'pr', CI_WATCHES_CREATE_USAGE);
124
+ } catch (error) {
125
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
126
+ }
127
+ if (!parsed.yes) {
128
+ return { output: `Refusing to create a CI watch for ${repo} without --yes.`, exitCode: 2 };
129
+ }
130
+ const payload: OperatorMethodInput<'ci.watches.create'> = {
131
+ repo,
132
+ ref: operatorFlagValue(parsed, 'ref'),
133
+ prNumber,
134
+ deliveryChannel,
135
+ triggerFixSession: operatorHasFlag(parsed, 'trigger-fix-session') || undefined,
136
+ };
137
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
138
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.create'>>(connection, 'ci.watches.create', CI_WATCHES_ROUTE, payload);
139
+ if (!result.ok) {
140
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
141
+ }
142
+ return {
143
+ output: jsonOrText(runtime, result, [`Created CI watch ${result.data.watch.id}`, renderCiWatch(result.data.watch)].join('\n')),
144
+ exitCode: 0,
145
+ };
146
+ }
147
+
148
+ async function handleCiWatchesDelete(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
149
+ const parsed = parseOperatorCommandArgs(args);
150
+ const watchId = parsed.positionals[0];
151
+ if (!watchId) return usageFailure(runtime, CI_WATCHES_DELETE_USAGE);
152
+ if (!parsed.yes) {
153
+ return { output: `Refusing to delete CI watch ${watchId} without --yes.`, exitCode: 2 };
154
+ }
155
+ const payload: OperatorMethodInput<'ci.watches.delete'> = { watchId };
156
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
157
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.delete'>>(connection, 'ci.watches.delete', CI_WATCH_ROUTE, payload);
158
+ if (!result.ok) {
159
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
160
+ }
161
+ return {
162
+ output: jsonOrText(runtime, result, `Deleted CI watch ${result.data.watchId}: ${result.data.deleted ? 'ok' : 'not found'}`),
163
+ exitCode: result.data.deleted ? 0 : 1,
164
+ };
165
+ }
166
+
167
+ async function handleCiWatchesRun(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
168
+ const parsed = parseOperatorCommandArgs(args);
169
+ const watchId = parsed.positionals[0];
170
+ if (!watchId) return usageFailure(runtime, CI_WATCHES_RUN_USAGE);
171
+ const payload: OperatorMethodInput<'ci.watches.run'> = { watchId };
172
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
173
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.run'>>(connection, 'ci.watches.run', CI_WATCH_RUN_ROUTE, payload);
174
+ if (!result.ok) {
175
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
176
+ }
177
+ return {
178
+ output: jsonOrText(runtime, result, [
179
+ renderCiReport(result.data.report),
180
+ ` notified ${result.data.notified ? 'yes' : 'no'}${result.data.notificationId ? ` (${result.data.notificationId})` : ''}`,
181
+ ` fix session triggered ${result.data.fixSessionTriggered ? 'yes' : 'no'}${result.data.fixSessionId ? ` (${result.data.fixSessionId})` : ''}`,
182
+ ].join('\n')),
183
+ exitCode: 0,
184
+ };
185
+ }
186
+
187
+ async function handleCiWatchesCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
188
+ const [sub = 'list', ...rest] = args;
189
+ switch (sub.toLowerCase()) {
190
+ case 'list':
191
+ return handleCiWatchesList(runtime);
192
+ case 'create':
193
+ return handleCiWatchesCreate(runtime, rest);
194
+ case 'delete':
195
+ case 'remove':
196
+ return handleCiWatchesDelete(runtime, rest);
197
+ case 'run':
198
+ return handleCiWatchesRun(runtime, rest);
199
+ default:
200
+ return usageFailure(runtime, CI_USAGE);
201
+ }
202
+ }
203
+
204
+ export async function handleCiCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
205
+ const [sub, ...rest] = runtime.cli.commandArgs;
206
+ switch ((sub ?? '').toLowerCase()) {
207
+ case 'status':
208
+ return handleCiStatus(runtime, rest);
209
+ case 'watches':
210
+ return handleCiWatchesCommand(runtime, rest);
211
+ default:
212
+ return usageFailure(runtime, CI_USAGE);
213
+ }
214
+ }
@@ -9,6 +9,11 @@ const COMMANDS = [
9
9
  'providers',
10
10
  'profiles',
11
11
  'routines',
12
+ 'ci',
13
+ 'principals',
14
+ 'channel-profiles',
15
+ 'workspaces',
16
+ 'fleet',
12
17
  'auth',
13
18
  'compat',
14
19
  'knowledge',