@pellux/goodvibes-tui 1.13.1 → 1.14.0

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 (158) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +363 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +79 -0
  141. package/src/runtime/services.ts +33 -33
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/gateway-verbs.ts +0 -47
  158. package/src/runtime/render-scheduler.ts +0 -90
@@ -4,6 +4,7 @@ import type { CommandRegistry } from '../command-registry.ts';
4
4
  import { VERSION } from '../../version.ts';
5
5
  import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
6
6
  import { handleLocalAuthCommand } from './local-auth-runtime.ts';
7
+ import { registerUpdateCommand } from './update-runtime.ts';
7
8
  import { buildAuthInspectionSnapshot, inspectProviderAuth } from '@/runtime/index.ts';
8
9
  import { requireProfileManager, requireSecretsManager, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
9
10
 
@@ -18,16 +19,6 @@ interface InstallBundle {
18
19
  readonly setupLinks: readonly string[];
19
20
  }
20
21
 
21
- interface UpdateBundle {
22
- readonly version: 1;
23
- readonly exportedAt: number;
24
- readonly appVersion: string;
25
- readonly updateChannel: 'stable' | 'preview';
26
- readonly subscriptionProviders: readonly string[];
27
- readonly sandboxProfile: string;
28
- readonly notes: readonly string[];
29
- }
30
-
31
22
  interface AuthReviewBundle {
32
23
  readonly version: 1;
33
24
  readonly exportedAt: number;
@@ -54,17 +45,6 @@ function inspectInstallBundle(bundle: InstallBundle): string {
54
45
  ].join('\n');
55
46
  }
56
47
 
57
- function inspectUpdateBundle(bundle: UpdateBundle): string {
58
- return [
59
- 'Update Bundle Review',
60
- ` appVersion: ${bundle.appVersion}`,
61
- ` updateChannel: ${bundle.updateChannel}`,
62
- ` subscriptionProviders: ${bundle.subscriptionProviders.length}`,
63
- ` sandboxProfile: ${bundle.sandboxProfile}`,
64
- ` notes: ${bundle.notes.length}`,
65
- ].join('\n');
66
- }
67
-
68
48
  function inspectAuthBundle(bundle: AuthReviewBundle): string {
69
49
  return [
70
50
  'Auth Review Bundle',
@@ -189,82 +169,7 @@ export function registerPlatformAccessRuntimeCommands(registry: CommandRegistry)
189
169
  },
190
170
  });
191
171
 
