@pellux/goodvibes-tui 1.9.1 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +16 -5
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/audio/player.ts +91 -5
  5. package/src/audio/spoken-turn-controller.ts +30 -2
  6. package/src/audio/spoken-turn-wiring.ts +3 -0
  7. package/src/config/credential-availability.ts +1 -1
  8. package/src/config/index.ts +1 -1
  9. package/src/core/turn-cancellation.ts +1 -1
  10. package/src/daemon/cli.ts +4 -4
  11. package/src/daemon/service-commands.ts +8 -8
  12. package/src/input/command-registry.ts +5 -5
  13. package/src/input/commands/config.ts +1 -1
  14. package/src/input/commands/memory.ts +1 -1
  15. package/src/input/commands/planning-runtime.ts +1 -1
  16. package/src/input/commands/recall-review.ts +2 -2
  17. package/src/input/commands/session-content.ts +1 -1
  18. package/src/input/feed-context-factory.ts +1 -1
  19. package/src/input/handler-content-actions.ts +2 -2
  20. package/src/input/handler-feed-routes.ts +2 -2
  21. package/src/input/handler-feed.ts +1 -1
  22. package/src/input/handler-onboarding-daemon-adopt.ts +3 -3
  23. package/src/input/handler-picker-routes.ts +2 -2
  24. package/src/input/handler-shortcuts.ts +1 -1
  25. package/src/input/model-picker.ts +2 -2
  26. package/src/input/onboarding/onboarding-wizard-apply.ts +3 -3
  27. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +3 -3
  28. package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
  29. package/src/input/session-picker-modal.ts +1 -1
  30. package/src/input/settings-modal-data.ts +2 -2
  31. package/src/main.ts +5 -4
  32. package/src/panels/base-panel.ts +1 -1
  33. package/src/panels/builtin/operations.ts +4 -4
  34. package/src/panels/builtin/shared.ts +1 -1
  35. package/src/panels/fleet-panel-format.ts +1 -1
  36. package/src/panels/fleet-panel.ts +15 -15
  37. package/src/panels/fleet-read-model.ts +11 -11
  38. package/src/panels/fleet-steer.ts +2 -2
  39. package/src/panels/fleet-stop.ts +2 -2
  40. package/src/panels/fleet-tabs.ts +4 -4
  41. package/src/panels/fleet-transcript.ts +5 -5
  42. package/src/panels/types.ts +1 -1
  43. package/src/renderer/compaction-quality.ts +1 -1
  44. package/src/renderer/fleet-tab-strip.ts +2 -2
  45. package/src/renderer/footer-tips.ts +1 -1
  46. package/src/renderer/model-workspace.ts +1 -1
  47. package/src/renderer/session-picker-modal.ts +4 -4
  48. package/src/renderer/shell-surface.ts +1 -1
  49. package/src/renderer/status-glyphs.ts +3 -3
  50. package/src/renderer/terminal-bg-probe.ts +1 -1
  51. package/src/renderer/theme.ts +2 -2
  52. package/src/renderer/turn-injection.ts +3 -3
  53. package/src/renderer/ui-factory.ts +7 -7
  54. package/src/renderer/ui-primitives.ts +3 -3
  55. package/src/runtime/bootstrap-core.ts +3 -3
  56. package/src/runtime/bootstrap-hook-bridge.ts +2 -2
  57. package/src/runtime/bootstrap-shell.ts +3 -3
  58. package/src/runtime/bootstrap.ts +5 -5
  59. package/src/runtime/code-index-services.ts +2 -2
  60. package/src/runtime/index.ts +11 -3
  61. package/src/runtime/legacy-daemon-migration.ts +9 -9
  62. package/src/runtime/memory-fold.ts +1 -1
  63. package/src/runtime/onboarding/snapshot.ts +3 -3
  64. package/src/runtime/onboarding/types.ts +2 -2
  65. package/src/runtime/operator-token-cleanup.ts +2 -2
  66. package/src/runtime/orchestrator-core-services.ts +2 -2
  67. package/src/runtime/process-lifecycle.ts +19 -5
  68. package/src/runtime/services.ts +14 -14
  69. package/src/runtime/session-inbound-inputs.ts +1 -1
  70. package/src/runtime/session-spine-transport.ts +2 -2
  71. package/src/runtime/ui-services.ts +1 -1
  72. package/src/runtime/workstream-services.ts +1 -1
  73. package/src/runtime/wrfc-persistence.ts +2 -2
  74. package/src/shell/ui-openers.ts +2 -2
  75. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,15 +4,26 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.9.2] — 2026-07-06
