@pellux/goodvibes-agent 1.8.0 → 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 +9 -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 +135434 -118764
  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 +257 -6
  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,165 @@
1
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
+ import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
4
+
5
+ /**
6
+ * Reads the connected daemon host's process-wide runtime metrics snapshot via
7
+ * the operator gateway's `runtime.metrics.get` verb (REST binding
8
+ * GET /api/runtime/metrics, scope read:telemetry) so `goodvibes-agent status`
9
+ * can show live host counters/gauges alongside the rest of the connected-host
10
+ * posture.
11
+ *
12
+ * Honesty is the whole point of the classification below: the metrics route
13
+ * requires the read:telemetry scope, and a token without it is answered 403 by
14
+ * the gateway. That case is reported as `scope_missing` and rendered as a plain
15
+ * "not permitted" line — never as a screen of zeros that would misrepresent an
16
+ * unauthorized token as a healthy-but-idle host. Missing token, an unreachable
17
+ * host, and an incompatible/absent route are each surfaced as their own honest
18
+ * state too.
19
+ */
20
+
21
+ export type CliConnectedHostMetricsStatus =
22
+ | 'ok'
23
+ | 'auth_required'
24
+ | 'scope_missing'
25
+ | 'route_unavailable'
26
+ | 'unavailable'
27
+ | 'error';
28
+
29
+ /** The compact, already-narrowed set of host metrics this surface renders. */
30
+ export interface CliConnectedHostMetrics {
31
+ readonly httpRequestsTotal: number;
32
+ readonly llmRequestsTotal: number;
33
+ readonly authSuccessTotal: number;
34
+ readonly authFailureTotal: number;
35
+ readonly transportRetriesTotal: number;
36
+ readonly sessionsActive: number;
37
+ readonly sseSubscribers: number;
38
+ readonly telemetryBufferFill: number;
39
+ }
40
+
41
+ export interface CliConnectedHostMetricsSnapshot {
42
+ readonly route: '/api/runtime/metrics';
43
+ readonly methodId: 'runtime.metrics.get';
44
+ readonly requiredScope: 'read:telemetry';
45
+ readonly status: CliConnectedHostMetricsStatus;
46
+ readonly statusCode: number | null;
47
+ readonly metrics: CliConnectedHostMetrics | null;
48
+ readonly error: string | null;
49
+ }
50
+
51
+ export interface CliConnectedHostMetricsInspectionOptions {
52
+ readonly configManager: Pick<ConfigManager, 'get'>;
53
+ readonly homeDirectory: string;
54
+ readonly timeoutMs?: number;
55
+ }
56
+
57
+ // Same base-url resolution the connected-host status probe uses
58
+ // (cli/external-runtime.ts): controlPlane.host/port, defaulting to the local
59
+ // daemon. Kept in step with that module so both probes target one host.
60
+ function resolveBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
61
+ const host = String(configManager.get('controlPlane.host') ?? '127.0.0.1');
62
+ const port = Number(configManager.get('controlPlane.port') ?? 3421);
63
+ return `http://${host}:${Number.isFinite(port) ? port : 3421}`;
64
+ }
65
+
66
+ function readNumber(value: unknown): number {
67
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
68
+ }
69
+
70
+ /** Sum a counter bucket (label -> count), tolerating a missing/!object bucket. */
71
+ function sumBucket(value: unknown): number {
72
+ if (!value || typeof value !== 'object') return 0;
73
+ let total = 0;
74
+ for (const entry of Object.values(value as Record<string, unknown>)) total += readNumber(entry);
75
+ return total;
76
+ }
77
+
78
+ function narrowMetrics(body: unknown): CliConnectedHostMetrics | null {
79
+ if (!body || typeof body !== 'object') return null;
80
+ const root = body as Record<string, unknown>;
81
+ const counters = (root.counters ?? {}) as Record<string, unknown>;
82
+ const gauges = (root.gauges ?? {}) as Record<string, unknown>;
83
+ const http = (counters.http ?? {}) as Record<string, unknown>;
84
+ const llm = (counters.llm ?? {}) as Record<string, unknown>;
85
+ const auth = (counters.auth ?? {}) as Record<string, unknown>;
86
+ const transport = (counters.transport ?? {}) as Record<string, unknown>;
87
+ const sessions = (gauges.sessions ?? {}) as Record<string, unknown>;
88
+ const sse = (gauges.sse ?? {}) as Record<string, unknown>;
89
+ const telemetry = (gauges.telemetry ?? {}) as Record<string, unknown>;
90
+ const httpRequests = (http.requests ?? {}) as Record<string, unknown>;
91
+ const llmRequests = (llm.requests ?? {}) as Record<string, unknown>;
92
+ const authSuccess = (auth.success ?? {}) as Record<string, unknown>;
93
+ const authFailure = (auth.failure ?? {}) as Record<string, unknown>;
94
+ const telemetryBuffer = (telemetry.buffer ?? {}) as Record<string, unknown>;
95
+ return {
96
+ httpRequestsTotal: sumBucket(httpRequests.total),
97
+ llmRequestsTotal: sumBucket(llmRequests.total),
98
+ authSuccessTotal: readNumber(authSuccess.total),
99
+ authFailureTotal: readNumber(authFailure.total),
100
+ transportRetriesTotal: readNumber(transport.retries_total),
101
+ sessionsActive: readNumber(sessions.active),
102
+ sseSubscribers: readNumber(sse.subscribers),
103
+ telemetryBufferFill: readNumber(telemetryBuffer.fill),
104
+ };
105
+ }
106
+
107
+ export async function inspectConnectedHostMetrics(
108
+ options: CliConnectedHostMetricsInspectionOptions,
109
+ ): Promise<CliConnectedHostMetricsSnapshot> {
110
+ const route = '/api/runtime/metrics' as const;
111
+ const base = {
112
+ route,
113
+ methodId: 'runtime.metrics.get' as const,
114
+ requiredScope: 'read:telemetry' as const,
115
+ };
116
+ const baseUrl = resolveBaseUrl(options.configManager);
117
+ const token = readConnectedHostOperatorToken(options.homeDirectory);
118
+ if (!token.token) {
119
+ return { ...base, status: 'auth_required', statusCode: null, metrics: null, error: `No connected-host operator token found at ${token.path}` };
120
+ }
121
+
122
+ const controller = new AbortController();
123
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 1500);
124
+ try {
125
+ const response = await fetch(`${baseUrl}${route}`, {
126
+ headers: { authorization: `Bearer ${token.token}` },
127
+ signal: controller.signal,
128
+ });
129
+ const text = await response.text();
130
+ let body: unknown = text;
131
+ if (text.trim().length > 0) {
132
+ try {
133
+ body = JSON.parse(text) as unknown;
134
+ } catch {
135
+ body = text;
136
+ }
137
+ }
138
+
139
+ if (response.ok) {
140
+ const metrics = narrowMetrics(body);
141
+ return metrics
142
+ ? { ...base, status: 'ok', statusCode: response.status, metrics, error: null }
143
+ : { ...base, status: 'error', statusCode: response.status, metrics: null, error: 'Connected host returned a runtime metrics body Agent could not read.' };
144
+ }
145
+ // 403 is the token-lacks-read:telemetry case: report it plainly rather than
146
+ // rendering zeros. 401 is unauthenticated; 404 means the route/verb is not
147
+ // present on this host (older or incompatible daemon).
148
+ const status: CliConnectedHostMetricsStatus =
149
+ response.status === 403 ? 'scope_missing'
150
+ : response.status === 401 ? 'auth_required'
151
+ : response.status === 404 ? 'route_unavailable'
152
+ : 'error';
153
+ return {
154
+ ...base,
155
+ status,
156
+ statusCode: response.status,
157
+ metrics: null,
158
+ error: typeof body === 'string' && body.trim() ? body : `HTTP ${response.status}`,
159
+ };
160
+ } catch (error) {
161
+ return { ...base, status: 'unavailable', statusCode: null, metrics: null, error: summarizeError(error) };
162
+ } finally {
163
+ clearTimeout(timer);
164
+ }
165
+ }
@@ -5,7 +5,7 @@ import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromMode
5
5
  import { readOnboardingCheckMarkers } from '../runtime/onboarding/index.ts';