192
- registry.register({
193
- name: 'update',
194
- aliases: ['upgrade'],
195
- description: 'Review update posture, choose release channel guidance, and package portable update bundles',
196
- usage: '[review|channel <stable|preview>|bundle export <path>|bundle inspect <path>]',
197
- handler(args, ctx) {
198
- const shellPaths = requireShellPaths(ctx);
199
- const sub = args[0] ?? 'review';
200
- const subscriptions = requireSubscriptionManager(ctx);
201
- const serviceRegistry = requireServiceRegistry(ctx);
202
- const secretsManager = requireSecretsManager(ctx);
203
- const builtinProviders = listBuiltinSubscriptionProviders().map((entry) => entry.provider);
204
- const sandboxProfile = [
205
- `${ctx.platform.configManager.get('sandbox.replIsolation')}`,
206
- `${ctx.platform.configManager.get('sandbox.mcpIsolation')}`,
207
- `${ctx.platform.configManager.get('sandbox.vmBackend')}`,
208
- ].join('/');
209
- const activeSubscriptions = subscriptions.list().map((entry) => entry.provider);
210
- if (sub === 'review') {
211
- const channel = ctx.platform.configManager.get('release.channel');
212
- ctx.print([
213
- 'Update Review',
214
- ` version: ${VERSION}`,
215
- ` channel: ${channel}`,
216
- ` built-in subscription providers: ${builtinProviders.length}${builtinProviders.length > 0 ? ` (${builtinProviders.join(', ')})` : ''}`,
217
- ` active subscriptions: ${activeSubscriptions.length}${activeSubscriptions.length > 0 ? ` (${activeSubscriptions.join(', ')})` : ''}`,
218
- ` sandbox profile: ${sandboxProfile}`,
219
- ' use /update channel <stable|preview> to change release posture',
220
- ].join('\n'));
221
- return;
222
- }
223
- if (sub === 'channel') {
224
- const channel = args[1];
225
- if (channel !== 'stable' && channel !== 'preview') {
226
- ctx.print('Usage: /update channel <stable|preview>');
227
- return;
228
- }
229
- ctx.platform.configManager.setDynamic('release.channel', channel);
230
- ctx.print(`Update channel set to ${channel}.`);
231
- return;
232
- }
233
- if (sub === 'bundle') {
234
- const mode = args[1];
235
- const pathArg = args[2];
236
- if ((mode === 'export' || mode === 'inspect') && !pathArg) {
237
- ctx.print(`Usage: /update bundle ${mode} <path>`);
238
- return;
239
- }
240
- const targetPath = shellPaths.resolveWorkspacePath(pathArg!);
241
- if (mode === 'export') {
242
- const bundle: UpdateBundle = {
243
- version: 1,
244
- exportedAt: Date.now(),
245
- appVersion: VERSION,
246
- updateChannel: ctx.platform.configManager.get('release.channel') as 'stable' | 'preview',
247
- subscriptionProviders: [...new Set([...builtinProviders, ...activeSubscriptions])],
248
- sandboxProfile,
249
- notes: [
250
- 'Preview channel is recommended only when operator review and sandbox isolation are enabled.',
251
- 'OAuth-backed provider subscriptions survive channel changes and continue to apply to supported provider surfaces.',
252
- ],
253
- };
254
- mkdirSync(dirname(targetPath), { recursive: true });
255
- writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
256
- ctx.print(`Update bundle exported to ${targetPath}`);
257
- return;
258
- }
259
- if (mode === 'inspect') {
260
- const bundle = JSON.parse(readFileSync(targetPath, 'utf-8')) as UpdateBundle;
261
- ctx.print(inspectUpdateBundle(bundle));
262
- return;
263
- }
264
- }
265
- ctx.print('Usage: /update [review|channel <stable|preview>|bundle export <path>|bundle inspect <path>]');
266
- },
267
- });
172
+ registerUpdateCommand(registry);
268
173
 