8
+
9
+ Patch release fixing text-to-speech playback so spoken responses play start-to-finish without losing audio at either end.
10
+
11
+ ### Text-to-speech playback fixes
12
+ - The beginning of spoken output is no longer clipped: playback now waits for the audio player to actually be ready before sending it audio, and the player's read-ahead buffering was restored so the opening of a response survives normal startup delay.
13
+ - The end of spoken output is no longer cut off: audio now drains fully once a response finishes speaking naturally. When you quit while audio is still playing, the final bit of audio gets a bounded two-second window to finish before shutdown continues. Deliberate interrupts (stopping speech, starting a new response, Ctrl+C) still cut instantly as before. A spurious error message that could appear when stopping playback has also been removed.
14
+
15
+ ### Other
16
+ - Documentation and code comments were cleaned up for clarity; no behavior changes.
17
+
7
18
  ## [1.9.1] — 2026-07-06
8
19
 
9
- Follow-up patch that completes the W6-C1 credential-status adoption for the TUI. The 1.9.0 release pinned SDK 1.0.0 and unified memory (C2) but left the C1 client-side credential-status read for a follow-up; this release lands it. Ships against @pellux/goodvibes-sdk 1.0.0.
20
+ Follow-up patch that completes the client-side credential-status adoption for the TUI. The 1.9.0 release pinned SDK 1.0.0 and unified memory but left the client-side credential-status read for a follow-up; this release lands it. Ships against @pellux/goodvibes-sdk 1.0.0.
10
21
 
11
- ### Credential-status read as a daemon client (W6-C1)
22
+ ### Credential-status read as a daemon client
12
23
  - When the TUI acts as a client of an adopted external daemon (`HostServiceStatus` mode `external`), provider/model/secret STATUS is read from that daemon's `credentials.get` wire method instead of the TUI's own surfaceRoot store. The read carries only status metadata (key / configured / usable / source / secure) — never a secret value, mirroring the SDK's status-only wire contract. The env-only API-key posture and all secret value-reads that provider auth needs stay local and are untouched; only STATUS reads use the daemon path.
13
24
  - Honest degraded state, mirroring goodvibes-webui v1.0.1 `deriveCredentialAvailability` exactly: a 503 `CREDENTIAL_STORE_UNAVAILABLE` (matched by machine code), a `METHOD_NOT_FOUND` from an older daemon, or any transport failure resolves to a reason-carrying `available: false` — never a fabricated "configured", never a stale confident value. The TUI-hosted daemon continues to serve `credentials.get` automatically from its existing secrets manager (SDK auto-wiring), so no serve-side change was needed.
14
25
 
15
- ### Memory unification follow-up (W6-C2)
26
+ ### Memory unification follow-up
16
27
  - The legacy per-project TUI memory store now actually folds into the home-scoped canonical store once at boot, after the canonical store initializes — making good on the behavior 1.9.0 described. The fold is id-keyed and idempotent (a re-run imports nothing new and never deletes the legacy file) and is strictly non-fatal: a fold error degrades to a warning and the canonical store serves on. The home-scoped store deliberately does not re-root with the working directory; only the code index and project index do.
17
28
 
18
29
  ## [1.7.0] — 2026-07-04