6
6
  import { GlobalNetworkTransportInstaller } from '@/runtime/index.ts';
7
7
  import { createShellPathService } from '@/runtime/index.ts';
8
- import { configureActivityLogger } from '@pellux/goodvibes-sdk/platform/utils';
8
+ import { configureActivityLogger, logger } from '@pellux/goodvibes-sdk/platform/utils';
9
9
  import {
10
10
  applyRuntimeCommandEndpointFlagOverrides,
11
11
  applyRuntimeConfigOverrides,
@@ -24,7 +24,10 @@ import {
24
24
  } from './index.ts';
25
25
  import { buildCliServicePosture } from './service-posture.ts';
26
26
  import { inspectCliExternalRuntime } from './external-runtime.ts';
27
+ import { inspectConnectedHostMetrics } from './connected-host-metrics.ts';
27
28
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
29
+ import { readCheckpointRegistrationSetting } from '../config/checkpoint-settings.ts';
30
+ import { migrateLegacyWorkspaceRegistryIfNeeded, resolveWorkspaceRegistrationSync } from '../config/workspace-registration.ts';
28
31
  import { resolveAgentRuntimeProfileHome, resolveSelectedAgentRuntimeProfileHome } from '../agent/runtime-profile.ts';
29
32
 
30
33
  type ShellEntrypointOwnership = {
@@ -186,14 +189,34 @@ export async function prepareShellCliRuntime(
186
189
  configManager,
187
190
  homeDirectory: bootstrapHomeDirectory,
188
191
  });
192
+ // Only probe host metrics when the host is actually reachable — otherwise
193
+ // the unreachability is already reported in the connected-host block above,
194
+ // and skipping the call avoids a second connect-timeout wait. When reachable,
195
+ // the probe classifies token/scope/route state honestly (including the
196
+ // read:telemetry scope-missing case) rather than rendering zeros.
197
+ const connectedHostMetrics = externalRuntime.reachable
198
+ ? await inspectConnectedHostMetrics({
199
+ configManager,
200
+ homeDirectory: bootstrapHomeDirectory,
201
+ })
202
+ : undefined;
189
203
  const effectiveOperatorTokenPath = externalRuntime.operatorToken.present
190
204
  ? externalRuntime.operatorToken.path
191
205
  : operatorTokenPath;
206
+ const registrationMigration = migrateLegacyWorkspaceRegistryIfNeeded(shellPaths);
207
+ if (registrationMigration) {
208
+ logger.info('Migrated the local workspace registry into the shared registration store', { ...registrationMigration });
209
+ }
210
+ const checkpoints = {
211
+ workspaceRegistered: resolveWorkspaceRegistrationSync(shellPaths, bootstrapWorkingDir).status === 'covered',
212
+ unregisteredWorkspaceMode: readCheckpointRegistrationSetting(configManager),
213
+ };
192
214
  const statusOptions = {
193
215
  configManager,
194
216
  workingDirectory: bootstrapWorkingDir,
195
217
  homeDirectory: bootstrapHomeDirectory,
196
218
  onboardingMarkers,
219
+ checkpoints,
197
220
  auth: {
198
221
  userStorePath,
199
222
  userStorePresent: existsSync(userStorePath),
@@ -204,6 +227,7 @@ export async function prepareShellCliRuntime(
204
227
  },
205
228
  service,
206
229
  externalRuntime,
230
+ connectedHostMetrics,
207
231
  doctor: cli.command === 'doctor',
208
232
  outputFormat: cli.flags.outputFormat,
209
233
  };
@@ -0,0 +1,178 @@
1
+ import type {
2
+ AttemptCandidate,
3
+ AttemptJudgment,
4
+ AttemptPickResult,
5
+ HeldMergeGroup,
6
+ } from '@pellux/goodvibes-sdk/platform/orchestration';
7
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
8
+ import {
9
+ operatorFlagValue,
10
+ parseOperatorCommandArgs,
11
+ } from './operator-command-args.ts';
12
+ import { withRuntimeServices } from './management.ts';
13
+ import type { CliCommandOutput } from './types.ts';
14
+ import type { CliCommandRuntime } from './management.ts';
15
+
16
+ /**
17
+ * `goodvibes-agent fleet` — the best-of-N attempts admin surface (SDK 1.6.1
18
+ * orchestration engine held-merge groups). Unlike ci/principals/channel-
19
+ * profiles (which reach a REMOTE connected host over HTTP), fleet.attempts.*
20
+ * is ws-only on the wire (no HTTP binding — see method-catalog-fleet.ts) and
21
+ * reads THIS agent's own orchestration engine, which persists workstream
22
+ * state to disk (.goodvibes/orchestration/<workstreamId>.json). So this
23
+ * command goes through withRuntimeServices (the same in-process
24
+ * RuntimeServices construction `tasks`/`sessions` already use — see
25
+ * management.ts), not the operator-gateway-call HTTP path ci-command.ts
26
+ * uses: calling sdk.operator.invoke on a method with no `http` binding always
27
+ * throws (methodHttpRoute in @pellux/goodvibes-operator-sdk), so the HTTP
28
+ * path can never reach these three verbs regardless of connection health.
29
+ * services.orchestrationEngine is the exact same FleetAttemptsController
30
+ * instance the fleet.attempts.* gateway verb handlers are wired to in
31
+ * runtime/services.ts, so this CLI and the gateway verbs read/write
32
+ * identical state.
33
+ */
34
+
35
+ const FLEET_ATTEMPTS_PICK_USAGE = 'Usage: goodvibes-agent fleet attempts pick <groupId> <winnerItemId> --yes';
36
+ const FLEET_ATTEMPTS_JUDGE_USAGE = 'Usage: goodvibes-agent fleet attempts judge <groupId>';
37
+ const FLEET_USAGE = 'Usage: goodvibes-agent fleet attempts [list [--workstream <workstreamId>]|pick <groupId> <winnerItemId> --yes|judge <groupId>]';
38
+
39
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
40
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
41
+ }
42
+
43
+ function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
44
+ return {
45
+ output: jsonOrText(runtime, { ok: false, kind: 'invalid_fleet_command', error }, error),
46
+ exitCode: 2,
47
+ };
48
+ }
49
+
50
+ function errorFailure(runtime: CliCommandRuntime, error: unknown): CliCommandOutput {
51
+ const message = summarizeError(error);
52
+ return {
53
+ output: jsonOrText(runtime, { ok: false, kind: 'fleet_attempts_error', error: message }, message),
54
+ exitCode: 1,
55
+ };
56
+ }
57
+
58
+ /** The model proposal is clearly labeled at every point it renders — never presented as a decision. */
59
+ function renderJudgment(judgment: AttemptJudgment | null): readonly string[] {
60
+ if (!judgment) return [' judge proposal: none yet'];
61
+ const winner = judgment.proposedWinnerItemId ?? '(judge declined to choose)';
62
+ const lines = [
63
+ ` MODEL PROPOSAL (scored by ${judgment.model ?? 'unknown model'}, not a decision — confirm with fleet attempts pick)`,
64
+ ` proposed winner ${winner}`,
65
+ ];
66
+ for (const reason of judgment.reasons) lines.push(` reason: ${reason}`);
67
+ return lines;
68
+ }
69
+
70
+ function renderCandidate(candidate: AttemptCandidate): readonly string[] {
71
+ const lines = [
72
+ ` ${candidate.itemId} attempt #${candidate.attemptIndex} ${candidate.state} ${candidate.title}`,
73
+ ];
74
+ if (candidate.state === 'failed' && candidate.failureReason) {
75
+ lines.push(` failure: ${candidate.failureReason}`);
76
+ }
77
+ if (candidate.diff) {
78
+ lines.push(` diff: ${candidate.diff.files.length} file(s) — ${candidate.diff.stat}`);
79
+ }
80
+ const usage = candidate.usage;
81
+ const cost = usage.costState === 'priced' && usage.costUsd !== null ? `$${usage.costUsd.toFixed(4)}` : usage.costState;
82
+ lines.push(` usage: ${usage.turnCount} turn(s), ${usage.toolCallCount} tool call(s), cost ${cost}`);
83
+ return lines;
84
+ }
85
+
86
+ function renderGroup(group: HeldMergeGroup): string {
87
+ return [
88
+ ` ${group.groupId} workstream ${group.workstreamId} ${group.sourceTitle}`,
89
+ ` ready ${group.ready ? 'yes' : 'no'} autoAccept ${group.autoAccept ? 'yes' : 'no'} candidates ${group.candidates.length}`,
90
+ ...group.candidates.flatMap(renderCandidate),
91
+ ...renderJudgment(group.judgment),
92
+ ].join('\n');
93
+ }
94
+
95
+ function renderGroupList(groups: readonly HeldMergeGroup[]): string {
96
+ if (groups.length === 0) {
97
+ return [
98
+ 'Best-of-N held-merge groups',
99
+ ' No groups are currently held for a winner pick.',
100
+ ].join('\n');
101
+ }
102
+ return [`Best-of-N held-merge groups (${groups.length})`, ...groups.map(renderGroup)].join('\n\n');
103
+ }
104
+
105
+ async function handleFleetAttemptsList(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
106
+ const parsed = parseOperatorCommandArgs(args, ['workstream']);
107
+ const workstreamId = operatorFlagValue(parsed, 'workstream');
108
+ try {
109
+ const groups = await withRuntimeServices(runtime, (services) => services.orchestrationEngine.listHeldMergeGroups(workstreamId));
110
+ return { output: jsonOrText(runtime, { ok: true, groups }, renderGroupList(groups)), exitCode: 0 };
111
+ } catch (error) {
112
+ return errorFailure(runtime, error);
113
+ }
114
+ }
115
+
116
+ async function handleFleetAttemptsPick(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
117
+ const parsed = parseOperatorCommandArgs(args);
118
+ const groupId = parsed.positionals[0];
119
+ const winnerItemId = parsed.positionals[1];
120
+ if (!groupId || !winnerItemId) return usageFailure(runtime, FLEET_ATTEMPTS_PICK_USAGE);
121
+ if (!parsed.yes) {
122
+ return { output: `Refusing to pick a winner for group ${groupId} without --yes. Losing siblings' worktrees are cleaned on pick.`, exitCode: 2 };
123
+ }
124
+ try {
125
+ const result: AttemptPickResult = await withRuntimeServices(runtime, (services) => services.orchestrationEngine.pickAttemptWinner(groupId, winnerItemId));
126
+ const text = [
127
+ `Picked winner ${result.winnerItemId} for group ${result.groupId}`,
128
+ ` losers cleaned: ${result.loserItemIds.length === 0 ? 'none' : result.loserItemIds.join(', ')}`,
129
+ ` auto-picked ${result.auto ? 'yes (judge auto-accept)' : 'no (explicit operator pick)'}`,
130
+ ].join('\n');
131
+ return { output: jsonOrText(runtime, { ok: true, ...result }, text), exitCode: 0 };
132
+ } catch (error) {
133
+ return errorFailure(runtime, error);
134
+ }
135
+ }
136
+
137
+ async function handleFleetAttemptsJudge(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
138
+ const parsed = parseOperatorCommandArgs(args);
139
+ const groupId = parsed.positionals[0];
140
+ if (!groupId) return usageFailure(runtime, FLEET_ATTEMPTS_JUDGE_USAGE);
141
+ try {
142
+ const judgment: AttemptJudgment = await withRuntimeServices(runtime, (services) => services.orchestrationEngine.proposeAttemptWinner(groupId));
143
+ const text = [
144
+ `Judge proposal for group ${groupId}`,
145
+ ...renderJudgment(judgment),
146
+ '',
147
+ 'This is a model judgment, not a decision. Confirm with:',
148
+ ` goodvibes-agent fleet attempts pick ${groupId} <winnerItemId> --yes`,
149
+ ].join('\n');
150
+ return { output: jsonOrText(runtime, { ok: true, judgment }, text), exitCode: 0 };
151
+ } catch (error) {
152
+ return errorFailure(runtime, error);
153
+ }
154
+ }
155
+
156
+ async function handleFleetAttemptsCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
157
+ const [sub = 'list', ...rest] = args;
158
+ switch (sub.toLowerCase()) {
159
+ case 'list':
160
+ return handleFleetAttemptsList(runtime, rest);
161
+ case 'pick':
162
+ return handleFleetAttemptsPick(runtime, rest);
163
+ case 'judge':
164
+ return handleFleetAttemptsJudge(runtime, rest);
165
+ default:
166
+ return usageFailure(runtime, FLEET_USAGE);
167
+ }
168
+ }
169
+
170
+ export async function handleFleetCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
171
+ const [sub, ...rest] = runtime.cli.commandArgs;
172
+ switch ((sub ?? '').toLowerCase()) {
173
+ case 'attempts':
174
+ return handleFleetAttemptsCommand(runtime, rest);
175
+ default:
176
+ return usageFailure(runtime, FLEET_USAGE);
177
+ }
178
+ }
package/src/cli/help.ts CHANGED
@@ -56,6 +56,12 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
56
56
  ' skills Manage local Agent skills and skill bundles',