269
174
  registry.register({
270
175
  name: 'auth',
@@ -0,0 +1,212 @@
1
+ /**
2
+ * principals-runtime.ts
3
+ *
4
+ * `/principals` — identity-mapping admin over `principals.*`: named
5
+ * principals (people, bots, services, tokens) and the channel identities
6
+ * ({channel, value} pairs, e.g. {channel:'slack', value:'U123'}) mapped to
7
+ * them. Goes over the operator wire (see operator-rpc.ts) for the same
8
+ * reason /ci and /checkin do: this verb family is not yet on the in-process
9
+ * OperatorClient facade.
10
+ *
11
+ * `principals.resolve` is the honest lookup this command exists to make
12
+ * reachable: an unmapped identity resolves to the shared "unknown" principal
13
+ * with `known: false` — the registry never guesses, and this command never
14
+ * papers over that with a name that looks real.
15
+ */
16
+ import type { CommandRegistry } from '../command-registry.ts';
17
+ import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
18
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
19
+
20
+ type Principal = OperatorMethodOutput<'principals.list'>['principals'][number];
21
+ type PrincipalKind = Principal['kind'];
22
+
23
+ const PRINCIPAL_KINDS: readonly PrincipalKind[] = ['user', 'bot', 'service', 'token'];
24
+
25
+ function isPrincipalKind(value: string | undefined): value is PrincipalKind {
26
+ return PRINCIPAL_KINDS.includes(value as PrincipalKind);
27
+ }
28
+
29
+ /** Parse `channel:value` tokens (e.g. `slack:U123`) into identity pairs. Skips malformed tokens. */
30
+ function parseIdentities(tokens: readonly string[]): Array<{ channel: string; value: string }> {
31
+ const identities: Array<{ channel: string; value: string }> = [];
32
+ for (const token of tokens) {
33
+ const idx = token.indexOf(':');
34
+ if (idx <= 0 || idx === token.length - 1) continue;
35
+ identities.push({ channel: token.slice(0, idx), value: token.slice(idx + 1) });
36
+ }
37
+ return identities;
38
+ }
39
+
40
+ /** Extract `--flag value` pairs from an args array, returning the remaining positional tokens. */
41
+ function extractFlags(args: readonly string[], flagNames: readonly string[]): { flags: Map<string, string>; positional: string[] } {
42
+ const flags = new Map<string, string>();
43
+ const positional: string[] = [];
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i]!;
46
+ const name = arg.startsWith('--') ? arg.slice(2) : null;
47
+ if (name && flagNames.includes(name)) {
48
+ const value = args[i + 1];
49
+ if (value !== undefined) {
50
+ flags.set(name, value);
51
+ i++;
52
+ continue;
53
+ }
54
+ }
55
+ positional.push(arg);
56
+ }
57
+ return { flags, positional };
58
+ }
59
+
60
+ export function renderPrincipal(principal: Principal): string {
61
+ const lines = [
62
+ ` ${principal.id} ${principal.name} [${principal.kind}]`,
63
+ ];
64
+ if (principal.identities.length > 0) {
65
+ lines.push(` identities: ${principal.identities.map((i) => `${i.channel}:${i.value}`).join(', ')}`);
66
+ } else {
67
+ lines.push(' identities: (none)');
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+
72
+ export function registerPrincipalsRuntimeCommands(registry: CommandRegistry): void {
73
+ registry.register({
74
+ name: 'principals',
75
+ description: 'Identity mappings: named principals and their channel identities',
76
+ usage: '[list|get <id>|create <name> <kind> [channel:value...]|update <id> [--name x] [--kind x] [--identities c:v,c:v]|delete <id>|resolve <channel> <value>]',
77
+ argsHint: '[list|get|create|update|delete|resolve]',
78
+ async handler(args, ctx) {
79
+ const sub = args[0] ?? 'list';
80
+ const knownSubcommands = ['list', 'get', 'create', 'update', 'delete', 'resolve'];
81
+ if (!knownSubcommands.includes(sub)) {
82
+ ctx.print(
83
+ 'Usage: /principals <subcommand>\n'
84
+ + ' /principals list — list every principal\n'
85
+ + ' /principals get <id> — show one principal\n'
86
+ + ' /principals create <name> <user|bot|service|token> [channel:value...]\n'
87
+ + ' /principals update <id> [--name x] [--kind x] [--identities c:v,c:v]\n'
88
+ + ' /principals delete <id> — permanently remove a principal\n'
89
+ + ' /principals resolve <channel> <value> — resolve a sender identity to its principal, or "unknown principal"'
90
+ );
91
+ return;
92
+ }
93
+
94
+ // Validate each subcommand's required positional arguments before
95
+ // touching the operator connection, so a usage mistake never depends
96
+ // on daemon reachability to be reported.
97
+ if (sub === 'get' && !args[1]) {
98
+ ctx.print('Usage: /principals get <id>');
99
+ return;
100
+ }
101
+ if (sub === 'create' && (!args[1] || !isPrincipalKind(args[2]))) {
102
+ ctx.print(`Usage: /principals create <name> <${PRINCIPAL_KINDS.join('|')}> [channel:value...]`);
103
+ return;
104
+ }
105
+ if (sub === 'update' && !args[1]) {
106
+ ctx.print('Usage: /principals update <id> [--name x] [--kind user|bot|service|token] [--identities c:v,c:v]');
107
+ return;
108
+ }
109
+ if (sub === 'delete' && !args[1]) {
110
+ ctx.print('Usage: /principals delete <id>');
111
+ return;
112
+ }
113
+ if (sub === 'resolve' && (!args[1] || !args[2])) {
114
+ ctx.print('Usage: /principals resolve <channel> <value>');
115
+ return;
116
+ }
117
+
118
+ const rpc = getOperatorRpc(ctx);
119
+ if (!rpc.available) {
120
+ ctx.print(`[principals] ${rpc.reason}`);
121
+ return;
122
+ }
123
+
124
+ if (sub === 'list') {
125
+ try {
126
+ const { principals } = await rpc.sdk.operator.invoke('principals.list', {});
127
+ if (principals.length === 0) {
128
+ ctx.print('[principals] no principals registered.');
129
+ return;
130
+ }
131
+ ctx.print(['Principals:', ...principals.map(renderPrincipal)].join('\n'));
132
+ } catch (error) {
133
+ ctx.print(`[principals list] ${describeOperatorRpcError(error)}`);
134
+ }
135
+ return;
136
+ }
137
+
138
+ if (sub === 'get') {
139
+ const id = args[1]!;
140
+ try {
141
+ const { principal } = await rpc.sdk.operator.invoke('principals.get', { principalId: id });
142
+ ctx.print(renderPrincipal(principal));
143
+ } catch (error) {
144
+ ctx.print(`[principals get] ${describeOperatorRpcError(error)}`);
145
+ }
146
+ return;
147
+ }
148
+
149
+ if (sub === 'create') {
150
+ const name = args[1]!;
151
+ const kind = args[2] as PrincipalKind;
152
+ const identities = parseIdentities(args.slice(3));
153
+ try {
154
+ const { principal } = await rpc.sdk.operator.invoke('principals.create', { name, kind, identities });
155
+ ctx.print(`[principals create] created ${principal.id}\n${renderPrincipal(principal)}`);
156
+ } catch (error) {
157
+ ctx.print(`[principals create] ${describeOperatorRpcError(error)}`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (sub === 'update') {
163
+ const id = args[1]!;
164
+ const { flags } = extractFlags(args.slice(2), ['name', 'kind', 'identities']);
165
+ const kind = flags.get('kind');
166
+ if (kind !== undefined && !isPrincipalKind(kind)) {
167
+ ctx.print(`Invalid --kind "${kind}". Valid values: ${PRINCIPAL_KINDS.join(', ')}`);
168
+ return;
169
+ }
170
+ const identitiesRaw = flags.get('identities');
171
+ try {
172
+ const { principal } = await rpc.sdk.operator.invoke('principals.update', {
173
+ principalId: id,
174
+ ...(flags.has('name') ? { name: flags.get('name')! } : {}),
175
+ ...(kind !== undefined ? { kind } : {}),
176
+ ...(identitiesRaw !== undefined ? { identities: parseIdentities(identitiesRaw.split(',')) } : {}),
177
+ });
178
+ ctx.print(`[principals update] updated ${principal.id}\n${renderPrincipal(principal)}`);
179
+ } catch (error) {
180
+ ctx.print(`[principals update] ${describeOperatorRpcError(error)}`);
181
+ }
182
+ return;
183
+ }
184
+
185
+ if (sub === 'delete') {
186
+ const id = args[1]!;
187
+ try {
188
+ const result = await rpc.sdk.operator.invoke('principals.delete', { principalId: id });
189
+ ctx.print(result.deleted ? `[principals delete] deleted ${id}.` : `[principals delete] no principal found with id ${id}.`);
190
+ } catch (error) {
191
+ ctx.print(`[principals delete] ${describeOperatorRpcError(error)}`);
192
+ }
193
+ return;
194
+ }
195
+
196
+ if (sub === 'resolve') {
197
+ const channel = args[1]!;
198
+ const value = args[2]!;
199
+ try {
200
+ const result = await rpc.sdk.operator.invoke('principals.resolve', { channel, value });
201
+ if (!result.known) {
202
+ ctx.print(`[principals resolve] unknown principal — ${channel}:${value} is not mapped to any registered principal.`);
203
+ return;
204
+ }
205
+ ctx.print(`[principals resolve] ${channel}:${value} -> ${result.principal.name}\n${renderPrincipal(result.principal)}`);
206
+ } catch (error) {
207
+ ctx.print(`[principals resolve] ${describeOperatorRpcError(error)}`);
208
+ }
209
+ }
210
+ },
211
+ });
212
+ }
@@ -154,11 +154,51 @@ function inspectReleaseBundle(path: string): string {
154
154
  export function registerProductRuntimeCommands(registry: CommandRegistry): void {
155
155
  registry.register({
156
156
  name: 'trust',
157
- description: 'Review trust posture and export portable trust bundles',
158
- usage: '[review|bundle export <path>|bundle inspect <path>]',
157
+ description: 'Review trust posture, set this workspace\'s trust level, or export trust bundles',
158
+ usage: '[review|workspace [trusted|restricted]|bundle export <path>|bundle inspect <path>]',
159
159
  async handler(args, ctx) {
160
- const shellPaths = requireShellPaths(ctx);
161
160
  const sub = args[0] ?? 'review';
161
+ if (sub === 'workspace') {
162
+ const manager = ctx.workspace.workspaceTrustManager;
163
+ if (!manager) {
164
+ ctx.print('Workspace trust is unavailable in this context.');
165
+ return;
166
+ }
167
+ const choice = (args[1] ?? '').toLowerCase();
168
+ if (choice === 'trusted' || choice === 'trust' || choice === 'full' || choice === 'yes') {
169
+ await manager.setLevel('trusted');
170
+ ctx.print('This workspace is now TRUSTED — all tools may run, subject to your permission settings.');
171
+ ctx.renderRequest();
172
+ return;
173
+ }
174
+ if (choice === 'restricted' || choice === 'restrict' || choice === 'readonly' || choice === 'read-only' || choice === 'no') {
175
+ await manager.setLevel('restricted');
176
+ ctx.print('This workspace is now RESTRICTED — read-only exploration; write/execute/delegate tools are denied until you run `/trust workspace trusted`.');
177
+ ctx.renderRequest();
178
+ return;
179
+ }
180
+ if (choice && choice !== 'status' && choice !== 'show') {
181
+ ctx.print(`Unknown workspace trust option "${args[1]}". Usage: /trust workspace [trusted|restricted]`);
182
+ return;
183
+ }
184
+ const level = manager.getLevel();
185
+ const lines = [
186
+ `Workspace trust: ${level.toUpperCase()}` +
187
+ (manager.wasGrandfathered() ? ' (grandfathered from prior GoodVibes state)' : ''),
188
+ level === 'trusted'
189
+ ? 'All tools may run, subject to your normal permission settings.'
190
+ : 'Read-only exploration: write, execute, and delegate tools are denied until you trust this workspace.',
191
+ ];
192
+ if (!manager.isDecided()) lines.push('Not yet decided — run `/trust workspace trusted` to enable full capability.');
193
+ const registrationManager = ctx.workspace.workspaceRegistrationManager;
194
+ if (registrationManager) {
195
+ lines.push(`Registration: ${await registrationManager.describe()}`);
196
+ }
197
+ lines.push('Change with: /trust workspace trusted | /trust workspace restricted');
198
+ ctx.print(lines.join('\n'));
199
+ return;
200
+ }
201
+ const shellPaths = requireShellPaths(ctx);
162
202
  if (sub === 'review') {
163
203
  const bundle = await buildTrustReviewBundle(ctx);
164
204
  ctx.print(formatTrustReview(bundle));
@@ -0,0 +1,54 @@
1
+ /**
2
+ * recall-files-config.ts — TUI-local synthetic setting for the memory file
3
+ * projection directory (`memory.projection.dir`).
4
+ *
5
+ * `memory.projection.dir` is not in the SDK's ConfigKey union — same
6
+ * situation as worktree.setup.commands (see worktree-setup-config.ts, which
7
+ * this file mirrors): a brand-new top-level config section with no
8
+ * DEFAULT_CONFIG entry, so ConfigManager.get()/set() path resolution can
9
+ * throw for it. Read defensively; the settings-modal write path already
10
+ * degrades a resolution failure into an honest "Save failed" message.
11
+ *
12
+ * Default: `.goodvibes/memory/projection`, relative to the project working
13
+ * directory — under the project's own `.goodvibes/` control directory,
14
+ * alongside the (separate) KV-state memory directory used by the state tool.
15
+ */
16
+ import type { ConfigKey, ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
17
+
18
+ export const MEMORY_PROJECTION_DIR_CONFIG_KEY = 'memory.projection.dir' as ConfigKey;
19
+
20
+ /** Default projection directory, relative to the project working directory. */
21
+ export const MEMORY_PROJECTION_DIR_DEFAULT = '.goodvibes/memory/projection';
22
+
23
+ export const MEMORY_PROJECTION_DIR_SYNTHETIC_SETTING: ConfigSetting = {
24
+ key: MEMORY_PROJECTION_DIR_CONFIG_KEY,
25
+ type: 'string',
26
+ default: MEMORY_PROJECTION_DIR_DEFAULT,
27
+ description: '/recall files sync writes one markdown file per standing (project/team-scope) memory record here, and commits the directory when it sits inside a git repository. Relative paths resolve against the project working directory. /recall files review reads user edits back from this directory as proposals — a file edit or deletion never mutates the store directly.',
28
+ };
29
+
30
+ /** Read the configured projection directory, falling back to the default for an absent/malformed value. */
31
+ export function readMemoryProjectionDir(configManager: Pick<ConfigManager, 'get'>): string {
32
+ let raw: unknown;
33
+ try {
34
+ raw = configManager.get(MEMORY_PROJECTION_DIR_CONFIG_KEY);
35
+ } catch {
36
+ return MEMORY_PROJECTION_DIR_DEFAULT;
37
+ }
38
+ return typeof raw === 'string' && raw.length > 0 ? raw : MEMORY_PROJECTION_DIR_DEFAULT;
39
+ }
40
+
41
+ export function isMemoryProjectionDirConfigKey(key: ConfigKey): boolean {
42
+ return key === MEMORY_PROJECTION_DIR_CONFIG_KEY;
43
+ }
44
+
45
+ export function buildMemoryProjectionDirSyntheticEntry(
46
+ configManager: Pick<ConfigManager, 'get'>,
47
+ ): { setting: ConfigSetting; currentValue: string; isDefault: boolean } {
48
+ const currentValue = readMemoryProjectionDir(configManager);
49
+ return {
50
+ setting: MEMORY_PROJECTION_DIR_SYNTHETIC_SETTING,
51
+ currentValue,
52
+ isDefault: currentValue === MEMORY_PROJECTION_DIR_DEFAULT,
53
+ };
54
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * recall-files-git.ts — the synchronous git seam for memory file projection.
3
+ *
4
+ * `projectMemoryToFiles(records, dir, { git })` (SDK, platform/state) is a
5
+ * pure synchronous function and its `MemoryProjectionGit` seam is declared
6
+ * synchronous (`add(dir): void`, `commit(dir, message): void`) — it throws on
7
+ * failure rather than returning a result. The TUI's existing git wrapper,
8
+ * `GitService` (platform/git), is async (wraps simple-git), so it cannot
9
+ * satisfy this seam directly. `GitService`'s own static helpers
10
+ * (initRepo/isGitRepo/getRepoRoot) ARE synchronous, using `Bun.spawnSync`
11
+ * directly (see service.ts) — this file follows the exact same convention
12
+ * for `add`/`commit` so the seam matches the SDK's synchronous contract.
13
+ */
14
+ import type { MemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
15
+ import { GitService } from '@pellux/goodvibes-sdk/platform/git';
16
+
17
+ function decode(bytes: Uint8Array | undefined): string {
18
+ return bytes ? new TextDecoder().decode(bytes).trim() : '';
19
+ }
20
+
21
+ /**
22
+ * Build a synchronous git seam rooted at `repoRoot`. `commit()` treats a
23
+ * "nothing to commit" exit as a benign no-op (not an error) so
24
+ * `/recall files sync` stays safely re-runnable when the projection files
25
+ * did not actually change since the last sync — any OTHER commit failure
26
+ * (no git identity configured, hook rejection, etc.) still throws, since the
27
+ * seam's contract has no way to report partial failure back to the caller
28
+ * other than throwing.
29
+ */
30
+ export function createSyncGitSeam(repoRoot: string): MemoryProjectionGit {
31
+ return {
32
+ add(dir: string): void {
33
+ const result = Bun.spawnSync(['git', 'add', dir], { cwd: repoRoot });
34
+ if (result.exitCode !== 0) {
35
+ throw new Error(`git add failed: ${decode(result.stderr) || 'unknown error'}`);
36
+ }
37
+ },
38
+ commit(dir: string, message: string): void {
39
+ const result = Bun.spawnSync(['git', 'commit', '-m', message, '--', dir], { cwd: repoRoot });
40
+ if (result.exitCode === 0) return;
41
+ const output = `${decode(result.stdout)} ${decode(result.stderr)}`;
42
+ if (/nothing to commit/i.test(output)) return; // benign — no new/changed files this run
43
+ throw new Error(`git commit failed: ${output.trim() || 'unknown error'}`);
44
+ },
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Resolve the git seam for `dir`, or null when `dir` is not inside a git
50
+ * repository. Never auto-initializes a repository — that is a much bigger
51
+ * side effect than a memory sync command should take unasked.
52
+ */
53
+ export function resolveSyncGitSeam(dir: string): { seam: MemoryProjectionGit; repoRoot: string } | null {
54
+ const repoRoot = GitService.getRepoRoot(dir);
55
+ if (!repoRoot) return null;
56
+ return { seam: createSyncGitSeam(repoRoot), repoRoot };
57
+ }