@pellux/goodvibes-tui 2.0.19 → 2.0.20

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.0.20] - 2026-08-22
8
+
9
+ ### Changes
10
+
11
+ - **`goodvibes pair` mints a pairing link again.** The command invoked the
12
+ CLI's own gateway catalog, which is deliberately empty in this surface,
13
+ so every attempt failed with "Gateway method has no internal handler".
14
+ Pairing and its tailscale lookup now route through the daemon, the same
15
+ path every other hosted command uses.
16
+ - **`pair --name` actually names the token.** The option lookup dropped
17
+ the flag's dashes and never matched, so every token got the default
18
+ name. The lookup now takes the flag token verbatim, and a test pins it.
19
+
20
+ ---
21
+
7
22
  ## [2.0.19] - 2026-08-22
8
23
 
9
24
  ### Changes
@@ -16,7 +31,7 @@ All notable changes to GoodVibes TUI.
16
31
  - **A dead subscription login is stamped and shown as ended**, everywhere a
17
32
  status renders; a near-expiry token refreshes silently before the send.
18
33
  - **A machine with no microphone is reported as that**, once, in plain
19
- words, with a gentle retry every minute — not a wake-detector crash loop
34
+ words, with a gentle retry every minute, not a wake-detector crash loop
20
35
  spamming the transcript and latching off. A busy device keeps prompt
21
36
  retries and the crash latch.
22
37
  - Pins move to sdk/terminal-shell/toolchain 2.0.22 and daemon 1.28.24;
@@ -28,7 +43,7 @@ All notable changes to GoodVibes TUI.
28
43
 
29
44
  - **Session start no longer crashes behind the scenes.** Every session
30
45
  opened with "[Error] Provider error: Cannot access 'render' before
31
- initialization" — a startup crash inside the TUI's own wiring, mislabeled
46
+ initialization", a startup crash inside the TUI's own wiring, mislabeled
32
47
  as a provider problem, that killed whatever bootstrap step was mid-flight.
33
48
  Render requests that arrive before the renderer exists are now deferred
34
49
  into the first paint instead of throwing.
@@ -36,7 +51,7 @@ All notable changes to GoodVibes TUI.
36
51
  session ended server-side used to surface as "the provider rejected your
37
52
  API key. Run /login to re-authenticate or check your API key", which reads
38
53
  as "your subscription isn't there" to a logged-in subscriber. It now says
39
- the subscription session has ended and to sign in again — and with
54
+ the subscription session has ended and to sign in again. With
40
55
  platform runtime 2.0.21 underneath, a token OpenAI rotated early is
41
56
  refreshed and retried before anything is surfaced at all.
42
57
  - **Escaped errors are captioned honestly.** An unhandled rejection with no
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-2.0.15-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-2.0.20-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  GoodVibes is a terminal console for coding and operations work with an AI model. You run `goodvibes` in a project directory and get a full-screen terminal app where you talk to a model that can read and edit your files, run shell commands, search the web, and hand work off to background agents, asking your permission before anything that writes or executes.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "2.0.19",
3
+ "version": "2.0.20",
4
4
  "description": "Terminal-native GoodVibes client for coding, operations, knowledge and channel workflows; a pure client of the GoodVibes daemon.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -397,14 +397,18 @@ export async function renderPairing(runtime: CliCommandRuntime): Promise<string>
397
397
  // lease change where a stable name exists. The verb re-derives the same origin
398
398
  // and returns its honest posture, so this call is a persistence side effect.
399
399
  ensurePublicBaseUrl(runtime.configManager);
400
- const name = readOptionValue(runtime.cli.commandArgs, 'name') ?? defaultPairingTokenName();
401
- // Mint through the canonical hand-off verb: it mints a fresh per-device token
402
- // and returns the exact `#pair=<token>` deep link the web app consumes. The
403
- // daemon filters the requested offer set down to what it can actually satisfy.
404
- const result = (await services.gatewayMethods.invoke('pairing.handoff.create', {
405
- body: { name, offers: [...PAIRING_HANDOFF_OFFER_KINDS] },
406
- context: { principalId: 'admin', principalKind: 'user', admin: true },
407
- })) as PairingHandoffCreateResult;
400
+ const name = readOptionValue(runtime.cli.commandArgs, '--name') ?? defaultPairingTokenName();
401
+ // Mint through the canonical hand-off verb ON THE DAEMON: the deep link
402
+ // pairs a device to the daemon-hosted web app, and the offer set (push,
403
+ // relay, passkey) is the daemon's to filter to what it can satisfy. The
404
+ // local `services.gatewayMethods` catalog is deliberately empty in this
405
+ // surface (see runtime/services.ts), so invoking it answered
406
+ // "no internal handler" for every verb; the daemon verb caller is the
407
+ // canonical route and refuses honestly when no daemon is reachable.
408
+ const result = await services.daemonVerbs.invoke<PairingHandoffCreateResult>(
409
+ 'pairing.handoff.create',
410
+ { name, offers: [...PAIRING_HANDOFF_OFFER_KINDS] },
411
+ );
408
412
  const link = result.deepLink ?? result.fragment;
