@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,230 @@
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
+ operatorRequiredFlag,
10
+ parseIdentityPairs,
11
+ parseOperatorCommandArgs,
12
+ } from './operator-command-args.ts';
13
+ import type { CliCommandOutput } from './types.ts';
14
+ import type { CliCommandRuntime } from './management.ts';
15
+ import { appendTemporalLabel } from './temporal-label.ts';
16
+
17
+ const PRINCIPALS_ROUTE = '/api/principals';
18
+ const PRINCIPAL_ROUTE = '/api/principals/{principalId}';
19
+ const PRINCIPALS_RESOLVE_ROUTE = '/api/principals/resolve';
20
+
21
+ const PRINCIPALS_GET_USAGE = 'Usage: goodvibes-agent principals get <id>';
22
+ const PRINCIPALS_CREATE_USAGE = 'Usage: goodvibes-agent principals create --name <name> --kind <user|bot|service|token> [--identity channel:value,channel:value] --yes';
23
+ const PRINCIPALS_UPDATE_USAGE = 'Usage: goodvibes-agent principals update <id> [--name <name>] [--kind <user|bot|service|token>] [--identity channel:value,channel:value] --yes';
24
+ const PRINCIPALS_DELETE_USAGE = 'Usage: goodvibes-agent principals delete <id> --yes';
25
+ const PRINCIPALS_RESOLVE_USAGE = 'Usage: goodvibes-agent principals resolve --channel <channel> --value <value>';
26
+ const PRINCIPALS_USAGE = 'Usage: goodvibes-agent principals [list|get <id>|create --name <name> --kind <user|bot|service|token> --yes|update <id> [--name ..] [--kind ..] --yes|delete <id> --yes|resolve --channel <channel> --value <value>]';
27
+
28
+ const PRINCIPAL_KINDS = new Set(['user', 'bot', 'service', 'token']);
29
+
30
+ type Principal = OperatorMethodOutput<'principals.list'>['principals'][number];
31
+ type PrincipalKind = OperatorMethodInput<'principals.create'>['kind'];
32
+
33
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
34
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
35
+ }
36
+
37
+ function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
38
+ return {
39
+ output: jsonOrText(runtime, { ok: false, kind: 'invalid_principals_command', error }, error),
40
+ exitCode: 2,
41
+ };
42
+ }
43
+
44
+ function requirePrincipalKind(value: string, usage: string): PrincipalKind {
45
+ const normalized = value.trim().toLowerCase();
46
+ if (!PRINCIPAL_KINDS.has(normalized)) throw new Error(`${usage}\n--kind must be one of: user, bot, service, token.`);
47
+ return normalized as PrincipalKind;
48
+ }
49
+
50
+ function renderPrincipal(principal: Principal): string {
51
+ const identities = principal.identities.length > 0
52
+ ? principal.identities.map((identity) => `${identity.channel}:${identity.value}`).join(', ')
53
+ : '(none)';
54
+ return [
55
+ `Principal ${principal.id}`,
56
+ ` name ${principal.name}`,
57
+ ` kind ${principal.kind}`,
58
+ ` identities ${identities}`,
59
+ ` created ${appendTemporalLabel(new Date(principal.createdAt).toISOString(), principal.createdAt)}`,
60
+ ` updated ${appendTemporalLabel(new Date(principal.updatedAt).toISOString(), principal.updatedAt)}`,
61
+ ].join('\n');
62
+ }
63
+
64
+ function renderPrincipalLine(principal: Principal): string {
65
+ const identities = principal.identities.length > 0
66
+ ? principal.identities.map((identity) => `${identity.channel}:${identity.value}`).join(', ')
67
+ : '(none)';
68
+ return ` ${principal.id} ${principal.kind.padEnd(8)} ${principal.name} identities ${identities}`;
69
+ }
70
+
71
+ function renderPrincipalList(principals: readonly Principal[]): string {
72
+ if (principals.length === 0) {
73
+ return [
74
+ 'Principals',
75
+ ' No principals registered yet.',
76
+ ' next goodvibes-agent principals create --name <name> --kind user --yes',
77
+ ].join('\n');
78
+ }
79
+ return [`Principals (${principals.length})`, ...principals.map(renderPrincipalLine)].join('\n');
80
+ }
81
+
82
+ async function handlePrincipalsList(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
83
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
84
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.list'>>(connection, 'principals.list', PRINCIPALS_ROUTE, {});
85
+ if (!result.ok) {
86
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
87
+ }
88
+ return { output: jsonOrText(runtime, result, renderPrincipalList(result.data.principals)), exitCode: 0 };
89
+ }
90
+
91
+ async function handlePrincipalsGet(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
92
+ const parsed = parseOperatorCommandArgs(args);
93
+ const principalId = parsed.positionals[0];
94
+ if (!principalId) return usageFailure(runtime, PRINCIPALS_GET_USAGE);
95
+ const payload: OperatorMethodInput<'principals.get'> = { principalId };
96
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
97
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.get'>>(connection, 'principals.get', PRINCIPAL_ROUTE, payload);
98
+ if (!result.ok) {
99
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
100
+ }
101
+ return { output: jsonOrText(runtime, result, renderPrincipal(result.data.principal)), exitCode: 0 };
102
+ }
103
+
104
+ async function handlePrincipalsCreate(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
105
+ const parsed = parseOperatorCommandArgs(args, ['name', 'kind', 'identity']);
106
+ let name: string;
107
+ let kind: PrincipalKind;
108
+ let identities: readonly { readonly channel: string; readonly value: string }[];
109
+ try {
110
+ name = operatorRequiredFlag(parsed, 'name', PRINCIPALS_CREATE_USAGE);
111
+ kind = requirePrincipalKind(operatorRequiredFlag(parsed, 'kind', PRINCIPALS_CREATE_USAGE), PRINCIPALS_CREATE_USAGE);
112
+ identities = parseIdentityPairs(operatorFlagValue(parsed, 'identity'));
113
+ } catch (error) {
114
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
115
+ }
116
+ if (!parsed.yes) {
117
+ return { output: `Refusing to create principal ${name} without --yes.`, exitCode: 2 };
118
+ }
119
+ const payload: OperatorMethodInput<'principals.create'> = {
120
+ name,
121
+ kind,
122
+ identities: identities.length > 0 ? identities : undefined,
123
+ };
124
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
125
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.create'>>(connection, 'principals.create', PRINCIPALS_ROUTE, payload);
126
+ if (!result.ok) {
127
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
128
+ }
129
+ return {
130
+ output: jsonOrText(runtime, result, [`Created principal ${result.data.principal.id}`, renderPrincipal(result.data.principal)].join('\n')),
131
+ exitCode: 0,
132
+ };
133
+ }
134
+
135
+ async function handlePrincipalsUpdate(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
136
+ const parsed = parseOperatorCommandArgs(args, ['name', 'kind', 'identity']);
137
+ const principalId = parsed.positionals[0];
138
+ if (!principalId) return usageFailure(runtime, PRINCIPALS_UPDATE_USAGE);
139
+ let kind: PrincipalKind | undefined;
140
+ let identities: readonly { readonly channel: string; readonly value: string }[];
141
+ try {
142
+ const kindValue = operatorFlagValue(parsed, 'kind');
143
+ kind = kindValue ? requirePrincipalKind(kindValue, PRINCIPALS_UPDATE_USAGE) : undefined;
144
+ identities = parseIdentityPairs(operatorFlagValue(parsed, 'identity'));
145
+ } catch (error) {
146
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
147
+ }
148
+ if (!parsed.yes) {
149
+ return { output: `Refusing to update principal ${principalId} without --yes.`, exitCode: 2 };
150
+ }
151
+ const payload: OperatorMethodInput<'principals.update'> = {
152
+ principalId,
153
+ name: operatorFlagValue(parsed, 'name'),
154
+ kind,
155
+ identities: identities.length > 0 ? identities : undefined,
156
+ };
157
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
158
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.update'>>(connection, 'principals.update', PRINCIPAL_ROUTE, payload);
159
+ if (!result.ok) {
160
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
161
+ }
162
+ return {
163
+ output: jsonOrText(runtime, result, [`Updated principal ${result.data.principal.id}`, renderPrincipal(result.data.principal)].join('\n')),
164
+ exitCode: 0,
165
+ };
166
+ }
167
+
168
+ async function handlePrincipalsDelete(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
169
+ const parsed = parseOperatorCommandArgs(args);
170
+ const principalId = parsed.positionals[0];
171
+ if (!principalId) return usageFailure(runtime, PRINCIPALS_DELETE_USAGE);
172
+ if (!parsed.yes) {
173
+ return { output: `Refusing to delete principal ${principalId} without --yes.`, exitCode: 2 };
174
+ }
175
+ const payload: OperatorMethodInput<'principals.delete'> = { principalId };
176
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
177
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.delete'>>(connection, 'principals.delete', PRINCIPAL_ROUTE, payload);
178
+ if (!result.ok) {
179
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
180
+ }
181
+ return {
182
+ output: jsonOrText(runtime, result, `Deleted principal ${result.data.principalId}: ${result.data.deleted ? 'ok' : 'not found'}`),
183
+ exitCode: result.data.deleted ? 0 : 1,
184
+ };
185
+ }
186
+
187
+ async function handlePrincipalsResolve(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
188
+ const parsed = parseOperatorCommandArgs(args, ['channel', 'value']);
189
+ let channel: string;
190
+ let value: string;
191
+ try {
192
+ channel = operatorRequiredFlag(parsed, 'channel', PRINCIPALS_RESOLVE_USAGE);
193
+ value = operatorRequiredFlag(parsed, 'value', PRINCIPALS_RESOLVE_USAGE);
194
+ } catch (error) {
195
+ return usageFailure(runtime, error instanceof Error ? error.message : String(error));
196
+ }
197
+ const payload: OperatorMethodInput<'principals.resolve'> = { channel, value };
198
+ const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
199
+ const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'principals.resolve'>>(connection, 'principals.resolve', PRINCIPALS_RESOLVE_ROUTE, payload);
200
+ if (!result.ok) {
201
+ return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
202
+ }
203
+ // known: false means the identity lookup found nothing for this channel/value — never assert a match in that case.
204
+ const text = result.data.known
205
+ ? [`known: true`, renderPrincipal(result.data.principal)].join('\n')
206
+ : `known: false\n No principal is mapped to ${channel}:${value}.`;
207
+ return { output: jsonOrText(runtime, result, text), exitCode: 0 };
208
+ }
209
+
210
+ export async function handlePrincipalsCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
211
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
212
+ switch (sub.toLowerCase()) {
213
+ case 'list':
214
+ return handlePrincipalsList(runtime);
215
+ case 'get':
216
+ case 'show':
217
+ return handlePrincipalsGet(runtime, rest);
218
+ case 'create':
219
+ return handlePrincipalsCreate(runtime, rest);
220
+ case 'update':
221
+ return handlePrincipalsUpdate(runtime, rest);
222
+ case 'delete':
223
+ case 'remove':
224
+ return handlePrincipalsDelete(runtime, rest);
225
+ case 'resolve':
226
+ return handlePrincipalsResolve(runtime, rest);
227
+ default:
228
+ return usageFailure(runtime, PRINCIPALS_USAGE);
229
+ }
230
+ }
@@ -18,6 +18,7 @@ import {
18
18
  } from '../agent/runtime-profile.ts';