57
57
  ' memory Manage Agent-owned durable memory records',
58
58
  ' routines Inspect local routines and explicitly promote one to a connected schedule',
59
+ ' ci Check connected-host CI status and manage standing CI watches',
60
+ ' principals Manage the connected-host cross-channel principal identity registry',
61
+ ' channel-profiles Manage per-channel model/provider/permission-mode defaults',
62
+ ' workspaces Manage which workspaces get automatic checkpoints',
63
+ ' relay Report the connected host\'s outbound relay configuration (status|pair)',
64
+ ' fleet Review and resolve this Agent\'s best-of-N attempt groups',
59
65
  ' auth Inspect Agent auth posture and connection token state',
60
66
  ' compat Inspect connected-host compatibility and Agent Knowledge route readiness',
61
67
  ' knowledge Use isolated Agent Knowledge routes',
@@ -67,6 +73,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
67
73
  ' pair|qrcode Print companion pairing payload and QR code',
68
74
  ' bundle export|inspect|import',
69
75
  ' Move setup/profile/trust/support bundles',
76
+ ' import openclaw [path] Migrate an OpenClaw workspace (dry-run default, --apply to write)',
70
77
  ' completion <shell> Generate shell completion script',
71
78
  ' help [command] Print this help or command-specific help',
72
79
  ' version Print version',