409
413
  const qr = renderQrToString(generateQrMatrix(link));
410
414
  const lines: string[] = [
@@ -445,10 +449,7 @@ interface TailscaleGetResult {
445
449
  */
446
450
  async function renderPairingTailscaleLines(services: RuntimeServices): Promise<string[]> {
447
451
  try {
448
- const ts = (await services.gatewayMethods.invoke('tailscale.get', {
449
- body: {},
450
- context: { principalId: 'admin', principalKind: 'user', admin: true },
451
- })) as TailscaleGetResult;
452
+ const ts = await services.daemonVerbs.invoke<TailscaleGetResult>('tailscale.get', {});
452
453
  if (!ts.available) return [];
453
454
  if (ts.httpsUrl) return ['', `Encrypted access (Tailscale): ${ts.httpsUrl}`];
454
455
  return ['', 'Tailscale detected: run it as a serve target for encrypted https access (see the pairing modal).'];
@@ -70,7 +70,7 @@ export function createBudgetBreachNotifier(deps: BudgetBreachNotifierDeps): Budg
70
70
  const breached = computeBudgetBreach(sessionCost, budgetThresholdUsd);
71
71
 
72
72
  if (!breached) {
73
- notified = false; // dropped back under budget — can re-fire on the next crossing
73
+ notified = false; // dropped back under budget, can re-fire on the next crossing
74
74
  return false;
75
75
  }
76
76
  if (notified) return false; // already alerted for this crossing
@@ -364,7 +364,7 @@ export class ConversationManager extends SdkConversationManager {
364
364
  this.messageKindRegistry = new Map();
365
365
  this.userReceiptIndices.clear();
366
366
  this.streamingStartLine = -1;
367
- this._displayFromMessageIndex = 0; // full reset — show everything on next render
367
+ this._displayFromMessageIndex = 0; // full reset, show everything on next render
368
368
  }
369
369
 
370
370
  /**
@@ -26,7 +26,7 @@ export function registerGraphRuntimeCommands(registry: CommandRegistry): void {
26
26
  async handler(args, ctx) {
27
27
  const workstreamId = (args[0] ?? '').trim();
28
28
  if (!workstreamId) {
29
- ctx.print('Usage: /graph <workstreamId> — renders that workstream\'s task graph (nodes, edges, states, pool).');
29
+ ctx.print("Usage: /graph <workstreamId> renders that workstream's task graph (nodes, edges, states, pool).");
30
30
  return;
31
31
  }
32
32
  const rpc = getOperatorRpc(ctx);
@@ -172,7 +172,7 @@ function renderChangeNames(result: ProfileWriteResultView): string[] {
172
172
  * forgetting something that was not there reports that it was not there.
173
173
  *
174
174
  * `ok: true` with an empty change list is also not a success line. The daemon
175
- * returns an empty disclosure when nothing changed, and inventing "Noted —"
175
+ * returns an empty disclosure when nothing changed, and inventing "Noted:"
176
176
  * over the top of it would be the false receipt the design forbids.
177
177
  */
178
178
  export function renderProfileWriteResult(result: ProfileWriteResultView): string {
@@ -330,7 +330,7 @@ function handleFallbackTest(
330
330
  ? node.explanation.rejections.map((r) => r.code).join(', ')
331
331
  : '';
332
332
  context.print(
333
- ` [${node.position}] ${node.providerId}/${node.modelId} — ${reasons || 'unknown'}`,
333
+ ` [${node.position}] ${node.providerId}/${node.modelId}: ${reasons || 'unknown'}`,
334
334
  );
335
335
  }
336
336
  if (rejected.length > 5) {
@@ -184,7 +184,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
184
184
  for (const [id, content] of pending) {
185
185
  const contentLines = content.split('\n');
186
186
  const preview = contentLines[0].slice(0, 80);
187
- lines.push(` ${id} — ${contentLines.length} lines — ${preview}${contentLines.length > 1 || preview.length < contentLines[0].length ? '…' : ''}`);
187
+ lines.push(` ${id} (${contentLines.length} lines): ${preview}${contentLines.length > 1 || preview.length < contentLines[0].length ? '…' : ''}`);
188
188
  }
189
189
  lines.push('', 'Full text: /pastes <id>');
190
190
  ctx.print(lines.join('\n'));
@@ -220,7 +220,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
220
220
  // every item here is a command, and picking one RUNS it, label
221
221
  // the verb "Run" (matching the slash-command palette) instead of the
222
222
  // generic "Select" default.
223
- ctx.openSelection('Help — Commands', items, { allowSearch: true, primaryVerbLabel: 'Run' }, (result) => {
223
+ ctx.openSelection('Help: Commands', items, { allowSearch: true, primaryVerbLabel: 'Run' }, (result) => {
224
224
  if (!result) return;
225
225
  const name = result.item.id;
226
226
  void (ctx.executeCommand?.(name, []) ?? registry.execute(name, [], ctx));
@@ -202,10 +202,10 @@ function formatItemMergeState(item: WorkItem): string {
202
202
  * dependencies contributes nothing.
203
203
  */
204
204
  function formatItemDependencyNote(item: WorkItem, ws: Workstream): string {
205
- if (item.state === 'blocked-dependency' && item.blockedReason) return ` — ${item.blockedReason}`;
205
+ if (item.state === 'blocked-dependency' && item.blockedReason) return `, ${item.blockedReason}`;
206
206
  if (item.dependsOn && item.dependsOn.length > 0) {
207
207
  const titleById = new Map(ws.items.map((i) => [i.id, i.title]));
208
- return ` — after: ${item.dependsOn.map((d) => titleById.get(d) ?? shortId(d)).join(', ')}`;
208
+ return `, after: ${item.dependsOn.map((d) => titleById.get(d) ?? shortId(d)).join(', ')}`;
209
209
  }
210
210
  return '';
211
211
  }
@@ -339,9 +339,9 @@ function renderWorkstreamStatus(ws: Workstream): string {
339
339
  }
340
340
  lines.push('Items:');
341
341
  for (const item of ws.items) {
342
- const mergeNote = isolated ? ` — ${formatItemMergeState(item)}` : '';
342
+ const mergeNote = isolated ? `, ${formatItemMergeState(item)}` : '';
343
343
  const depNote = formatItemDependencyNote(item, ws);
344
- lines.push(` ${shortId(item.id)} [${item.state}] ${item.title} — phase: ${item.currentPhaseId ?? '—'}${depNote}${mergeNote}`);
344
+ lines.push(` ${shortId(item.id)} [${item.state}] ${item.title}, phase: ${item.currentPhaseId ?? '—'}${depNote}${mergeNote}`);
345
345
  }
346
346
  if (isolated) {
347
347
  // Honest terminal-summary truth (never inferred from item.state alone,
@@ -249,7 +249,7 @@ export function handleGlobalShortcutToken(
249
249
  }
250
250
 
251
251
  case 'panel-ops': {
252
- // Ctrl+O: TOGGLE the Fleet panel (— same semantics as F2 above; see
252
+ // Ctrl+O: TOGGLE the Fleet panel (same semantics as F2 above; see
253
253
  // toggleFleetPanel's doc comment). The former Ops Control panel was
254
254
  // retired to an 'ops-control' -> 'fleet' alias; rather than route
255
255
  // through the now-aliased openOpsPanel callback (which opens without
@@ -287,7 +287,7 @@ export class InputHandler implements InputHandlerLike {
287
287
  this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
288
288
  this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager, uiServices.sessions.sessionBroker, getSharedHostedSessionRoster(), () => this.requestRender()); // third source: the daemon-hosted roster
289
289
  this.profilePickerModal = new ProfilePickerModal(uiServices.shell.profileManager);
290
- this.selectionQueue = attachSelectionModalQueue(this); // wires exitApp teardown too — see selection-modal-queue.ts
290
+ this.selectionQueue = attachSelectionModalQueue(this); // wires exitApp teardown too, see selection-modal-queue.ts
291
291
  this.initFeedContext();
292
292
  }
293
293
 
@@ -249,7 +249,7 @@ export function buildDefaultModelStep(controller: OnboardingWizardControllerLike
249
249
  kind: 'modelPicker',
250
250
  id: 'default-model.primary-model',
251
251
  label: 'Default provider + model',
252
- hint: 'Open the nested model picker for the shell’s main routing target.',
252
+ hint: "Open the nested model picker for the shell's main routing target.",
253
253
  target: 'main',
254
254
  defaultSelection: {
255
255
  providerId: normalizeText(routing?.primaryProviderId),
@@ -127,7 +127,7 @@ export function agentKindStyle(kind: AgentInspectorEntryKind, colors: Record<str
127
127
  }
128
128
 
129
129
  // ---------------------------------------------------------------------------
130
- // Inspector filter modes (ported from the merged agent-logs console —)
130
+ // Inspector filter modes (ported from the merged agent-logs console)
131
131
  // ---------------------------------------------------------------------------
132
132
 
133
133
  export type AgentInspectorFilterType = 'all' | 'assistant' | 'tool' | 'error';
@@ -256,7 +256,7 @@ export function buildWrfcCostSegments(
256
256
  }
257
257
 
258
258
  // ---------------------------------------------------------------------------
259
- // Agent lifecycle event subscriptions (ported from agent-logs —)
259
+ // Agent lifecycle event subscriptions (ported from agent-logs)
260
260
  // ---------------------------------------------------------------------------
261
261
 
262
262
  export interface AgentActivityHooks {
@@ -123,7 +123,7 @@ export interface BuiltinPanelDeps {
123
123
  workPlanStore?: import('@pellux/goodvibes-sdk/platform/workflow').WorkPlanStore;
124
124
  /** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
125
125
  uiServices?: UiRuntimeServices;
126
- /** Shared plugin manager for plugin and security panels (widened past the read-only observer surface, — so PluginsPanel can drive enable/disable/verify/lift-quarantine). */
126
+ /** Shared plugin manager for plugin and security panels (widened past the read-only observer surface, so PluginsPanel can drive enable/disable/verify/lift-quarantine). */
127
127
  pluginManager?: PluginManagerControls;
128
128
  /** Shared hook dispatcher for the hooks control-room panel. */
129
129
  hookDispatcher?: Pick<HookDispatcher, 'listHooks' | 'getChains'>;
@@ -127,7 +127,7 @@ export class FleetActs {
127
127
  const attention = fleetNodeAttention(node);
128
128
  if (attention?.reason === 'pick') { void this.beginPick(node); return true; }
129
129
  if (attention?.reason === 'conflict') { void this.resolveConflict(node); return true; }
130
- return false; // not an act row — let attach handle it
130
+ return false; // not an act row, let attach handle it
131
131
  }
132
132
  if (key === 'D') {
133
133
  return this.discardWorktree(node);
@@ -364,7 +364,7 @@ export class FleetActs {
364
364
  public discardWorktree(node: ProcessNode): boolean {
365
365
  const item = workItemFromNode(node);
366
366
  const path = item?.worktreePath;
367
- if (!path) return false; // not a worktree-owning row — let the key fall through
367
+ if (!path) return false; // not a worktree-owning row, let the key fall through
368
368
  const gateway = this.requireGateway();
369
369
  if (!gateway) return true;
370
370
  this.deps.diffSurface.armConfirm({
@@ -419,7 +419,7 @@ export class FleetActs {
419
419
  [selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
420
420
  [`${cand.attemptIndex + 1}. `, P.label],
421
421
  [cand.title, selected ? P.value : P.dim],
422
- [` — ${files}, ${cost}`, P.dim],
422
+ [`, ${files}, ${cost}`, P.dim],
423
423
  ]));
424
424
  });
425
425
  if (group.judgment?.proposedWinnerItemId) {
@@ -235,7 +235,7 @@ export class FleetSpawn {
235
235
  lines.push(buildPanelLine(width, [
236
236
  [selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
237
237
  [agent.title, selected ? P.value : P.dim],
238
- [` — ${agent.binaryPath}`, P.dim],
238
+ [`, ${agent.binaryPath}`, P.dim],
239
239
  ]));
240
240
  });
241
241
  } else {
@@ -246,7 +246,7 @@ export class FleetSpawn {
246
246
  lines.push(buildPanelLine(width, [
247
247
  [selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
248
248
  [cand.label, selected ? P.value : P.dim],
249
- [` — ${cand.path}`, P.dim],
249
+ [`, ${cand.path}`, P.dim],
250
250
  ]));
251
251
  });
252
252
  }
@@ -366,7 +366,7 @@ export class GitPanel extends BasePanel {
366
366
  }
367
367
  if (isPanelSearchCommit(key)) {
368
368
  const message = (this.commitMessage ?? '').trim();
369
- if (!message) return true; // absorb — require a non-empty message before confirming
369
+ if (!message) return true; // absorb, require a non-empty message before confirming
370
370
  this.confirm = { subject: { kind: 'commit', message }, label: message, verb: 'Commit' };
371
371
  this.commitMessage = null;
372
372
  this.markDirty();
@@ -255,7 +255,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
255
255
  if (key === 'enter' || key === 'return') {
256
256
  // Submit: call the auth API if a non-empty password was entered.
257
257
  if (state.buffer.length === 0) {
258
- return true; // no-op — require at least one character
258
+ return true; // no-op, require at least one character
259
259
  }
260
260
  const password = state.buffer;
261
261
  const { kind, username, auth } = state;
@@ -124,10 +124,10 @@ class MarketplaceModalSurface implements ConfigModalSurface {
124
124
  }
125
125
  if (this.rows.length === 0) {
126
126
  if (!this.deps.ecosystemPaths) {
127
- rows.push(infoRow('empty:unwired', 'Marketplace catalog roots aren’t wired into this session, so there’s nothing to read yet.'));
127
+ rows.push(infoRow('empty:unwired', "Marketplace catalog roots aren't wired into this session, so there's nothing to read yet."));
128
128
  } else {
129
129
  rows.push(infoRow('empty:0', 'This is your local plugin, skill, hook-pack, and policy-pack catalog; not a remote store.'));
130
- rows.push(infoRow('empty:1', 'It’s empty because nothing has been published or imported into this workspace yet. Entries appear here once you publish a local component or import a bundle.'));
130
+ rows.push(infoRow('empty:1', "It's empty because nothing has been published or imported into this workspace yet. Entries appear here once you publish a local component or import a bundle."));
131
131
  }
132
132
  rows.push(infoRow('empty:title', 'Populate it', { bold: true }));
133
133
  rows.push(infoRow('empty:publish', '/marketplace publish <kind> <path> — publish local plugins/skills into the catalog', { dim: true }));
@@ -167,7 +167,7 @@ class MarketplaceModalSurface implements ConfigModalSurface {
167
167
  if (recommendations.length > 0) {
168
168
  rows.push(infoRow('rec:title', 'Recommended', { bold: true }));
169
169
  for (const [i, rec] of recommendations.slice(0, 3).entries()) {
170
- rows.push(infoRow(`rec:${i}`, `${rec.title} — ${rec.command}`, { dim: true }));
170
+ rows.push(infoRow(`rec:${i}`, `${rec.title}: ${rec.command}`, { dim: true }));
171
171
  }
172
172
  }
173
173
 
@@ -605,7 +605,7 @@ export class TokenBudgetPanel extends BasePanel {
605
605
  ];
606
606
 
607
607
  for (const [lbl, val, color] of rows) {
608
- if (lines.length >= 8) break; // guard — don't overflow the section
608
+ if (lines.length >= 8) break; // guard, don't overflow the section
609
609
  lines.push(buildStyledPanelLine(width, [
610
610
  { text: lbl, fg: C.label },
611
611
  { text: fmtTok(val), fg: color, bold: lbl.includes('Total') },
@@ -181,7 +181,7 @@ export class Compositor {
181
181
  const borderFgForRow = (i: number): string => {
182
182
  if (!hasPanel || !panel!.separator || !panelFocused) return PANEL_BORDER_DIM;
183
183
  if (!hasBottomPane) return panel!.topFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
184
- if (i === 0) return PANEL_FOCUS_ACCENT; // workspace bar — panel is focused
184
+ if (i === 0) return PANEL_FOCUS_ACCENT; // workspace bar, panel is focused
185
185
  if (i <= topPaneHeight) return panel!.topFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
186
186
  return panel!.bottomFocused ? PANEL_FOCUS_ACCENT : PANEL_BORDER_DIM;
187
187
  };
@@ -9,8 +9,8 @@
9
9
  * surfacing as an unhandled rejection that killed whatever bootstrap step was
10
10
  * mid-flight. The firing path has to be a callback some installer in that
11
11
  * window invokes synchronously with the rejection crossing a promise boundary
12
- * on the way out — the window contains no await, so no timer or scan callback
13
- * can interleave into it — and this indirection removes the entire class
12
+ * on the way out. The window contains no await, so no timer or scan callback
13
+ * can interleave into it, and this indirection removes the entire class
14
14
  * rather than the one caller: a closure that fires before {@link
15
15
  * DeferredRender.set} runs gets a no-op, and main()'s unconditional first
16
16
  * paint after wiring covers everything deferred.
@@ -708,7 +708,7 @@ export async function initializeBootstrapCore(
708
708
  sandboxExecAskDepsFromRuntime(configManager, featureFlags),
709
709
  (request) => services.requestApproval({ request }),
710
710
  ),
711
- () => trustPromptRef.requestTrustDecision(), // indirection through the ref, not bound early — main.ts patches the real impl in later
711
+ () => trustPromptRef.requestTrustDecision(), // indirection through the ref, not bound early. main.ts patches the real impl in later
712
712
  ),
713
713
  createPermissionConfigReader(configManager),
714
714
  policyRuntimeState,
@@ -100,7 +100,7 @@ function acquireLock(lp: string): boolean {
100
100
  if (Date.now() - st.mtimeMs >= LOCK_STALE_MS) {
101
101
  try { unlinkSync(lp); } catch { /* another process may have beaten us */ }
102
102
  }
103
- } catch { /* lockfile does not exist — expected happy path */ }
103
+ } catch { /* lockfile does not exist, expected happy path */ }
104
104
 
105
105
  try {
106
106
  // 'wx' ≡ O_CREAT | O_EXCL | O_WRONLY, fails atomically if file exists.
@@ -157,7 +157,7 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
157
157
  // Recognized kinds (auth, rate-limit, network...) keep their specific
158
158
  // line. A generic rejection used to be captioned "Provider error",
159
159
  // which sent a startup UI crash out dressed as a model-backend
160
- // problem — but the reverse lie matters too: the classifier has no
160
+ // problem. The reverse lie matters too: the classifier has no
161
161
  // 5xx rule, so a provider outage also classifies generic, and calling
162
162
  // THAT a GoodVibes bug blames us for OpenAI's downtime. The
163
163
  // discriminator is whether the reason carries provider markers
@@ -305,5 +305,5 @@ export interface RuntimeServices {
305
305
  * rather than preserved.
306
306
  */
307
307
  cancelHostedAgentRuns(): number;
308
- dispose(): void; // Stop every poller this graph started; best-effort, total, idempotent. This surface owns its graph — see disposal-wiring.ts.
308
+ dispose(): void; // Stop every poller this graph started; best-effort, total, idempotent. This surface owns its graph, see disposal-wiring.ts.
309
309
  }
@@ -453,7 +453,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
453
453
  configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory, GOODVIBES_TUI_SURFACE_ROOT), surface, shellPaths,
454
454
  ...(options.currentSessionId ? { currentSessionId: options.currentSessionId } : {}),
455
455
  });
456
- const { processRegistry } = createFleetServices({ // Shared archive-aware fleet registry — see fleet-services.ts
456
+ const { processRegistry } = createFleetServices({ // Shared archive-aware fleet registry, see fleet-services.ts
457
457
  agentManager, wrfcController,
458
458
  orchestrationEngine,
459
459
  codeIndexService: codeIndexStore,
@@ -48,7 +48,7 @@ type AgentTaskArgs = {
48
48
  * Emitted whenever the guard changes the effective routing decision:
49
49
  * - 'spawn-forced-wrfc' , implementation-like spawn promoted to WRFC
50
50
  * - 'spawn-suppressed-wrfc' , spawn judged read-only; WRFC suppressed
51
- * - 'batch-collapsed-to-wrfc'— batch-spawn collapsed into a single WRFC owner chain
51
+ * - 'batch-collapsed-to-wrfc', batch-spawn collapsed into a single WRFC owner chain
52
52
  */
53
53
  export type WrfcGuardTraceKind =
54
54
  | 'spawn-forced-wrfc'