@@ -77,7 +88,7 @@ The evolution release: six batched development cycles (internal 1.2.0–1.6.0, w
77
88
 
78
89
  ## [1.1.0] — 2026-07-03
79
90
 
80
- Wave 0 of the post-1.0 evolution effort: trust repairs. Every change closes a defect found by live dogfooding v1.0.0 where the product reported something other than the truth. Ships against @pellux/goodvibes-sdk 0.36.0 (same effort, SDK side).
91
+ The first cycle of the post-1.0 evolution effort: trust repairs. Every change closes a defect found by live dogfooding v1.0.0 where the product reported something other than the truth. Ships against @pellux/goodvibes-sdk 0.36.0 (same effort, SDK side).
81
92
 
82
93
  ### Fixed
83
94
  - Failing commands are visible again: the SDK masked every command-level failure as "Error: Unknown error", so a red test suite was literally invisible to the model — it would honestly conclude "no failure observed" while your tests were failing. Exit code, stdout, and stderr now reach the model byte-faithfully, and long-output truncation keeps the tail (where test runners print failures) instead of the head. Verified live: the model reads a sabotaged test's actual assertion diff and fixes the right side.
@@ -101,7 +112,7 @@ Wave 0 of the post-1.0 evolution effort: trust repairs. Every change closes a de
101
112
  ### Internal
102
113
  - SDK dependency 0.35.0 → 0.36.0; foundation artifacts regenerated (new event contract entries).
103
114
  - Local-SDK overlay dev tooling (`scripts/sdk-dev.ts link|status|restore`) with release-blocking gates in release.ts and publish-check so a local SDK can never ship.
104
- - Wave verified three ways: full gate block, a 3-lens adversarial code review, and a live tmux scenario replay of the five previously-failing dogfood scenarios (all PASS).
115
+ - Verified three ways: full gate block, a 3-lens adversarial code review, and a live tmux scenario replay of the five previously-failing dogfood scenarios (all PASS).
105
116
 
106
117
  ## [1.0.0] — 2026-07-03
107
118
 
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-1.9.1-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-1.9.2-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -15,6 +15,14 @@ export interface StreamingAudioPlayer {
15
15
  readonly available: boolean;
16
16
  play(chunks: AsyncIterable<VoiceAudioChunk>, options: StreamingAudioPlaybackOptions): Promise<void>;
17
17
  stop(): void;
18
+ /**
19
+ * Resolves once the currently playing sink has finished naturally (its
20
+ * process closed) or after `timeoutMs`, whichever comes first; resolves
21
+ * immediately when nothing is playing. The exit path uses this to let the
22
+ * audio the user is already hearing finish inside a short bounded window
23
+ * instead of killing it mid-drain. stop() remains the instant cut.
24
+ */
25
+ waitForDrain(timeoutMs: number): Promise<void>;
18
26
  }
19
27
 
20
28
  export interface StreamingAudioPlaybackOptions {
@@ -25,6 +33,8 @@ export interface StreamingAudioPlaybackOptions {
25
33
  interface SpawnProcess {
26
34
  readonly stdin: Writable;
27
35
  once(event: 'close', listener: () => void): this;
36
+ once(event: 'spawn', listener: () => void): this;
37
+ once(event: 'error', listener: (error: unknown) => void): this;
28
38
  kill(signal?: NodeJS.Signals | number): boolean;
29
39
  }
30
40
  type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnProcess;
@@ -32,6 +42,13 @@ type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnPr
32
42
  export interface LocalStreamingAudioPlayerOptions {
33
43
  readonly env?: NodeJS.ProcessEnv;
34
44
  readonly spawnProcess?: SpawnProcessFactory;
45
+ /**
46
+ * Injected player command — the test seam companion to `spawnProcess`.
47
+ * When set, the constructor uses it verbatim instead of scanning PATH, so
48
+ * deterministic fake-sink tests never depend on mpv/ffplay being installed
49
+ * on the machine running them. Pass null to model "no player found".
50
+ */
51
+ readonly command?: StreamingAudioPlayerCommand | null;
35
52
  }
36
53
 
37
54
  export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
@@ -40,7 +57,9 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
40
57
  private readonly spawnProcess: SpawnProcessFactory;
41
58
 
42
59
  constructor(options: LocalStreamingAudioPlayerOptions = {}) {
43
- this.command = resolveStreamingAudioPlayerCommand(options.env ?? process.env);
60
+ this.command = options.command !== undefined
61
+ ? options.command
62
+ : resolveStreamingAudioPlayerCommand(options.env ?? process.env);
44
63
  this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
45
64
  }
46
65
 
@@ -67,12 +86,27 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
67
86
  options.signal?.addEventListener('abort', abort, { once: true });
68
87
 
69
88
  try {
89
+ // Head survival: hold the first audio byte until the sink has actually
90
+ // started (its 'spawn' event) instead of writing into a process that has
91
+ // not exec'd yet. Writing before the player is up is the spawn race that
92
+ // clipped the beginning of playback. A spawn failure rejects here so the
93
+ // caller can report it honestly rather than swallowing a dead player.
94
+ await awaitReady(proc, options.signal);
95
+ if (options.signal?.aborted) return;
96
+
70
97
  for await (const chunk of chunks) {
71
98
  if (options.signal?.aborted) break;
72
99
  if (chunk.data.byteLength === 0) continue;
73
100
  await writeStdin(proc, chunk.data);
74
101
  }
75
- proc.stdin.end();
102
+
103
+ // An intentional interrupt (turn cancel / quit chord / /tts stop) has
104
+ // already torn the process down via `abort` and must cut immediately —
105
+ // do not wait on a graceful drain. A natural end-of-speech, by contrast,
106
+ // closes stdin and waits for the sink to play out every buffered sample
107
+ // so the tail of the response is never truncated.
108
+ if (options.signal?.aborted) return;
109
+ try { proc.stdin.end(); } catch { /* ignore */ }
76
110
  await waitForExit(proc);
77
111
  } finally {
78
112
  options.signal?.removeEventListener('abort', abort);
@@ -87,14 +121,33 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
87
121
  try { proc.stdin.destroy(); } catch { /* ignore */ }
88
122
  try { proc.kill('SIGTERM'); } catch { /* ignore */ }
89
123
  }
124
+
125
+ waitForDrain(timeoutMs: number): Promise<void> {
126
+ const proc = this.activeProcess;
127
+ if (!proc) return Promise.resolve();
128
+ return new Promise((resolve) => {
129
+ let settled = false;
130
+ const settle = () => {
131
+ if (settled) return;
132
+ settled = true;
133
+ clearTimeout(timer);
134
+ resolve();
135
+ };
136
+ const timer = setTimeout(settle, timeoutMs);
137
+ proc.once('close', settle);
138
+ });
139
+ }
90
140
  }
91
141
 
92
142
  export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = process.env): StreamingAudioPlayerCommand | null {
93
143
  const mpv = findExecutable('mpv', env);
94
144
  if (mpv) {
145
+ // No --cache=no: mpv's read-ahead cache buffers the incoming pipe so the
146
+ // opening audio survives device-open latency and network jitter instead of
147
+ // underrunning while the output device is still spinning up.
95
148
  return {
96
149
  command: mpv,
97
- args: ['--no-terminal', '--really-quiet', '--force-window=no', '--cache=no', '-'],
150
+ args: ['--no-terminal', '--really-quiet', '--force-window=no', '-'],
98
151
  label: 'mpv',
99
152
  };
100
153
  }
@@ -102,18 +155,51 @@ export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = proc
102
155
  if (ffplay) {
103
156
  return {
104
157
  command: ffplay,
105
- args: ['-nodisp', '-autoexit', '-loglevel', 'error', '-i', 'pipe:0'],
158
+ args: FFPLAY_BASE_ARGS,
106
159
  label: 'ffplay',
107
160
  };
108
161
  }
109
162
  return null;
110
163
  }
111
164
 
165
+ // ffplay -autoexit quits as soon as its input ends, before the audio output
166
+ // buffer has drained — that clips the tail of the response. `apad` appends a
167
+ // short run of silence so the real audio is fully played out and only the
168
+ // trailing silence gets trimmed.
169
+ const FFPLAY_APAD = ['-af', 'apad=pad_dur=0.3'] as const;
170
+ const FFPLAY_BASE_ARGS = ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-i', 'pipe:0'] as const;
171
+
112
172
  function buildPlayerArgs(command: StreamingAudioPlayerCommand, format?: string): readonly string[] {
113
173
  if (command.label !== 'ffplay' || !format) return command.args;
114
174
  const normalized = format.trim().toLowerCase();
115
175
  if (!normalized || normalized.includes('/')) return command.args;
116
- return ['-nodisp', '-autoexit', '-loglevel', 'error', '-f', normalized, '-i', 'pipe:0'];
176
+ return ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-f', normalized, '-i', 'pipe:0'];
177
+ }
178
+
179
+ /**
180
+ * awaitReady — resolves once the spawned player has actually started (its
181
+ * 'spawn' event), rejects if it fails to start ('error'), and resolves early
182
+ * if the caller aborts during startup so an intentional interrupt is never
183
+ * blocked. This is the readiness gate that keeps the first audio byte from
184
+ * being written into a not-yet-running sink.
185
+ */
186
+ function awaitReady(proc: SpawnProcess, signal?: AbortSignal): Promise<void> {
187
+ if (signal?.aborted) return Promise.resolve();
188
+ return new Promise<void>((resolve, reject) => {
189
+ let settled = false;
190
+ const settle = (action: () => void) => {
191
+ if (settled) return;
192
+ settled = true;
193
+ signal?.removeEventListener('abort', onAbort);
194
+ action();
195
+ };
196
+ const onSpawn = () => settle(resolve);
197
+ const onError = (error: unknown) => settle(() => reject(error instanceof Error ? error : new Error(String(error))));
198
+ const onAbort = () => settle(resolve);
199
+ proc.once('spawn', onSpawn);
200
+ proc.once('error', onError);
201
+ signal?.addEventListener('abort', onAbort, { once: true });
202
+ });
117
203
  }
118
204
 
119
205
  function findExecutable(name: string, env: NodeJS.ProcessEnv): string | null {
@@ -64,8 +64,8 @@ export class SpokenTurnController {
64
64
  /**
65
65
  * Returns whether speech was actually ACTIVE when stopped. The notice only
66
66
  * prints in that case — stop() on an idle controller used to notify anyway,
67
- * spamming "[TTS] Spoken output stopped." on every Ctrl+C (batch replay
68
- * D5/N-noise); callers use the return to decide whether the press "did a
67
+ * spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay
68
+ * fix); callers use the return to decide whether the press "did a
69
69
  * job" (see handleCtrlC's consume-on-speech-stop).
70
70
  */
71
71
  stop(message?: string): boolean {
@@ -85,6 +85,29 @@ export class SpokenTurnController {
85
85
  return wasActive;
86
86
  }
87
87
 
88
+ /**
89
+ * Exit-path teardown: drops everything not yet audible (pending arm,
90
+ * buffered text, queued chunks) but lets the audio the user is already
91
+ * hearing finish naturally, capped at `drainTimeoutMs`, before the hard
92
+ * stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their
93
+ * instant path through stop(); this is only for exiting the app while the
94
+ * final audio of a completed response is still draining.
95
+ */
96
+ async stopForExit(drainTimeoutMs = 2000): Promise<void> {
97
+ this.pendingPrompt = null;
98
+ this.activeTurnId = null;
99
+ this.chunker?.reset();
100
+ this.chunker = null;
101
+ this.stopTimer();
102
+ // Cancel chunks that have not started playing; the chunk currently in the
103
+ // sink is not in this set (its controller is released before playback).
104
+ for (const controller of this.abortControllers) controller.abort();
105
+ this.abortControllers.clear();
106
+ await this.player.waitForDrain(drainTimeoutMs);
107
+ // Backstop: anything still alive after the window is torn down hard.
108
+ this.stop();
109
+ }
110
+
88
111
  handleTurnEvent(event: TurnEvent): void {
89
112
  if (event.type === 'TURN_SUBMITTED') {
90
113
  this.maybeStartTurn(event.turnId, event.prompt);
@@ -170,6 +193,11 @@ export class SpokenTurnController {
170
193
  if (abortController.signal.aborted) return;
171
194
  const result = await resultPromise;
172
195
  this.abortControllers.delete(abortController);
196
+ // Re-check after the await: an abort that landed while synthesis was in
197
+ // flight (deliberate stop or exit) makes the rejection expected — it
198
+ // must not route into reportError, which would print a spurious error
199
+ // and hard-stop a sink that may still be draining the previous chunk.
200
+ if (abortController.signal.aborted) return;
173
201
  if (!result.ok) {
174
202
  this.reportError(result.error);
175
203
  return;
@@ -10,6 +10,8 @@ export interface SpokenTurnRuntime {
10
10
  submitNextTurn(prompt: string): boolean;
11
11
  /** Returns whether speech was actually active (see controller.stop). */
12
12
  stop(message?: string): boolean;
13
+ /** Exit-path stop: lets the audio already playing drain, bounded (see controller.stopForExit). */
14
+ stopForExit(drainTimeoutMs?: number): Promise<void>;
13
15
  }
14
16
 
15
17
  export interface WireSpokenTurnRuntimeOptions {
@@ -63,5 +65,6 @@ export function wireSpokenTurnRuntime(options: WireSpokenTurnRuntimeOptions): Sp
63
65
  unsubs,
64
66
  submitNextTurn: (prompt) => controller.submitNextTurn(prompt),
65
67
  stop: (message) => controller.stop(message),
68
+ stopForExit: (drainTimeoutMs) => controller.stopForExit(drainTimeoutMs),
66
69
  };
67
70
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * credential-availability.ts — W6-C1 client-side credential-status read.
2
+ * credential-availability.ts — client-side credential-status read.
3
3
  *
4
4
  * When the TUI acts as a CLIENT of an adopted external daemon (the host-service
5
5
  * `mode === 'external'` topology — see runtime/bootstrap.ts), provider/model/secret
@@ -56,7 +56,7 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
56
56
 
57
57
  export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
58
58
 
59
- // W6-C1: client-side credential-status read (honest-degrade, status-only). When the
59
+ // Client-side credential-status read (honest-degrade, status-only). When the
60
60
  // TUI acts as a client of an adopted external daemon it reads provider/model/secret
61
61
  // STATUS from that daemon's `credentials.get`, never from its own surfaceRoot store.
62
62
  export {
@@ -8,7 +8,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
8
8
  * cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
9
9
  *
10
10
  * Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
11
- * TTS is consumed by that job (batch replay D5), same as a press that
11
+ * TTS is consumed by that job (an earlier replay fix), same as a press that
12
12
  * cleared a non-empty prompt; the quit chord starts from a quiet state.
13
13
  */
14
14
  export function createCancelGeneration(
package/src/daemon/cli.ts CHANGED
@@ -163,7 +163,7 @@ async function main(): Promise<void> {
163
163
  logger.info('daemon: --hostname flag applied', { hostname: cliFlags.hostname });
164
164
  }
165
165
 
166
- // D7a Layer 1: `install-service` / `uninstall-service` / `service-status` manage
166
+ // Service lifecycle: `install-service` / `uninstall-service` / `service-status` manage
167
167
  // the systemd USER unit for the shared daemon. They run BEFORE the daemon boots —
168
168
  // no runtime/services are constructed — and exit with the honest result code.
169
169
  const serviceSubcommand = cli.positionals[0];
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
204
204
  homeDirectory,
205
205
  });
206
206
 
207
- // F2: Load persisted providers from disk so the provider registry is pre-populated
207
+ // Load persisted providers from disk so the provider registry is pre-populated
208
208
  // on standalone daemon startup (same machinery the TUI uses after /scan).
209
209
  const discoveryRoots = { homeDirectory, surfaceRoot: 'tui' };
210
210
  const persistedProviders = loadPersistedProviders(discoveryRoots);
@@ -213,7 +213,7 @@ async function main(): Promise<void> {
213
213
  logger.info('daemon: loaded persisted providers', { count: persistedProviders.length });
214
214
  }
215
215
 
216
- // F2: Run a background LAN scan (non-blocking). Discovered servers are registered
216
+ // Run a background LAN scan (non-blocking). Discovered servers are registered
217
217
  // and persisted so subsequent daemon restarts benefit from the warm cache.
218
218
  void scan().then((result) => {
219
219
  if (result.servers.length > 0) {
@@ -245,7 +245,7 @@ async function main(): Promise<void> {
245
245
  // If no explicit daemon token is set, use the companion token so mobile apps can connect.
246
246
  const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
247
247
  const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
248
- // F3 resolution (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
248
+ // Fix (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
249
249
  // token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
250
250
  const prune = pruneStaleOperatorTokens({
251
251
  daemonHomeDir,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * D7a Layer 1 (TUI wiring) — `goodvibes-daemon install-service | uninstall-service
2
+ * TUI wiring — `goodvibes-daemon install-service | uninstall-service
3
3
  * | service-status`.
4
4
  *
5
5
  * The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
@@ -7,8 +7,8 @@
7
7
  * on Windows) so N surfaces share ONE daemon that survives reboots, instead of
8
8
  * every surface spawning a session-scoped daemon.
9
9
  *
10
- * W3 drift note: the SDK's D7a-era `systemd-user-service.ts` (a Linux-only,
11
- * bespoke systemd shim) was dead code and got deleted in SDK W3-S5. The SDK's
10
+ * Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
11
+ * bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
12
12
  * REAL wired service machinery — reached in production by the daemon's own HTTP
13
13
  * `/api/service/*` routes via facade-composition.ts — is
14
14
  * `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
@@ -26,7 +26,7 @@
26
26
  * stray "enabled" symlink may remain until `systemctl --user daemon-reload`
27
27
  * (offered back as a suggested follow-up) or the next login cleans it up.
28
28
  *
29
- * W4-D1: a fourth subcommand, `migrate-service`, closes the Wave-3 "detect and
29
+ * A fourth subcommand, `migrate-service`, closes the earlier "detect and
30
30
  * disclose only" inheritance (W3 Finding 4, below) with a GUIDED, CONSENTED
31
31
  * takeover of the legacy `goodvibes-daemon.service` unit. Design constraints,
32
32
  * all load-bearing:
@@ -77,9 +77,9 @@ export {
77
77
  export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migration.ts';
78
78
 
79
79
  // ---------------------------------------------------------------------------
80
- // W3 Finding 4: legacy-unit detection. W4-D1: guided migration.
80
+ // Legacy-unit detection, plus guided migration.
81
81
  //
82
- // The prior D7a-era command installed the daemon's systemd unit under the
82
+ // The prior command installed the daemon's systemd unit under the
83
83
  // literal name `goodvibes-daemon.service`. This module (rewired onto
84
84
  // PlatformServiceManager, see the file banner above) manages a DIFFERENT
85
85
  // unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
@@ -243,7 +243,7 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
243
243
 
244
244
  /**
245
245
  * `migrate-service`: the guided, consented takeover of the legacy
246
- * `goodvibes-daemon.service` unit (W4-D1). Thin wrapper over
246
+ * `goodvibes-daemon.service` unit. Thin wrapper over
247
247
  * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) — see
248
248
  * that module for the design constraints (never auto-migrate,
249
249
  * new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
@@ -314,7 +314,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
314
314
  const extra: string[] = [];
315
315
  if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
316
316
  // W3 Finding 4: this command only ever touches the TRACKED unit
317
- // (whatever name/path actually resolved, per F2 — not necessarily the
317
+ // (whatever name/path actually resolved — not necessarily the
318
318
  // SERVICE_NAME constant) above — say so explicitly when a legacy unit
319
319
  // also exists, so its continued presence is never a silent surprise.
320
320
  if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
@@ -192,16 +192,16 @@ export interface CommandSessionServices {
192
192
  * fresh Orchestrator's zeroed default (W0.9).
193
193
  */
194
194
  readonly hydrateSessionUsage?: () => void;
195
- /** Wave 4 (wo703): the orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
195
+ /** The orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
196
196
  readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
197
- /** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
197
+ /** The repo source-tree code index — see runtime/code-index-services.ts. */
198
198
  readonly codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
199
- /** Wave-5 Stage B: tool-site reindex scheduler — `/codebase status` reports its last activity. */
199
+ /** Tool-site reindex scheduler — `/codebase status` reports its last activity. */
200
200
  readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
201
- /** Wave-5 Stage B: whether the (default-off) `agent-passive-code-injection` flag is on — for `/codebase status`. */
201
+ /** Whether the (default-off) `agent-passive-code-injection` flag is on — for `/codebase status`. */
202
202
  readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
203
203
  /**
204
- * Wave 5 (wo805): the MAIN interactive session's per-turn passive-injection
204
+ * The MAIN interactive session's per-turn passive-injection
205
205
  * honesty ring — `Orchestrator.getTurnInjections()`, the main-session
206
206
  * counterpart to `AgentRecord.turnInjections` (wo801). `/recall injections`
207
207
  * with no agent id reads this. Optional so command contexts built without an
@@ -10,7 +10,7 @@ export function registerConfigCommand(registry: CommandRegistry): void {
10
10
  usage: '[category|key] | set <key> <value>',
11
11
  argsHint: '[category|key] | set <key> <value>',
12
12
  handler(args, ctx) {
13
- // Batch replay D2: `/config set <key> <value>` used to fall through to
13
+ // An earlier replay fix: `/config set <key> <value>` used to fall through to
14
14
  // the workspace with the assignment silently ignored — the dishonest-
15
15
  // fallthrough class. `set` is now a real verb.
16
16
  if (args[0] === 'set') {
@@ -29,7 +29,7 @@ import { VALID_CLASSES, VALID_REVIEW_STATES, VALID_SCOPES } from './recall-share
29
29
  * src/input/commands/session-content.ts) with no modal surface — so that
30
30
  * work order deliberately did NOT touch /memory and used /recall instead.
31
31
  *
32
- * W6-C3 update (Wave 6 core-verb pass, MEMORY fragmentation — worst-class
32
+ * Update from the core-verb naming pass (MEMORY fragmentation — worst-class
33
33
  * collision #2): the agent's own `/memory` command was a plain alias for its
34
34
  * `/recall`-equivalent the whole time, meaning "/memory" meant two unrelated
35
35
  * things depending which surface you were on. The session-notes command was
@@ -262,7 +262,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
262
262
  return;
263
263
  }
264
264
 
265
- // Defense (Wave 6 review): a single verb-looking token is almost never a
265
+ // Defense (review finding): a single verb-looking token is almost never a
266
266
  // real planning goal — it is a mistyped or removed subcommand. The
267
267
  // Planning modal used to dispatch `/plan dismiss`, which has no
268
268
  // subcommand and silently fell through to this free-form branch, seeding
@@ -104,9 +104,9 @@ export function handleRecallExplain(args: string[], context: CommandContext): vo
104
104
  }
105
105
 
106
106
  /**
107
- * W5.2 (wo803) — per-turn passive knowledge injection inspectability.
107
+ * Per-turn passive knowledge injection inspectability.
108
108
  *
109
- * With an explicit agent id, reads `AgentRecord.turnInjections` (Wave-5 wo801)
109
+ * With an explicit agent id, reads `AgentRecord.turnInjections`
110
110
  * via `context.ops.agentManager.exportState()` — the CommandContext-exposed
111
111
  * `ShellAgentManagerService` doesn't have a `getStatus`/`list` pair, but
112
112
  * `exportState()` returns the same full `AgentRecord[]` (it's what
@@ -384,7 +384,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
384
384
  },
385
385
  });
386
386
 
387
- // W6-C3 (Wave 6 core-verb pass, MEMORY fragmentation — worst-class
387
+ // From the core-verb naming pass (MEMORY fragmentation — worst-class
388
388
  // collision #2): this command used to be named /memory, colliding with
389
389
  // the agent's /memory (an alias that forwards to /recall, the durable
390
390
  // cross-session MemoryStore) — same command name, two unrelated features.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
3
3
  *
4
- * Extracted from handler.ts (Wave 4α review, 0.18.23) to keep handler.ts under the
4
+ * Extracted from handler.ts (review follow-up, 0.18.23) to keep handler.ts under the
5
5
  * 800-line architecture cap.
6
6
  *
7
7
  * Two exported functions:
@@ -447,8 +447,8 @@ export function handleCtrlC(
447
447
  } catch {
448
448
  // Non-fatal to the exit chord; the quit-window bookkeeping still runs.
449
449
  }
450
- // A press that silenced LIVE speech is consumed by that job (batch replay
451
- // D5) — symmetric with the prompt-clearing press above. The quit chord
450
+ // A press that silenced LIVE speech is consumed by that job (an earlier replay
451
+ // fix) — symmetric with the prompt-clearing press above. The quit chord
452
452
  // ("Ctrl+C x2") starts from a quiet state; turn-aborts still count toward
453
453
  // the double-press.
454
454
  if (stoppedSpeech) return;
@@ -189,7 +189,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
189
189
  export type IndicatorFocusRouteState = {
190
190
  indicatorFocused: boolean;
191
191
  modalOpened: (name: string) => void;
192
- /** W2.2: the footer process indicator's [Enter] opens the Fleet panel (F2 also opens the Fleet panel; the retired process modal was removed in W6.2). */
192
+ /** The footer process indicator's [Enter] opens the Fleet panel (F2 also opens it; the process modal was retired). */
193
193
  openFleetPanel: () => void;
194
194
  requestRender: () => void;
195
195
  };
@@ -337,7 +337,7 @@ export type KeyRouteState = {
337
337
  commandRegistry?: CommandRegistry | null;
338
338
  autocomplete: AutocompleteEngine | null;
339
339
  blockActionsMenu: { open: (block: BlockMeta) => void };
340
- /** W6.2 e: F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
340
+ /** F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
341
341
  openFleetPanel: () => void;
342
342
  modalOpened: (name: string) => void;
343
343
  saveUndoState: () => void;
@@ -186,7 +186,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
186
186
  // focus to it. panelManager.open() only makes the panel active — focus is a
187
187
  // separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
188
188
  // focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
189
- // the footer indicator's [Enter] and by F2 (W6.2 e: F2 and the footer
189
+ // the footer indicator's [Enter] and by F2 (F2 and the footer
190
190
  // indicator both subsume the retired process modal). Mirrors the Ctrl+P
191
191
  // panel-picker launcher (ui-openers.ts openPanelPicker).
192
192
  const openFleetPanel = (): void => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * F1 (One-Platform Wave 2 friction fix): the onboarding wizard's "Connect to
2
+ * Friction fix: the onboarding wizard's "Connect to
3
3
  * this daemon now" action, wired from the Network step's `network.daemon-source`
4
4
  * = 'adopt' fields. Extracted from handler-onboarding.ts to keep that file
5
5
  * under the architecture line cap.
@@ -88,8 +88,8 @@ export async function handleConnectExistingDaemonForHandler(handler: InputHandle
88
88
  }
89
89
 
90
90
  /**
91
- * W4-D1: the guided UX entry point for migrating a legacy
92
- * `goodvibes-daemon.service` unit (Wave 3 shipped detect+disclose only; this
91
+ * The guided UX entry point for migrating a legacy
92
+ * `goodvibes-daemon.service` unit (an earlier release shipped detect+disclose only; this
93
93
  * closes that inheritance). This is a thin wrapper over
94
94
  * `detectLegacyUnit` + `runLegacyDaemonMigration`
95
95
  * (`../runtime/legacy-daemon-migration.ts`) — the actual migration mechanics
@@ -22,7 +22,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
22
22
 
23
23
  if (token.type === 'key') {
24
24
  if (token.logicalName === 'escape') {
25
- // W3-T2: search now starts focused by default (see
25
+ // Search now starts focused by default (see
26
26
  // ModelPickerModal.openAllModels() doc comment). Escape with an empty
27
27
  // query used to only blurSearch() here, leaving the picker open and
28
28
  // requiring a SECOND Escape to actually close it — confusing on a
@@ -133,7 +133,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
133
133
  state.modelPicker.focusSearch();
134
134
  } else if (state.modelPicker.searchFocused) {
135
135
  // Symmetric with 'down' (below): search now starts focused by default
136
- // (W3-T2), so 'up' must have a way out of it too, or it would be a
136
+ // so 'up' must have a way out of it too, or it would be a
137
137
  // silent no-op the very first time a user reaches for the list.
138
138
  state.modelPicker.blurSearch();
139
139
  } else {
@@ -121,7 +121,7 @@ export function handleGlobalShortcutToken(
121
121
  case 'panel-close': {
122
122
  const pm = state.panelManager;
123
123
  const active = pm.getActivePanel();
124
- // Wave-3: give the active panel a chance to consume Ctrl+X for an
124
+ // Give the active panel a chance to consume Ctrl+X for an
125
125
  // in-panel action (FleetPanel session-tab detach) before it closes the
126
126
  // panel outright — see Panel.interceptPanelClose's doc comment.
127
127
  if (active?.interceptPanelClose?.()) {
@@ -264,7 +264,7 @@ export class ModelPickerModal {
264
264
  /**
265
265
  * Open showing all models — entry point for /model.
266
266
  *
267
- * W3-T2: search starts FOCUSED. Live tmux repro (cc-w3-t2) showed the actual
267
+ * Search starts FOCUSED. A live tmux repro showed the actual
268
268
  * friction wasn't the filter itself (it already works — fuzzy multi-word
269
269
  * substring over id/displayName/provider) but that with search unfocused by
270
270
  * default, typing a search term went character-by-character into single-key
@@ -295,7 +295,7 @@ export class ModelPickerModal {
295
295
  this.scrollOffset = 0;
296
296
  }
297
297
 
298
- /** Open showing providers first — entry point for /provider. W3-T2: search starts focused — see openAllModels(). */
298
+ /** Open showing providers first — entry point for /provider. Search starts focused — see openAllModels(). */
299
299
  openProviders(providers: string[], currentProvider: string): void {
300
300
  this.previousMode = null;
301
301
  this.providers = providers;