@@ -112,6 +119,11 @@ export function renderGoodVibesHelp(binary = 'goodvibes-agent'): string {
112
119
  ` ${binary} memory add fact "Prefers concise morning briefings" --scope project --tags preference`,
113
120
  ` ${binary} memory search "morning briefings"`,
114
121
  ` ${binary} routines promote daily-operations-sweep --cron "0 9 * * *" --timezone America/Chicago --yes`,
122
+ ` ${binary} ci status my-org/my-repo --ref main`,
123
+ ` ${binary} ci watches create my-org/my-repo --ref main --delivery-channel slack:C123 --yes`,
124
+ ` ${binary} principals resolve --channel slack --value U123`,
125
+ ` ${binary} channel-profiles set slack --model openai:gpt-5.4 --permission-mode plan --yes`,
126
+ ` ${binary} fleet attempts list`,
115
127
  ` ${binary} compat`,
116
128
  ` ${binary} knowledge status`,
117
129
  ` ${binary} knowledge ask "What is GoodVibes Agent?"`,
@@ -290,6 +302,95 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
290
302
  summary: 'List, inspect, select, pin, and review model choices.',
291
303
  examples: ['models current', 'models openai', 'models use openai:gpt-5.4'],
292
304
  },
305
+ ci: {
306
+ usage: [
307
+ 'ci status <repo> [--ref <ref>] [--pr <number>]',
308
+ 'ci watches list',
309
+ 'ci watches create <repo> --delivery-channel <channel> [--ref <ref>] [--pr <number>] [--trigger-fix-session] --yes',
310
+ 'ci watches delete <watchId> --yes',
311
+ 'ci watches run <watchId>',
312
+ ],
313
+ summary: 'Check per-job CI status on the connected host and manage standing CI watches. Status is always rendered per job (name, status, conclusion), never collapsed to a single rollup.',
314
+ examples: [
315
+ 'ci status my-org/my-repo --ref main',
316
+ 'ci status my-org/my-repo --pr 42',
317
+ 'ci watches list',
318
+ 'ci watches create my-org/my-repo --ref main --delivery-channel slack:C123 --yes',
319
+ 'ci watches run watch-123',
320
+ 'ci watches delete watch-123 --yes',
321
+ ],
322
+ },
323
+ principals: {
324
+ usage: [
325
+ 'principals list',
326
+ 'principals get <id>',
327
+ 'principals create --name <name> --kind <user|bot|service|token> [--identity channel:value,channel:value] --yes',
328
+ 'principals update <id> [--name <name>] [--kind <kind>] [--identity channel:value,channel:value] --yes',
329
+ 'principals delete <id> --yes',
330
+ 'principals resolve --channel <channel> --value <value>',
331
+ ],
332
+ summary: 'Manage the connected-host cross-channel principal identity registry, mapping channel-specific sender identities (a Slack user id, an email address, a phone number) onto one named principal. resolve reports known: false rather than asserting a match when no identity is mapped.',
333
+ examples: [
334
+ 'principals list',
335
+ 'principals create --name "Mike Davis" --kind user --identity slack:U123,email:mike@example.com --yes',
336
+ 'principals resolve --channel slack --value U123',
337
+ 'principals delete principal-1 --yes',
338
+ ],
339
+ },
340
+ 'channel-profiles': {
341
+ usage: [
342
+ 'channel-profiles list',
343
+ 'channel-profiles get <surfaceKind> [--channel-id <id>]',
344
+ 'channel-profiles set <surfaceKind> [--channel-id <id>] [--model <model>] [--provider <provider>] [--permission-mode <plan|normal|accept-edits|auto>] --yes',
345
+ 'channel-profiles delete <surfaceKind> [--channel-id <id>] --yes',
346
+ ],
347
+ summary: 'Manage per-channel default model, provider, and permission-mode bindings applied to sessions that channel originates.',
348
+ examples: [
349
+ 'channel-profiles list',
350
+ 'channel-profiles set slack --model openai:gpt-5.4 --permission-mode plan --yes',
351
+ 'channel-profiles get slack',
352
+ 'channel-profiles delete slack --yes',
353
+ ],
354
+ },
355
+ workspaces: {
356
+ usage: [
357
+ 'workspaces list',
358
+ 'workspaces register [path] [--label <label>] --yes',
359
+ 'workspaces unregister [path] --yes',
360
+ ],
361
+ summary: 'Manage the registered-workspace list that gates automatic (turn-end/lifecycle) checkpoints. A workspace not in this list gets no automatic checkpoints; path defaults to the current working directory.',
362
+ examples: [
363
+ 'workspaces list',
364
+ 'workspaces register --yes',
365
+ 'workspaces register /home/mike/Projects/goodvibes-agent --label agent --yes',
366
+ 'workspaces unregister --yes',
367
+ ],
368
+ },
369
+ relay: {
370
+ usage: [
371
+ 'relay status',
372
+ 'relay pair',
373
+ ],
374
+ summary: 'Report the connected GoodVibes host\'s imported relay.* configuration and the relay-connect feature flag. Agent hosts no daemon itself, so this is not a live check, and relay pair honestly refuses — pairing payloads are minted by whichever daemon holds the relay identity key.',
375
+ examples: [
376
+ 'relay status',
377
+ 'relay pair',
378
+ ],
379
+ },
380
+ fleet: {
381
+ usage: [
382
+ 'fleet attempts list [--workstream <workstreamId>]',
383
+ 'fleet attempts pick <groupId> <winnerItemId> --yes',
384
+ 'fleet attempts judge <groupId>',
385
+ ],
386
+ summary: 'Review and resolve this Agent\'s own best-of-N attempt groups (orchestration engine state, disk-persisted, not a connected-host call): siblings that ran in isolated worktrees and are held for a winner pick instead of auto-merging. judge runs the optional judge model and PROPOSES a winner (clearly labeled as a model proposal, never an auto-decision); pick is the confirm-gated action that actually merges the winner and cleans losing worktrees.',
387
+ examples: [
388
+ 'fleet attempts list',
389
+ 'fleet attempts list --workstream workstream-1',
390
+ 'fleet attempts judge group-1',
391
+ 'fleet attempts pick group-1 item-2 --yes',
392
+ ],
393
+ },
293
394
  auth: {
294
395
  usage: ['auth', 'auth status', 'auth review', 'auth users', 'auth sessions'],
295
396
  summary: 'Inspect Agent auth posture and connection token state. Runtime user/session administration stays outside Agent.',
@@ -381,6 +482,15 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
381
482
  summary: 'Print companion pairing connection details and a QR code.',
382
483
  examples: ['pair', 'qrcode'],
383
484
  },