19
19
  import { formatAgentRecordSource } from '../agent/record-labels.ts';
20
20
  import type { CliCommandOutput, GoodVibesCliParseResult } from './types.ts';
21
+ import { appendTemporalLabel } from './temporal-label.ts';
21
22
 
22
23
  interface ProfilesCommandRuntime {
23
24
  readonly cli: GoodVibesCliParseResult;
@@ -67,7 +68,7 @@ function parseCsvFlag(args: readonly string[], names: readonly string[]): readon
67
68
  }
68
69
 
69
70
  function profileLine(profile: AgentRuntimeProfileInfo): string {
70
- const created = profile.createdAt ? ` created ${profile.createdAt}` : '';
71
+ const created = profile.createdAt ? ` created ${appendTemporalLabel(profile.createdAt, profile.createdAt)}` : '';
71
72
  const starter = profile.starterTemplateId ? ` starter=${profile.starterTemplateId}` : '';
72
73
  return ` ${profile.id} home ${profile.homeDirectory}${created}${starter}`;
73
74
  }
@@ -0,0 +1,100 @@
1
+ import { FEATURE_FLAG_MAP } from '@pellux/goodvibes-sdk/platform/runtime/state';
2
+ import type { CliCommandOutput } from './types.ts';
3
+ import type { CliCommandRuntime } from './management.ts';
4
+
5
+ /**
6
+ * `goodvibes-agent relay status|pair` — the outbound zero-knowledge relay
7
+ * (SDK 1.6.1, platform/relay) that lets a daemon register with a
8
+ * self-hostable rendezvous relay so surfaces can reach it from outside the
9
+ * LAN. Relay reachability (RelayReachability, the live registration
10
+ * lifecycle, and the relay identity keypair a pairing payload is minted
11
+ * from) is owned entirely by the SDK's DaemonServer facade
12
+ * (buildDaemonRelayReachability, started at boot in facade.ts) — the daemon
13
+ * process holding the relay identity's private key material.
14
+ *
15
+ * GoodVibes Agent does not host that daemon. Its product boundary is
16
+ * explicit and enforced elsewhere in this CLI (see parser.ts's
17
+ * BLOCKED_PRODUCT_COMMAND_HINTS: "GoodVibes Agent connects to an externally
18
+ * managed GoodVibes host and does not start or manage host processes" /
19
+ * "does not start listeners or expose inbound host endpoints"). Relay is the
20
+ * same shape of concern as those blocked commands, so this command reports
21
+ * what is honestly knowable from here — the connected host's imported
22
+ * relay.* configuration and the relay-connect feature flag's static
23
+ * definition — and refuses to fabricate a live registration status or a
24
+ * pairing payload neither of which this process can observe or mint. The
25
+ * SDK's operator wire ships no remote relay-status/pairing route yet (relay
26
+ * reuses the existing REST surface for tunneled traffic, not a status
27
+ * query), so there is no live check to perform even for a genuinely
28
+ * connected host.
29
+ */
30
+
31
+ const RELAY_USAGE = 'Usage: goodvibes-agent relay [status|pair]';
32
+
33
+ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
34
+ return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
35
+ }
36
+
37
+ function relayConnectFlagSummary(): { readonly id: string; readonly name: string; readonly defaultState: string } {
38
+ const flag = FEATURE_FLAG_MAP.get('relay-connect');
39
+ return {
40
+ id: 'relay-connect',
41
+ name: flag?.name ?? 'Outbound Zero-Knowledge Relay',
42
+ defaultState: flag?.defaultState ?? 'disabled',
43
+ };
44
+ }
45
+
46
+ function handleRelayStatus(runtime: CliCommandRuntime): CliCommandOutput {
47
+ const config = runtime.configManager;
48
+ const enabled = config.get('relay.enabled') === true;
49
+ const url = String(config.get('relay.url') ?? '');
50
+ const rendezvousId = String(config.get('relay.rendezvousId') ?? '');
51
+ const label = String(config.get('relay.label') ?? '');
52
+ const requireStepUpForMutations = config.get('relay.requireStepUpForMutations') === true;
53
+ const flag = relayConnectFlagSummary();
54
+
55
+ const payload = {
56
+ ok: true,
57
+ source: 'agent-local-config' as const,
58
+ liveVerified: false,
59
+ config: { enabled, url, rendezvousId, label, requireStepUpForMutations },
60
+ flag,
61
+ note: 'This reflects goodvibes-agent\'s own imported copy of relay.* settings, not a live check of the connected GoodVibes daemon\'s actual relay registration — Agent hosts no daemon and the SDK has no remote relay-status route yet.',
62
+ };
63
+
64
+ const lines = [
65
+ 'GoodVibes relay (connected-host configuration; not live-verified)',
66
+ ` relay.enabled: ${enabled ? 'true' : 'false'}`,
67
+ ` relay.url: ${url || '(empty)'}`,
68
+ ` relay.rendezvousId: ${rendezvousId || '(not generated yet)'}`,
69
+ ` relay.label: ${label || '(empty)'}`,
70
+ ` relay.requireStepUpForMutations: ${requireStepUpForMutations ? 'true' : 'false'}`,
71
+ ` ${flag.id} feature flag: ${flag.name} (default ${flag.defaultState})`,
72
+ '',
73
+ 'disabled/registered/offline is a property of the daemon that actually holds the relay',
74
+ 'identity and dials the relay server — that daemon is managed outside goodvibes-agent.',
75
+ 'Check its own status/doctor output, or the connected host directly.',
76
+ ];
77
+
78
+ return { output: jsonOrText(runtime, payload, lines.join('\n')), exitCode: 0 };
79
+ }
80
+
81
+ function handleRelayPair(runtime: CliCommandRuntime): CliCommandOutput {
82
+ const message = [
83
+ 'GoodVibes Agent cannot mint a relay pairing payload.',
84
+ 'A pairing payload (relay URL + rendezvous id + pinned daemon public key) is minted',
85
+ 'in-process by the daemon that holds the relay identity\'s private key — Agent never',
86
+ 'holds that key material because it does not host the daemon (see "goodvibes-agent',
87
+ 'relay status"). Run the pairing step on the machine hosting the connected GoodVibes',
88
+ 'daemon, or use that host\'s own relay pairing surface once it ships one.',
89
+ ].join('\n');
90
+ const payload = { ok: false, available: false, reason: 'relay-identity-not-hosted-here', error: message };
91
+ return { output: jsonOrText(runtime, payload, message), exitCode: 1 };
92
+ }
93
+
94
+ export function handleRelayCommand(runtime: CliCommandRuntime): CliCommandOutput {
95
+ const [sub = 'status'] = runtime.cli.commandArgs;
96
+ if (sub === 'status') return handleRelayStatus(runtime);
97
+ if (sub === 'pair' || sub === 'pairing' || sub === 'qr' || sub === 'qrcode') return handleRelayPair(runtime);
98
+ const message = `Unknown relay subcommand: ${sub}\n${RELAY_USAGE}`;
99
+ return { output: jsonOrText(runtime, { ok: false, error: message }, message), exitCode: 2 };
100
+ }
@@ -9,6 +9,7 @@ import {
9
9
  resolveAgentConnectedHostConnection,
10
10
  } from '../agent/routine-schedule-promotion.ts';
