@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
@@ -36,11 +36,23 @@ export interface ShellFooterBuildOptions {
36
36
  readonly composerStatus?: string;
37
37
  readonly composerFlags?: readonly string[];
38
38
  readonly composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote';
39
+ /**
40
+ * Current session permission mode (config value: 'prompt' | 'allow-all' |
41
+ * 'custom' | 'plan' | 'accept-edits'). Rendered as a pill in the composer
42
+ * posture block. Cycled by Shift+Tab and toggled by /plan.
43
+ */
44
+ readonly permissionMode?: string;
39
45
  /**
40
46
  * Passive context pressure hint from buildContextStatusHint.
41
47
  * Rendered as a dim informational line above the prompt when non-null.
42
48
  */
43
49
  readonly contextStatusHint?: string | null;
50
+ /**
51
+ * Output of the user's scriptable status line (`statusline.command`).
52
+ * Rendered as a dim informational line above the prompt when non-null,
53
+ * below the context pressure hint.
54
+ */
55
+ readonly scriptableStatusLine?: string | null;
44
56
  /**
45
57
  * Compact footer posture for short terminals (~<30 rows). Collapses the
46
58
  * footer to its essentials (prompt box + token/cost line + help) and drops
@@ -72,6 +84,10 @@ const PROCESS_INDICATOR_ROWS = 1;
72
84
  // border, token-usage line, and the help/exit line.
73
85
  const COMPACT_FOOTER_BASE_ROWS = 4;
74
86
 
87
+ // Dim slate foreground shared by the passive footer status lines (context
88
+ // pressure hint and the scriptable status line).
89
+ const DIM_STATUS_FG = '#64748b';
90
+
75
91
  /**
76
92
  * Real height of the most recently rendered footer, tagged with the compact
77
93
  * posture it was rendered under. estimateShellFooterHeight prefers this so
@@ -127,6 +143,7 @@ export function buildShellFooter(
127
143
  options.composerPendingRisk,
128
144
  options.compact ?? false,
129
145
  options.sessionSpineStatus,
146
+ options.permissionMode,
130
147
  );
131
148
  // Compact posture drops the process indicator and context hint entirely so
132
149
  // the footer fits within ~5 rows on short terminals.
@@ -140,9 +157,14 @@ export function buildShellFooter(
140
157
  );
141
158
  const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
142
159
  lines.splice(inputBoxRows, 0, ...processIndicator);
160
+ // Scriptable status line — dim informational line above the prompt. Unshifted
161
+ // before the context hint so the context hint (if any) sits above it.
162
+ if (options.scriptableStatusLine) {
163
+ lines.unshift(UIFactory.stringToLine(options.scriptableStatusLine, options.width, { fg: DIM_STATUS_FG }));
164
+ }
143
165
  // Passive context status hint — rendered as a dim informational line before the prompt.
144
166
  if (options.contextStatusHint) {
145
- const hintLine = UIFactory.stringToLine(options.contextStatusHint, options.width, { fg: '#64748b' });
167
+ const hintLine = UIFactory.stringToLine(options.contextStatusHint, options.width, { fg: DIM_STATUS_FG });
146
168
  lines.unshift(hintLine);
147
169
  }
148
170
  }
@@ -46,6 +46,10 @@ export function classifySystemMessage(content: string): SystemMessageType {
46
46
  return 'info';
47
47
  }
48
48
 
49
+ // [Routing] chip — a mid-session model change is informational (the [Failover]
50
+ // line, not this, carries the alarming transport-error cases).
51
+ if (/^\[Routing\]/.test(content)) return 'info';
52
+
49
53
  // [Local] and [Recovery] messages
50
54
  if (/^\[Local\]/.test(content)) return 'info';
51
55
  if (/^\[Recovery\]/.test(content)) {
@@ -93,20 +93,65 @@ function summarizeEdit(obj: Record<string, unknown>): string | null {
93
93
  return `${verb} ${applied} edit${applied === 1 ? '' : 's'}${target}${failText}`;
94
94
  }
95
95
 
96
+ /** Credential-bearing env-var names withheld from the spawn (never values). Empty when the field is absent/empty. */
97
+ function withheldEnvNames(obj: Record<string, unknown>): string[] {
98
+ const raw = obj.withheld_env;
99
+ if (!Array.isArray(raw)) return [];
100
+ return raw.filter((n): n is string => typeof n === 'string' && n.length > 0);
101
+ }
102
+
103
+ /** Compact "withheld: NAME, NAME, …" suffix, capped so a long scrub list doesn't dominate the collapsed line. */
104
+ function withheldEnvSuffix(names: readonly string[]): string {
105
+ if (names.length === 0) return '';
106
+ const MAX_SHOWN = 3;
107
+ const shown = names.slice(0, MAX_SHOWN).join(', ');
108
+ const more = names.length > MAX_SHOWN ? `, +${names.length - MAX_SHOWN} more` : '';
109
+ return ` · withheld: ${shown}${more}`;
110
+ }
111
+
112
+ /**
113
+ * Compact per-command exec-sandbox suffix: `sandboxed` fields are present
114
+ * only when the sandbox was active for the run (SDK's attachSandboxMeta stays
115
+ * quiet otherwise), so absence here means "ran unsandboxed" and the suffix is
116
+ * empty — never a fabricated claim either way.
117
+ *
118
+ * sandboxed, network disabled -> " · sandboxed (net off)"
119
+ * sandboxed, network enabled, escalation -> " · sandboxed (net on, +1 escalation)"
120
+ * requested but host couldn't provide it -> " · sandbox unavailable (<reason>)"
121
+ */
122
+ function sandboxSuffix(obj: Record<string, unknown>): string {
123
+ const sandboxed = obj.sandboxed;
124
+ if (sandboxed === undefined) return '';
125
+ if (sandboxed === false) {
126
+ const reason = typeof obj.sandbox_boundary === 'string' ? obj.sandbox_boundary : undefined;
127
+ return reason ? ` · sandbox unavailable (${reason})` : ' · sandbox unavailable';
128
+ }
129
+ const network = typeof obj.sandbox_network === 'string' ? obj.sandbox_network : undefined;
130
+ const escalations = Array.isArray(obj.sandbox_escalations)
131
+ ? obj.sandbox_escalations.filter((e): e is string => typeof e === 'string')
132
+ : [];
133
+ const netText = network ? `net ${network}` : undefined;
134
+ const escText = escalations.length > 0 ? `+${escalations.length} escalation${escalations.length === 1 ? '' : 's'}` : undefined;
135
+ const detail = [netText, escText].filter((s): s is string => s !== undefined).join(', ');
136
+ return detail ? ` · sandboxed (${detail})` : ' · sandboxed';
137
+ }
138
+
96
139
  /** A single exec command result. */
97
140
  function summarizeExecOne(obj: Record<string, unknown>): string | null {
98
141
  const exit = obj.exit_code;
99
142
  const dur = asNumber(obj.duration_ms);
100
143
  const durText = dur !== undefined ? ` · ${formatDuration(dur)}` : '';
101
- if (obj.timed_out === true) return `timed out${durText}`;
102
- if (obj.cancelled === true) return `cancelled${durText}`;
144
+ const withheldText = withheldEnvSuffix(withheldEnvNames(obj));
145
+ const sandboxText = sandboxSuffix(obj);
146
+ if (obj.timed_out === true) return `timed out${durText}${withheldText}${sandboxText}`;
147
+ if (obj.cancelled === true) return `cancelled${durText}${withheldText}${sandboxText}`;
103
148
  const stdout = typeof obj.stdout === 'string' ? obj.stdout : '';
104
149
  const lineCount = stdout.length === 0
105
150
  ? 0
106
151
  : stdout.replace(/\n$/, '').split('\n').length;
107
152
  const lineText = lineCount > 0 ? ` · ${lineCount} line${lineCount === 1 ? '' : 's'}` : '';
108
153
  const code = typeof exit === 'number' ? String(exit) : '?';
109
- return `exit ${code}${durText}${lineText}`;
154
+ return `exit ${code}${durText}${lineText}${withheldText}${sandboxText}`;
110
155
  }
111
156
 
112
157
  /** exec: single { cmd, exit_code, ... } or multi { commands: [...], total }. */
@@ -116,9 +161,24 @@ function summarizeExec(obj: Record<string, unknown>): string | null {
116
161
  const failed = obj.commands.filter(
117
162
  (c) => c && typeof c === 'object' && (c as Record<string, unknown>).success === false,
118
163
  ).length;
164
+ // Union of withheld names across every command in the batch — the scrub
165
+ // runs per-spawn, so different commands can withhold different names.
166
+ const withheldAcrossCommands = new Set<string>();
167
+ for (const c of obj.commands) {
168
+ if (c && typeof c === 'object') {
169
+ for (const name of withheldEnvNames(c as Record<string, unknown>)) withheldAcrossCommands.add(name);
170
+ }
171
+ }
172
+ const withheldText = withheldEnvSuffix([...withheldAcrossCommands]);
173
+ // Count sandboxed commands across the batch — a compact "N sandboxed" beats
174
+ // repeating each command's full boundary detail on the collapsed line.
175
+ const sandboxedCount = obj.commands.filter(
176
+ (c) => c && typeof c === 'object' && (c as Record<string, unknown>).sandboxed === true,
177
+ ).length;
178
+ const sandboxText = sandboxedCount > 0 ? ` · ${sandboxedCount} sandboxed` : '';
119
179
  return failed > 0
120
- ? `${total} commands · ${failed} failed`
121
- : `${total} commands · all ok`;
180
+ ? `${total} commands · ${failed} failed${withheldText}${sandboxText}`
181
+ : `${total} commands · all ok${withheldText}${sandboxText}`;
122
182
  }
123
183
  if ('exit_code' in obj || 'timed_out' in obj || 'cancelled' in obj) {
124
184
  return summarizeExecOne(obj);
@@ -126,6 +186,35 @@ function summarizeExec(obj: Record<string, unknown>): string | null {
126
186
  return null;
127
187
  }
128
188
 
189
+ /**
190
+ * agent status/get/wait: an optional `failure` field (ChildFailureEnvelope,
191
+ * see the SDK's tools/agent/child-failure-envelope.ts) is present once the
192
+ * child agent terminated abnormally (max_turns, circuit_breaker,
193
+ * watchdog_timeout, budget_exhausted, claim_unverified, api_error, killed,
194
+ * interrupted, error). Renders it compactly on the collapsed preview line so
195
+ * a failed child is visible without expanding the full result; absent
196
+ * `failure` (the common, successful case) falls through to the raw preview.
197
+ */
198
+ function summarizeAgentFailure(obj: Record<string, unknown>): string | null {
199
+ const failure = obj.failure;
200
+ if (!failure || typeof failure !== 'object') return null;
201
+ const f = failure as Record<string, unknown>;
202
+ const agentId = typeof f.agentId === 'string' ? f.agentId : '?';
203
+ const phase = typeof f.phase === 'string' ? f.phase : undefined;
204
+ const reason = f.reason && typeof f.reason === 'object' ? f.reason as Record<string, unknown> : undefined;
205
+ const code = reason && typeof reason.code === 'string' ? reason.code : 'error';
206
+ const message = reason && typeof reason.message === 'string' ? reason.message : '';
207
+ const partialOutputs = f.partialOutputs && typeof f.partialOutputs === 'object' ? f.partialOutputs as Record<string, unknown> : undefined;
208
+ const turnsCompleted = asNumber(partialOutputs?.turnsCompleted);
209
+
210
+ const MAX_MSG_LEN = 80;
211
+ const truncatedMessage = message.length > MAX_MSG_LEN ? `${message.slice(0, MAX_MSG_LEN - 3)}...` : message;
212
+ const phaseText = phase ? ` at ${phase}` : '';
213
+ const turnsText = turnsCompleted !== undefined ? ` (${turnsCompleted} turn${turnsCompleted === 1 ? '' : 's'} completed)` : '';
214
+ const msgText = truncatedMessage.length > 0 ? `: ${truncatedMessage}` : '';
215
+ return `agent ${agentId} failed [${code}]${phaseText}${msgText}${turnsText}`;
216
+ }
217
+
129
218
  export function summarizeToolResult(toolName: string | undefined, content: string): string | null {
130
219
  if (!toolName) return null;
131
220
  const family = toolName.includes('__') ? toolName.split('__').pop()! : toolName;
@@ -143,6 +232,7 @@ export function summarizeToolResult(toolName: string | undefined, content: strin
143
232
  case 'read': return summarizeRead(obj);
144
233
  case 'edit': return summarizeEdit(obj);
145
234
  case 'exec': return summarizeExec(obj);
235
+ case 'agent': return summarizeAgentFailure(obj);
146
236
  default: return null;
147
237
  }
148
238
  }
@@ -9,6 +9,7 @@ import { activeUiTones } from './theme.ts';
9
9
  import { formatElapsed } from '../utils/format-elapsed.ts';
10
10
  import { abbreviateCount } from '../utils/format-number.ts';
11
11
  import { computeContextUsage } from '../core/context-usage.ts';
12
+ import { permissionModeLabel, permissionModeTone } from '../core/permission-mode.ts';
12
13
  import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
13
14
  import { buildFooterTip, isAgentActive } from './footer-tips.ts';
14
15
  import type { StreamMetrics } from '../core/stream-event-wiring.ts';
@@ -198,6 +199,9 @@ export class UIFactory {
198
199
  // Honest cross-surface spine posture. Defined ONLY in adopted-daemon
199
200
  // mode (undefined in embedded/local), so the segment is absent otherwise.
200
201
  sessionSpineStatus?: 'online' | 'offline',
202
+ // Session permission mode (config value). Rendered as a leading pill in the
203
+ // posture block; undefined suppresses the pill (e.g. bare test callers).
204
+ permissionMode?: string,
201
205
  ): Line[] {
202
206
  const lines: Line[] = [];
203
207
  const promptLines = prompt.split('\n');
@@ -332,6 +336,24 @@ export class UIFactory {
332
336
  // (light-terminal-aware); state.info reads on both light and dark terminals.
333
337
  const t = activeUiTones();
334
338
  const composerTokens: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
339
+ // Permission-mode pill — always shown so the active session posture (normal
340
+ // / plan / accept-edits / auto) is visible at a glance. Tone: normal reads
341
+ // dim/neutral, plan reads info (read-only, safe), accept-edits and auto read
342
+ // as caution (raised autonomy). Cycled by Shift+Tab, toggled by /plan.
343
+ if (permissionMode) {
344
+ const modeTone = permissionModeTone(permissionMode);
345
+ const modeFg = modeTone === 'caution' ? t.chrome.warn : modeTone === 'info' ? t.state.info : '244';
346
+ composerTokens.push({ text: ` mode:${permissionModeLabel(permissionMode)} `, fg: modeFg, bold: modeTone !== 'neutral', dim: modeTone === 'neutral' });
347
+ }
348
+ // Context-usage chip — always-visible compaction-pressure indicator so the
349
+ // user sees compaction approaching before it happens. Colored by proximity
350
+ // to the auto-compact threshold (fraction; falls back to 0.85 when unset).
351
+ if (contextWindow && contextWindow > 0) {
352
+ const ctxUsage = computeContextUsage(lastInputTokens ?? 0, contextWindow);
353
+ const ctxThr = compactThreshold && compactThreshold > 0 ? compactThreshold : 0.85;
354
+ const ctxFg = ctxUsage.clampedRatio >= ctxThr ? t.chrome.bad : ctxUsage.clampedRatio >= ctxThr * 0.85 ? t.chrome.warn : '244';
355
+ composerTokens.push({ text: ` ctx:${ctxUsage.pct}% `, fg: ctxFg, bold: ctxUsage.clampedRatio >= ctxThr, dim: ctxUsage.clampedRatio < ctxThr * 0.85 });
356
+ }
335
357
  if (composerMode) composerTokens.push({ text: ` ${GLYPHS.status.active} ${composerMode} `, fg: t.state.info, bold: true });
336
358
  if (composerPendingRisk && composerPendingRisk !== 'none') {
337
359
  const riskColor = composerPendingRisk === 'approval-wait'
@@ -18,6 +18,9 @@ import type { ForensicsRegistry } from '@/runtime/index.ts';
18
18
  import type { PolicyRuntimeState } from '@/runtime/index.ts';
19
19
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
20
20
  import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
21
+ import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
22
+ import type { WorkspaceTrustManager } from './trust/workspace-trust.ts';
23
+ import type { WorkspaceRegistrationManager } from './trust/workspace-registration.ts';
21
24
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
22
25
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
23
26
  import type { IntegrationHelperService } from '@/runtime/index.ts';
@@ -53,6 +56,7 @@ import {
53
56
 
54
57
  export type CreateBootstrapCommandContextOptions = {
55
58
  configManager: ConfigManager;
59
+ featureFlagManager?: import('@/runtime/index.ts').FeatureFlagManager;
56
60
  providerRegistry: ProviderRegistry;
57
61
  conversation: ConversationManager;
58
62
  runtime: MutableRuntimeState;
@@ -75,6 +79,9 @@ export type CreateBootstrapCommandContextOptions = {
75
79
  planRuntime?: PlanRuntimeService;
76
80
  fileUndoManager: FileUndoManager;
77
81
  workspaceCheckpointManager?: WorkspaceCheckpointManager;
82
+ gatewayMethods?: GatewayMethodCatalog;
83
+ workspaceTrustManager?: WorkspaceTrustManager;
84
+ workspaceRegistrationManager?: WorkspaceRegistrationManager;
78
85
  memoryRegistry?: MemoryRegistry;
79
86
  integrationHelpers?: IntegrationHelperService;
80
87
  automationManager?: ShellAutomationManagerRuntimeService;
@@ -136,6 +143,7 @@ export function createBootstrapCommandContext(
136
143
  const {
137
144
  providerRegistry,
138
145
  configManager,
146
+ featureFlagManager,
139
147
  conversation,
140
148
  runtime,
141
149
  requestRender,
@@ -156,6 +164,9 @@ export function createBootstrapCommandContext(
156
164
  planRuntime,
157
165
  fileUndoManager,
158
166
  workspaceCheckpointManager,
167
+ gatewayMethods,
168
+ workspaceTrustManager,
169
+ workspaceRegistrationManager,
159
170
  memoryRegistry,
160
171
  integrationHelpers,
161
172
  automationManager,
@@ -265,6 +276,9 @@ export function createBootstrapCommandContext(
265
276
  keybindingsManager,
266
277
  fileUndoManager,
267
278
  workspaceCheckpointManager,
279
+ gatewayMethods,
280
+ workspaceTrustManager,
281
+ workspaceRegistrationManager,
268
282
  panelManager,
269
283
  profileManager,
270
284
  bookmarkManager,
@@ -272,7 +286,7 @@ export function createBootstrapCommandContext(
272
286
  projectPlanningProjectId,
273
287
  workPlanStore,
274
288
  }, shellServices);
275
- const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
289
+ const platform = createBootstrapCommandPlatformSection({ configManager, featureFlagManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
276
290
  const extensions = createBootstrapCommandExtensionsSection({
277
291
  toolRegistry,
278
292
  mcpRegistry,
@@ -9,6 +9,7 @@ import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp';
9
9
  import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
10
10
  import type { ProviderApi } from '@pellux/goodvibes-sdk/platform/providers';
11
11
  import type { OpsApi } from '@/runtime/index.ts';
12
+ import type { FeatureFlagManager } from '@/runtime/index.ts';
12
13
  import type { MutableRuntimeState } from '@/runtime/index.ts';
13
14
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
14
15
  import type { CommandContext } from '../input/command-registry.ts';
@@ -19,6 +20,9 @@ import type { ForensicsRegistry } from '@/runtime/index.ts';
19
20
  import type { PolicyRuntimeState } from '@/runtime/index.ts';
20
21
  import type { CodeIndexStore, FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
21
22
  import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
23
+ import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
24
+ import type { WorkspaceTrustManager } from './trust/workspace-trust.ts';
25
+ import type { WorkspaceRegistrationManager } from './trust/workspace-registration.ts';
22
26
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
23
27
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
24
28
  import type { IntegrationHelperService } from '@/runtime/index.ts';
@@ -70,6 +74,7 @@ export interface BootstrapCommandActionOptions {
70
74
 
71
75
  export interface BootstrapCommandSectionOptions {
72
76
  readonly configManager: ConfigManager;
77
+ readonly featureFlagManager?: FeatureFlagManager;
73
78
  readonly providerRegistry: ProviderRegistry;
74
79
  readonly conversation: ConversationManager;
75
80
  readonly runtime: MutableRuntimeState;
@@ -91,6 +96,9 @@ export interface BootstrapCommandSectionOptions {
91
96
  readonly shellPaths: ShellPathService;
92
97
  readonly fileUndoManager: FileUndoManager;
93
98
  readonly workspaceCheckpointManager?: WorkspaceCheckpointManager;
99
+ readonly gatewayMethods?: GatewayMethodCatalog;
100
+ readonly workspaceTrustManager?: WorkspaceTrustManager;
101
+ readonly workspaceRegistrationManager?: WorkspaceRegistrationManager;
94
102
  readonly memoryRegistry?: MemoryRegistry;
95
103
  readonly integrationHelpers?: IntegrationHelperService;
96
104
  readonly knowledgeService?: KnowledgeService;
@@ -362,7 +370,7 @@ export function createBootstrapCommandProviderSection(
362
370
  export function createBootstrapCommandWorkspaceSection(
363
371
  options: Pick<
364
372
  BootstrapCommandSectionOptions,
365
- 'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
373
+ 'keybindingsManager' | 'fileUndoManager' | 'workspaceCheckpointManager' | 'gatewayMethods' | 'workspaceTrustManager' | 'workspaceRegistrationManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
366
374
  | 'projectPlanningService' | 'projectPlanningProjectId' | 'workPlanStore'
367
375
  >,
368
376
  shellServices: BootstrapCommandShellServices,
@@ -371,6 +379,9 @@ export function createBootstrapCommandWorkspaceSection(
371
379
  keybindingsManager: options.keybindingsManager,
372
380
  fileUndoManager: options.fileUndoManager,
373
381
  workspaceCheckpointManager: options.workspaceCheckpointManager,
382
+ gatewayMethods: options.gatewayMethods,
383
+ workspaceTrustManager: options.workspaceTrustManager,
384
+ workspaceRegistrationManager: options.workspaceRegistrationManager,
374
385
  panelManager: options.panelManager,
375
386
  profileManager: options.profileManager,
376
387
  bookmarkManager: options.bookmarkManager,
@@ -384,13 +395,14 @@ export function createBootstrapCommandWorkspaceSection(
384
395
  export function createBootstrapCommandPlatformSection(
385
396
  options: Pick<
386
397
  BootstrapCommandSectionOptions,
387
- 'configManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
398
+ 'configManager' | 'featureFlagManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
388
399
  >,
389
400
  shellServices: BootstrapCommandShellServices,
390
401
  ): BootstrapCommandPlatformSection {
391
402
  return {
392
403
  config: getConfigSnapshot(options.configManager),
393
404
  configManager: options.configManager,
405
+ featureFlagManager: options.featureFlagManager,
394
406
  voiceProviderRegistry: options.voiceProviderRegistry,
395
407
  voiceService: options.voiceService,
396
408
  webSearchService: options.webSearchService,
@@ -1,4 +1,5 @@
1
1
  import { ConversationManager } from '../core/conversation';
2
+ import { registerSessionConversation } from './conversation-rewind-port.ts';
2
3
  import { SelectionManager } from '../input/selection.ts';
3
4
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
4
5
  import { ConfigManager, getConfiguredSystemPrompt } from '../config/index.ts';
@@ -13,19 +14,15 @@ import type { SystemMessageRouter } from '../core/system-message-router.ts';
13
14
  import type { ConversationFollowUpItem } from '@pellux/goodvibes-sdk/platform/core';
14
15
  import type { OrchestratorUserInputOptions } from '../core/orchestrator.ts';
15
16
  import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
16
- import type { MutableRuntimeState } from '@/runtime/index.ts';
17
17
  import type { BootstrapOptions } from './context.ts';
18
- import { createFeatureFlagManager } from '@/runtime/index.ts';
19
- import { RuntimeEventBus } from '@/runtime/index.ts';
20
- import type { SessionEvent } from '@/runtime/index.ts';
21
18
  import { createRuntimeStore, createDomainDispatch, type RuntimeStore } from './store/index.ts';
22
- import { ForensicsCollector, ForensicsRegistry } from '@/runtime/index.ts';
23
19
  import {
24
- generateUserSessionId,
20
+ type MutableRuntimeState, type SessionEvent, createFeatureFlagManager, RuntimeEventBus, ForensicsCollector,
21
+ ForensicsRegistry, generateUserSessionId, loadBootstrapSystemPrompt, syncConfiguredServices,
22
+ registerBootstrapHookBridge, registerBootstrapRuntimeEvents,
25
23
  } from '@/runtime/index.ts';
26
- import { loadBootstrapSystemPrompt, syncConfiguredServices } from '@/runtime/index.ts';
27
- import { registerBootstrapHookBridge } from '@/runtime/index.ts';
28
- import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
24
+ import { readExecEnvScrubAllowlist } from '../input/exec-env-scrub-config.ts';
25
+ import { createSandboxExecAsk, sandboxExecAskDepsFromRuntime } from '../permissions/sandbox-exec-gate.ts';
29
26
  import { createRuntimeServices, type RuntimeServices } from './services.ts';
30
27
  import { runBootMemoryFold } from './memory-fold.ts';
31
28
  import { setPricingSource } from '../export/cost-utils.ts';
@@ -36,6 +33,8 @@ import { createWrfcPersistence, type WrfcPersistence } from './wrfc-persistence.
36
33
  import type { SystemMessagePriority } from '../core/system-message-router.ts';
37
34
  import { SessionSpineClient, SessionUnionCache, TUI_SPINE_PARTICIPANT } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
38
35
  import { SessionInboundInputPoller, createBootstrapInboundInputPoller } from './session-inbound-inputs.ts';
36
+ import { detectPriorWorkspaceState, trustGatedAsk } from './trust/workspace-trust.ts';
37
+ import { syncNotifierQueueIntegrations } from './bootstrap-notifier-sync.ts';
39
38
 
40
39
  // ---------------------------------------------------------------------------
41
40
  // Pre-router buffer
@@ -250,7 +249,10 @@ export async function initializeBootstrapCore(
250
249
  getConversationTitle: () => getConversationTitle(),
251
250
  workingDir,
252
251
  homeDirectory,
252
+ // Sampled BEFORE any service writes to <cwd>/.goodvibes so a new workspace isn't misread as used.
253
+ workspaceHadPriorState: detectPriorWorkspaceState(workingDir),
253
254
  });
255
+ await services.workspaceTrustManager.load(); // settle trust (+ grandfathering) before any tool runs
254
256
  const providerRegistry = services.providerRegistry;
255
257
  providerRegistry.initModelLimits();
256
258
  services.benchmarkStore.initBenchmarks();
@@ -371,6 +373,15 @@ export async function initializeBootstrapCore(
371
373
  serviceRegistry: services.serviceRegistry,
372
374
  overflowHandler: services.overflowHandler,
373
375
  changeTracker: services.sessionChangeTracker,
376
+ // Master switch stays on (SDK default); this only widens the allowlist of
377
+ // variable NAMES kept even though they look credential-bearing. See
378
+ // exec-env-scrub-config.ts (permissions.execEnvScrubAllowlist).
379
+ credentialEnvScrub: { allowlist: readExecEnvScrubAllowlist(configManager) },
380
+ // Registers the context_accounting tool on this (the real, interactive)
381
+ // registry against OUR holder rather than a fresh internal one, so the
382
+ // Orchestrator-backed source bound at bootstrap.ts is what the tool
383
+ // actually reads from. See runtime/context-accounting-source.ts.
384
+ contextAccountingHolder: services.contextAccountingHolder,
374
385
  });
375
386
  // Note: installWrfcAgentToolGuard is called after routeOrBuffer is defined
376
387
  // (further below) so the onTrace callback can route guard decisions through
@@ -400,6 +411,7 @@ export async function initializeBootstrapCore(
400
411
  memoryRegistry: services.memoryRegistry,
401
412
  sandboxSessionRegistry: services.sandboxSessionRegistry,
402
413
  workflowServices: services.workflow,
414
+ contextAccountingHolder: services.contextAccountingHolder,
403
415
  });
404
416
 
405
417
  const bootstrapUnsubs: Array<() => void> = [];
@@ -679,39 +691,24 @@ export async function initializeBootstrapCore(
679
691
  services.webhookNotifier.attachToRuntimeBus(runtimeBus);
680
692
 
681
693
  const notifier = await Notifier.fromConfig(services.serviceRegistry);
682
- const queueStatuses = notifier.getQueueStatus();
683
- if (queueStatuses.length > 0) {
684
- notifier.attachToRuntimeBus(runtimeBus);
685
- for (const queueStatus of queueStatuses) {
686
- domainDispatch.syncIntegration({
687
- id: queueStatus.channel,
688
- displayName: queueStatus.channel[0]!.toUpperCase() + queueStatus.channel.slice(1),
689
- category: 'communication',
690
- status: queueStatus.metrics.deadLettered > 0 ? 'degraded' : 'healthy',
691
- enabled: true,
692
- successCount: queueStatus.metrics.delivered,
693
- errorCount: queueStatus.metrics.deadLettered,
694
- ...(queueStatus.dlqEntries[0]?.deadAt ? { lastErrorAt: queueStatus.dlqEntries[0].deadAt } : {}),
695
- ...(queueStatus.dlqEntries[0]?.finalError ? { lastError: queueStatus.dlqEntries[0].finalError } : {}),
696
- meta: {
697
- attempts: queueStatus.metrics.totalAttempts,
698
- retrying: queueStatus.metrics.retrying,
699
- deadLetters: queueStatus.metrics.deadLettered,
700
- dlqSize: queueStatus.metrics.dlqSize,
701
- sloEnforced: queueStatus.sloEnforced,
702
- },
703
- }, 'bootstrap.notifier');
704
- }
705
- }
694
+ syncNotifierQueueIntegrations(notifier, runtimeBus, domainDispatch);
706
695
 
707
696
  await syncConfiguredServices(domainDispatch.syncIntegration, services.serviceRegistry);
708
697
 
709
698
  const permissionManager = new PermissionManager(
710
- (request) => approvalBroker.requestApproval({
711
- request,
712
- sessionId: runtimeSessionIdRef.value,
713
- localPrompt: permissionPromptRef.requestPermission,
714
- }),
699
+ // Composed ask layer: the workspace trust gate (outer) wraps the sandbox-aware
700
+ // exec gate (inner); see sandbox-exec-gate.ts. The catastrophic block is untouched.
701
+ trustGatedAsk(
702
+ services.workspaceTrustManager,
703
+ createSandboxExecAsk(
704
+ sandboxExecAskDepsFromRuntime(configManager, featureFlags),
705
+ (request) => approvalBroker.requestApproval({
706
+ request,
707
+ sessionId: runtimeSessionIdRef.value,
708
+ localPrompt: permissionPromptRef.requestPermission,
709
+ }),
710
+ ),
711
+ ),
715
712
  createPermissionConfigReader(configManager),
716
713
  policyRuntimeState,
717
714
  services.hookDispatcher,
@@ -728,6 +725,9 @@ export async function initializeBootstrapCore(
728
725
  sessionId: userSessionId,
729
726
  };
730
727
  runtimeSessionIdRef.value = runtime.sessionId;
728
+ // Register the live conversation so the composed daemon's rewind.plan/apply verbs
729
+ // can serve conversation scope for this session (see conversation-rewind-port.ts).
730
+ registerSessionConversation(runtime.sessionId, conversation);
731
731
  void sharedSessionBroker.createSession({
732
732
  id: runtime.sessionId,
733
733
  title: 'Terminal UI session',
@@ -0,0 +1,41 @@
1
+ import type { Notifier } from '@pellux/goodvibes-sdk/platform/integrations';
2
+ import type { RuntimeEventBus } from '@/runtime/index.ts';
3
+ import type { createDomainDispatch } from './store/index.ts';
4
+
5
+ type DomainDispatch = ReturnType<typeof createDomainDispatch>;
6
+
7
+ /**
8
+ * Reflect the notifier's outbound delivery queues into the integrations domain
9
+ * (so the UI shows per-channel health) and attach the notifier to the runtime
10
+ * bus when any queue exists. Extracted from bootstrap-core to keep that file
11
+ * within the module size budget.
12
+ */
13
+ export function syncNotifierQueueIntegrations(
14
+ notifier: Notifier,
15
+ runtimeBus: RuntimeEventBus,
16
+ domainDispatch: DomainDispatch,
17
+ ): void {
18
+ const queueStatuses = notifier.getQueueStatus();
19
+ if (queueStatuses.length === 0) return;
20
+ notifier.attachToRuntimeBus(runtimeBus);
21
+ for (const queueStatus of queueStatuses) {
22
+ domainDispatch.syncIntegration({
23
+ id: queueStatus.channel,
24
+ displayName: queueStatus.channel[0]!.toUpperCase() + queueStatus.channel.slice(1),
25
+ category: 'communication',
26
+ status: queueStatus.metrics.deadLettered > 0 ? 'degraded' : 'healthy',
27
+ enabled: true,
28
+ successCount: queueStatus.metrics.delivered,
29
+ errorCount: queueStatus.metrics.deadLettered,
30
+ ...(queueStatus.dlqEntries[0]?.deadAt ? { lastErrorAt: queueStatus.dlqEntries[0].deadAt } : {}),
31
+ ...(queueStatus.dlqEntries[0]?.finalError ? { lastError: queueStatus.dlqEntries[0].finalError } : {}),
32
+ meta: {
33
+ attempts: queueStatus.metrics.totalAttempts,
34
+ retrying: queueStatus.metrics.retrying,
35
+ deadLetters: queueStatus.metrics.deadLettered,
36
+ dlqSize: queueStatus.metrics.dlqSize,
37
+ sloEnforced: queueStatus.sloEnforced,
38
+ },
39
+ }, 'bootstrap.notifier');
40
+ }
41
+ }
@@ -17,6 +17,7 @@ import { GitStatusProvider } from '../renderer/git-status.ts';
17
17
  import type { GitHeaderInfo } from '../renderer/git-status.ts';
18
18
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
19
19
  import { registerBuiltinPanels } from '../panels/builtin-panels.ts';
20
+ import { WorkspaceRegistrationManager } from './trust/workspace-registration.ts';
20
21
  import { createSystemMessageRouter, type SystemMessageRouter } from '../core/system-message-router.ts';
21
22
  import { getConfigSnapshot } from '../config/index.ts';
22
23
  import { createBootstrapCommandContext } from './bootstrap-command-context.ts';
@@ -259,6 +260,12 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
259
260
  planRuntime,
260
261
  fileUndoManager: services.fileUndoManager,
261
262
  workspaceCheckpointManager: services.workspaceCheckpointManager,
263
+ gatewayMethods: services.gatewayMethods,
264
+ workspaceTrustManager: services.workspaceTrustManager,
265
+ // Registration half is stateless (reads the shared registry on demand), so
266
+ // it is constructed here for the command context rather than threaded through
267
+ // RuntimeServices — no early-load requirement like the trust gate has.
268
+ workspaceRegistrationManager: new WorkspaceRegistrationManager({ shellPaths: services.shellPaths }),
262
269
  memoryRegistry: services.memoryRegistry,
263
270
  integrationHelpers: services.integrationHelpers,
264
271
  automationManager: services.automationManager,
@@ -291,6 +298,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
291
298
  codeIndexStore: services.codeIndexStore,
292
299
  codeIndexReindexScheduler: services.codeIndexReindexScheduler,
293
300
  isPassiveCodeInjectionFlagEnabled: () => services.featureFlags.isEnabled('agent-passive-code-injection'),
301
+ featureFlagManager: services.featureFlags,
294
302
  // Expose the MAIN session's per-turn passive-injection ring
295
303
  // so `/recall injections` (no agent id) renders it — see recall-review.ts.
296
304
  getMainSessionTurnInjections: () => orchestrator.getTurnInjections(),