485
+ import: {
486
+ usage: ['import openclaw [path] [--apply]'],
487
+ summary: 'Migrate an OpenClaw workspace into the Agent registries. Dry-run is the default; it prints the personas, memory records, skills, and permission allowlist categories that would be created, plus any skipped files with reasons. Re-run with --apply to write through the persona registry, the canonical memory store, the skill registry, and the permission settings. Reads ~/.openclaw by default, or a supplied path.',
488
+ examples: [
489
+ 'import openclaw',
490
+ 'import openclaw ~/.openclaw',
491
+ 'import openclaw ~/.openclaw --apply',
492
+ ],
493
+ },
384
494
  completion: {
385
495
  usage: ['completion <bash|zsh|fish>'],
386
496
  summary: 'Generate shell completion scripts.',
@@ -401,6 +511,10 @@ const HELP_ALIASES: Record<string, string> = {
401
511
  'agent-skills': 'skills',
402
512
  memories: 'memory',
403
513
  routine: 'routines',
514
+ principal: 'principals',
515
+ 'channel-profile': 'channel-profiles',
516
+ workspace: 'workspaces',
517
+ fleets: 'fleet',
404
518
  know: 'knowledge',
405
519
  kb: 'knowledge',
406
520
  find: 'search',
@@ -412,6 +526,7 @@ const HELP_ALIASES: Record<string, string> = {
412
526
  bundles: 'bundle',
413
527
  qrcode: 'pair',
414
528
  qr: 'pair',
529
+ migrate: 'import',
415
530
  completions: 'completion',
416
531
  };
417
532