11
11
  import { parseRoutineSchedulePromotionArgs } from '../agent/routine-schedule-args.ts';
12
+ import { appendTemporalLabel } from './temporal-label.ts';
12
13
  import {
13
14
  formatRoutineScheduleCorrelation,
14
15
  formatRoutineScheduleFailure,
@@ -245,9 +246,9 @@ function renderRoutine(routine: AgentRoutineRecord): string {
245
246
  ` origin ${formatAgentRecordOrigin(routine.source, routine.provenance)}`,
246
247
  ` tags ${routine.tags.join(', ') || '(none)'}`,
247
248
  ` triggers ${routine.triggers.join(', ') || '(manual)'}`,
248
- ` started ${routine.startCount}${routine.lastStartedAt ? `; last ${routine.lastStartedAt}` : ''}`,
249
- ` created ${routine.createdAt}`,
250
- ` updated ${routine.updatedAt}`,
249
+ ` started ${routine.startCount}${routine.lastStartedAt ? `; last ${appendTemporalLabel(routine.lastStartedAt, routine.lastStartedAt)}` : ''}`,
250
+ ` created ${appendTemporalLabel(routine.createdAt, routine.createdAt)}`,
251
+ ` updated ${appendTemporalLabel(routine.updatedAt, routine.updatedAt)}`,
251
252
  routine.staleReason ? ` stale reason ${routine.staleReason}` : '',
252
253
  '',
253
254
  routine.description,
@@ -7,6 +7,7 @@ import {
7
7
  } from '../agent/skill-registry.ts';
8
8
  import type { CliCommandOutput } from './types.ts';
9
9
  import type { CliCommandRuntime } from './management.ts';
10
+ import { appendTemporalLabel } from './temporal-label.ts';
10
11
  import {
11
12
  csvOption,
12
13
  errorOutput,
@@ -60,8 +61,8 @@ function renderBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkil
60
61
  ` review: ${formatAgentRecordReviewState(bundle.reviewState)}`,
61
62
  ` origin: ${formatAgentRecordOrigin(bundle.source, bundle.provenance)}`,
62
63
  ` skills: ${bundle.skillIds.join(', ')}`,
63
- ` created: ${bundle.createdAt}`,
64
- ` updated: ${bundle.updatedAt}`,
64
+ ` created: ${appendTemporalLabel(bundle.createdAt, bundle.createdAt)}`,
65
+ ` updated: ${appendTemporalLabel(bundle.updatedAt, bundle.updatedAt)}`,
65
66
  bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
66
67
  '',
67
68
  bundle.description,
@@ -9,6 +9,7 @@ import {
9
9
  import type { CliCommandOutput } from './types.ts';
10
10
  import type { CliCommandRuntime } from './management.ts';
11
11
  import { handleSkillBundleCommand, renderBundleList } from './skill-bundle-command.ts';
12
+ import { appendTemporalLabel } from './temporal-label.ts';
12
13
  import {
13
14
  csvOption,
14
15
  errorOutput,
@@ -114,8 +115,8 @@ function renderSkill(skill: AgentSkillRecord): string {
114
115
  ` origin: ${formatAgentRecordOrigin(skill.source, skill.provenance)}`,
115
116
  ` tags: ${skill.tags.join(', ') || '(none)'}`,
116
117
  ` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
117
- ` created: ${skill.createdAt}`,
118
- ` updated: ${skill.updatedAt}`,
118
+ ` created: ${appendTemporalLabel(skill.createdAt, skill.createdAt)}`,
119
+ ` updated: ${appendTemporalLabel(skill.updatedAt, skill.updatedAt)}`,
119
120
  skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
120
121
  '',
121
122
  skill.description,
package/src/cli/status.ts CHANGED
@@ -4,10 +4,12 @@ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
4
4
  import type { GoodVibesCliOutputFormat } from './types.ts';
5
5
  import type { CliServicePosture } from './service-posture.ts';
6
6
  import type { CliExternalRuntimeSnapshot } from './external-runtime.ts';
7
+ import type { CliConnectedHostMetricsSnapshot } from './connected-host-metrics.ts';
7
8
  import { getProviderIdFromModel } from '../config/provider-model.ts';
8
9
  import { formatAgentRecordSource } from '../agent/record-labels.ts';
9
10
  import { formatAgentKnowledgeFailureKind } from './agent-knowledge-format.ts';
10
11
  import { computeApprovalPosture, type ApprovalPosture } from '../permissions/approval-posture.ts';
12
+ import { appendTemporalLabel } from './temporal-label.ts';
11
13
 
12
14
  export interface CliStatusOptions {
13
15
  readonly configManager: Pick<ConfigManager, 'get'>;
@@ -17,10 +19,23 @@ export interface CliStatusOptions {
17
19
  readonly auth?: CliAuthStatus;
18
20
  readonly service?: CliServicePosture;
19
21
  readonly externalRuntime?: CliExternalRuntimeSnapshot;
22
+ readonly connectedHostMetrics?: CliConnectedHostMetricsSnapshot;
23
+ readonly checkpoints?: CliCheckpointsStatus;
20
24
  readonly doctor?: boolean;
21
25
  readonly outputFormat?: GoodVibesCliOutputFormat;
22
26
  }
23
27
 
28
+ /**
29
+ * Registered-workspaces-only checkpoint posture (owner ruling, 2026-07-10; see
30
+ * ../config/workspace-registration.ts and ../runtime/services.ts). Computed by the
31
+ * caller (entrypoint.ts already builds a ShellPathService for status/doctor)
32
+ * and passed in here so this module stays a pure renderer.
33
+ */
34
+ export interface CliCheckpointsStatus {
35
+ readonly workspaceRegistered: boolean;
36
+ readonly unregisteredWorkspaceMode: 'off' | 'guarded';
37
+ }
38
+
24
39
  export interface CliAuthStatus {
25
40
  readonly userStorePath: string;
26
41
  readonly userStorePresent: boolean;
@@ -65,6 +80,13 @@ export interface CliStatusSnapshot {
65
80
  readonly lifecycle?: CliServicePosture;
66
81
  };
67
82
  readonly externalRuntime: CliExternalRuntimeSnapshot | null;
83
+ readonly connectedHostMetrics: CliConnectedHostMetricsSnapshot | null;
84
+ readonly relay: {
85
+ readonly enabled: boolean;
86
+ readonly url: string;
87
+ readonly requireStepUpForMutations: boolean;
88
+ readonly liveVerified: false;
89
+ };
68
90
  readonly runtimeEndpoints: {
69
91
  readonly controlPlane: ReturnType<typeof resolveRuntimeEndpointBinding> & { readonly enabled: unknown };
70
92
  readonly httpListener: ReturnType<typeof resolveRuntimeEndpointBinding> & { readonly enabled: unknown };
@@ -75,13 +97,70 @@ export interface CliStatusSnapshot {
75
97
  readonly scope: string;
76
98
  readonly updatedAt: number | null;
77
99
  };
100
+ readonly checkpoints: {
101
+ readonly automaticCheckpointsActive: boolean;
102
+ readonly workspaceRegistered: boolean;
103
+ readonly unregisteredWorkspaceMode: 'off' | 'guarded';
104
+ readonly statusLine: string;
105
+ } | null;
78
106
  readonly findings: readonly CliDoctorFinding[];
79
107
  }
80
108
 
109
+ /**
110
+ * The one honest line this surface must never omit or soften (owner ruling,
111
+ * 2026-07-10): whether automatic checkpoints are active for THIS working
112
+ * directory, and why. `"checkpoints off: workspace not registered"` is the
113
+ * literal wording the ruling calls for the default (unregistered, `"off"`) case.
114
+ */
115
+ function checkpointsStatusLine(checkpoints: CliCheckpointsStatus): { active: boolean; line: string } {
116
+ if (checkpoints.workspaceRegistered) {
117
+ return { active: true, line: 'checkpoints on: workspace registered' };
118
+ }
119
+ if (checkpoints.unregisteredWorkspaceMode === 'guarded') {
120
+ return { active: true, line: 'checkpoints on: unregistered workspace allowed via checkpoints.unregisteredWorkspaces="guarded"' };
121
+ }
122
+ return { active: false, line: 'checkpoints off: workspace not registered' };
123
+ }
124
+
81
125
  function yesNo(value: unknown): string {
82
126
  return value === true ? 'yes' : 'no';
83
127
  }
84
128
 
129
+ /**
130
+ * The connected-host runtime-metrics lines. Each non-ok state is rendered as a
131
+ * plain sentence naming exactly why there are no numbers to show — most
132
+ * importantly, a token without the read:telemetry scope reads as "not
133
+ * permitted", never as a wall of zeros that would misrepresent an unauthorized
134
+ * token as a healthy idle host.
135
+ */
136
+ function connectedHostMetricsLines(snapshot: CliConnectedHostMetricsSnapshot | null): string[] {
137
+ if (!snapshot) return [' live check unavailable'];
138
+ switch (snapshot.status) {
139
+ case 'ok': {
140
+ const m = snapshot.metrics!;
141
+ return [
142
+ ` http requests total: ${m.httpRequestsTotal}`,
143
+ ` llm requests total: ${m.llmRequestsTotal}`,
144
+ ` auth success/failure: ${m.authSuccessTotal}/${m.authFailureTotal}`,
145
+ ` transport retries total: ${m.transportRetriesTotal}`,
146
+ ` sessions active: ${m.sessionsActive}`,
147
+ ` sse subscribers: ${m.sseSubscribers}`,
148
+ ` telemetry buffer fill: ${m.telemetryBufferFill}`,
149
+ ];
150
+ }
151
+ case 'scope_missing':
152
+ return [` not shown: the connected-host token does not include the ${snapshot.requiredScope} scope required by ${snapshot.methodId}.`];
153
+ case 'auth_required':
154
+ return [' not shown: connected-host operator token is missing or unauthenticated.'];
155
+ case 'route_unavailable':
156
+ return [` not shown: this connected host does not expose ${snapshot.route} (incompatible or older daemon).`];
157
+ case 'unavailable':
158
+ return [` not shown: connected host is not reachable${snapshot.error ? ` (${snapshot.error})` : '.'}`];
159
+ default:
160
+ return [` not shown: ${snapshot.error ?? 'runtime metrics could not be read.'}`];
161
+ }
162
+ }
163
+
85
164
  /** permissions.tools.* keys — mirrors PermissionsToolConfig in the SDK config schema. */
86
165
  const PERMISSION_TOOL_KEYS = [
87
166
  'read', 'write', 'edit', 'exec', 'find', 'fetch', 'analyze',
@@ -268,6 +347,13 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
268
347
  ...(options.service ? { lifecycle: options.service } : {}),
269
348
  },
270
349
  externalRuntime: options.externalRuntime ?? null,
350
+ connectedHostMetrics: options.connectedHostMetrics ?? null,
351
+ relay: {
352
+ enabled: config.get('relay.enabled') === true,
353
+ url: String(config.get('relay.url') ?? ''),
354
+ requireStepUpForMutations: config.get('relay.requireStepUpForMutations') === true,
355
+ liveVerified: false,
356
+ },
271
357
  runtimeEndpoints: {
272
358
  controlPlane: { enabled: config.get('controlPlane.enabled'), ...controlPlaneBinding },
273
359
  httpListener: { enabled: config.get('danger.httpListener'), ...httpListenerBinding },
@@ -278,6 +364,14 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
278
364
  scope: marker?.scope ?? 'none',
279
365
  updatedAt: marker?.payload?.updatedAt ?? null,
280
366
  },
367
+ checkpoints: options.checkpoints
368
+ ? {
369
+ automaticCheckpointsActive: checkpointsStatusLine(options.checkpoints).active,
370
+ workspaceRegistered: options.checkpoints.workspaceRegistered,
371
+ unregisteredWorkspaceMode: options.checkpoints.unregisteredWorkspaceMode,
372
+ statusLine: checkpointsStatusLine(options.checkpoints).line,
373
+ }
374
+ : null,
281
375
  findings,
282
376
  };
283
377
  }
@@ -327,6 +421,14 @@ export function renderCliStatus(options: CliStatusOptions): string {
327
421
  ' live check unavailable',
328
422
  ]),
329
423
  '',
424
+ 'Connected host metrics (runtime.metrics.get; read:telemetry):',
425
+ ...connectedHostMetricsLines(snapshot.connectedHostMetrics),
426
+ '',
427
+ 'Relay (connected host\'s imported config; not live-verified — see goodvibes-agent relay status):',
428
+ ` relay.enabled: ${yesNo(snapshot.relay.enabled)}`,
429
+ ` relay.url: ${snapshot.relay.url || '(empty)'}`,
430
+ ` relay.requireStepUpForMutations: ${yesNo(snapshot.relay.requireStepUpForMutations)}`,
431
+ '',
330
432
  'Agent role:',
331
433
  ' product: interactive operator TUI',
332
434
  ' host lifecycle: external',
@@ -337,7 +439,17 @@ export function renderCliStatus(options: CliStatusOptions): string {
337
439
  'Onboarding',
338
440
  ` checked ${marker?.exists ? 'yes' : 'no'}`,
339
441
  ` scope ${marker?.scope ?? 'none'}`,
340
- ` updated ${marker?.payload ? new Date(marker.payload.updatedAt).toISOString() : 'n/a'}`,
442
+ ` updated ${marker?.payload ? appendTemporalLabel(new Date(marker.payload.updatedAt).toISOString(), marker.payload.updatedAt) : 'n/a'}`,
443
+ '',
444
+ 'Checkpoints',
445
+ ...(snapshot.checkpoints ? [
446
+ ` ${snapshot.checkpoints.statusLine}`,
447
+ ` unregisteredWorkspaces mode: ${snapshot.checkpoints.unregisteredWorkspaceMode}`,
448
+ ' next goodvibes-agent workspaces list',
449
+ ...(snapshot.checkpoints.workspaceRegistered ? [] : [' next goodvibes-agent workspaces register --yes']),
450
+ ] : [
451
+ ' checkpoint posture unknown',
452
+ ]),
341
453
  ];
342
454
 
343
455
  if (options.doctor) {
@@ -375,7 +487,7 @@ export function renderOnboardingCliStatus(options: CliStatusOptions): string {
375
487
  ` scope ${marker?.scope ?? 'none'}`,
376
488
  ` origin ${marker?.payload?.source ? formatAgentRecordSource(marker.payload.source) : 'n/a'}`,
377
489
  ` mode ${marker?.payload?.mode ?? 'n/a'}`,
378
- ` updated ${marker?.payload ? new Date(marker.payload.updatedAt).toISOString() : 'n/a'}`,
490
+ ` updated ${marker?.payload ? appendTemporalLabel(new Date(marker.payload.updatedAt).toISOString(), marker.payload.updatedAt) : 'n/a'}`,
379
491
  ` working directory ${options.workingDirectory}`,
380
492
  ].join('\n');
381
493
  }