@pellux/goodvibes-tui 1.7.0 → 1.9.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 (137) hide show
  1. package/README.md +10 -9
  2. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  3. package/package.json +2 -2
  4. package/src/audio/spoken-turn-controller.ts +12 -2
  5. package/src/audio/spoken-turn-wiring.ts +2 -1
  6. package/src/cli/help.ts +8 -1
  7. package/src/cli/service-posture.ts +2 -1
  8. package/src/cli/status.ts +2 -1
  9. package/src/cli/surface-command.ts +2 -2
  10. package/src/core/composer-state.ts +11 -3
  11. package/src/core/conversation-line-cache.ts +27 -3
  12. package/src/core/conversation-rendering.ts +71 -14
  13. package/src/core/stream-event-wiring.ts +20 -1
  14. package/src/core/system-message-noise.ts +87 -0
  15. package/src/core/system-message-router.ts +68 -1
  16. package/src/core/turn-cancellation.ts +7 -2
  17. package/src/core/turn-event-wiring.ts +10 -2
  18. package/src/daemon/cli.ts +29 -2
  19. package/src/daemon/handlers/register.ts +8 -1
  20. package/src/daemon/service-commands.ts +329 -0
  21. package/src/input/autocomplete.ts +27 -1
  22. package/src/input/command-registry.ts +46 -4
  23. package/src/input/commands/codebase-runtime.ts +46 -6
  24. package/src/input/commands/config.ts +43 -3
  25. package/src/input/commands/health-runtime.ts +9 -1
  26. package/src/input/commands/memory.ts +68 -35
  27. package/src/input/commands/operator-panel-runtime.ts +31 -7
  28. package/src/input/commands/planning-runtime.ts +95 -6
  29. package/src/input/commands/qrcode-runtime.ts +25 -5
  30. package/src/input/commands/remote-runtime-setup.ts +5 -3
  31. package/src/input/commands/session-content.ts +20 -9
  32. package/src/input/commands/settings-sync-runtime.ts +15 -3
  33. package/src/input/commands/shell-core.ts +10 -1
  34. package/src/input/commands/workstream-runtime.ts +168 -18
  35. package/src/input/config-modal-types.ts +15 -1
  36. package/src/input/config-modal.ts +227 -12
  37. package/src/input/feed-context-factory.ts +3 -0
  38. package/src/input/handler-command-route.ts +10 -3
  39. package/src/input/handler-content-actions.ts +17 -2
  40. package/src/input/handler-feed-routes.ts +43 -121
  41. package/src/input/handler-feed.ts +32 -4
  42. package/src/input/handler-modal-routes.ts +78 -13
  43. package/src/input/handler-modal-stack.ts +11 -2
  44. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  45. package/src/input/handler-onboarding.ts +71 -59
  46. package/src/input/handler-picker-routes.ts +15 -8
  47. package/src/input/handler-shortcuts.ts +59 -9
  48. package/src/input/handler.ts +6 -1
  49. package/src/input/keybindings.ts +6 -5
  50. package/src/input/model-picker.ts +19 -2
  51. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  52. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  53. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  54. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  55. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  56. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  57. package/src/input/panel-mouse-geometry.ts +97 -0
  58. package/src/input/panel-paste-flood-guard.ts +86 -0
  59. package/src/input/selection-modal.ts +11 -0
  60. package/src/input/session-picker-modal.ts +44 -4
  61. package/src/input/settings-modal-data.ts +51 -2
  62. package/src/input/settings-modal-types.ts +4 -2
  63. package/src/main.ts +28 -27
  64. package/src/panels/builtin/shared.ts +9 -3
  65. package/src/panels/fleet-deep-link.ts +31 -0
  66. package/src/panels/fleet-panel-format.ts +62 -0
  67. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  68. package/src/panels/fleet-panel.ts +89 -131
  69. package/src/panels/fleet-read-model.ts +43 -0
  70. package/src/panels/fleet-steer.ts +35 -2
  71. package/src/panels/fleet-stop.ts +29 -1
  72. package/src/panels/modals/keybindings-modal.ts +16 -1
  73. package/src/panels/modals/modal-theme.ts +35 -29
  74. package/src/panels/modals/pairing-modal.ts +25 -6
  75. package/src/panels/modals/planning-modal.ts +43 -21
  76. package/src/panels/modals/work-plan-modal.ts +28 -0
  77. package/src/panels/panel-manager.ts +15 -4
  78. package/src/panels/polish-core.ts +38 -25
  79. package/src/panels/project-planning-answer-actions.ts +8 -13
  80. package/src/panels/types.ts +24 -0
  81. package/src/permissions/prompt.ts +160 -13
  82. package/src/renderer/autocomplete-overlay.ts +28 -2
  83. package/src/renderer/compositor.ts +2 -3
  84. package/src/renderer/config-modal.ts +19 -5
  85. package/src/renderer/footer-tips.ts +5 -1
  86. package/src/renderer/fullscreen-primitives.ts +32 -22
  87. package/src/renderer/git-status.ts +3 -1
  88. package/src/renderer/layout.ts +0 -4
  89. package/src/renderer/markdown.ts +7 -3
  90. package/src/renderer/modal-factory.ts +25 -20
  91. package/src/renderer/model-workspace.ts +18 -3
  92. package/src/renderer/overlay-box.ts +21 -17
  93. package/src/renderer/process-indicator.ts +14 -3
  94. package/src/renderer/selection-modal-overlay.ts +6 -1
  95. package/src/renderer/session-picker-modal.ts +196 -3
  96. package/src/renderer/settings-modal-helpers.ts +2 -0
  97. package/src/renderer/settings-modal.ts +7 -0
  98. package/src/renderer/shell-surface.ts +8 -1
  99. package/src/renderer/status-glyphs.ts +14 -15
  100. package/src/renderer/system-message.ts +15 -3
  101. package/src/renderer/terminal-bg-probe.ts +339 -0
  102. package/src/renderer/terminal-escapes.ts +20 -0
  103. package/src/renderer/theme-mode-config.ts +67 -0
  104. package/src/renderer/theme.ts +91 -1
  105. package/src/renderer/thinking.ts +11 -3
  106. package/src/renderer/tool-call.ts +15 -9
  107. package/src/renderer/tool-result-summary.ts +148 -0
  108. package/src/renderer/turn-injection.ts +22 -3
  109. package/src/renderer/ui-factory.ts +154 -85
  110. package/src/renderer/ui-primitives.ts +30 -129
  111. package/src/runtime/bootstrap-command-context.ts +6 -0
  112. package/src/runtime/bootstrap-command-parts.ts +8 -4
  113. package/src/runtime/bootstrap-core.ts +33 -11
  114. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  115. package/src/runtime/bootstrap-shell.ts +19 -1
  116. package/src/runtime/bootstrap.ts +118 -5
  117. package/src/runtime/code-index-services.ts +25 -2
  118. package/src/runtime/legacy-daemon-migration.ts +516 -0
  119. package/src/runtime/memory-fold.ts +26 -0
  120. package/src/runtime/onboarding/derivation.ts +7 -2
  121. package/src/runtime/onboarding/snapshot.ts +27 -1
  122. package/src/runtime/onboarding/types.ts +31 -1
  123. package/src/runtime/operator-token-cleanup.ts +82 -1
  124. package/src/runtime/orchestrator-core-services.ts +10 -0
  125. package/src/runtime/resume-notice.ts +209 -0
  126. package/src/runtime/services.ts +12 -8
  127. package/src/runtime/session-inbound-inputs.ts +252 -0
  128. package/src/runtime/session-spine-transport.ts +64 -0
  129. package/src/runtime/terminal-output-guard.ts +15 -8
  130. package/src/runtime/ui-services.ts +19 -3
  131. package/src/runtime/workstream-services.ts +160 -28
  132. package/src/runtime/wrfc-persistence.ts +124 -17
  133. package/src/shell/blocking-input.ts +46 -3
  134. package/src/shell/recovery-input-helpers.ts +170 -1
  135. package/src/shell/ui-openers.ts +42 -9
  136. package/src/utils/terminal-width.ts +52 -0
  137. package/src/version.ts +1 -1
@@ -8,7 +8,7 @@ import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/pe
8
8
  import type { SelectionItem, SelectionResult, SelectionAction } from './selection-modal.ts';
9
9
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
10
10
  import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
11
- import type { PanelManager } from '../panels/panel-manager.ts';
11
+ import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
12
12
  import type { KeybindingsManager } from './keybindings.ts';
13
13
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
14
14
  import type { OpenOnboardingWizardOptions } from './handler-ui-state.ts';
@@ -48,6 +48,13 @@ export interface CommandRuntimeState {
48
48
  systemPrompt: string;
49
49
  reasoningEffort: string;
50
50
  sessionId: string;
51
+ /**
52
+ * Cumulative count of direct terminal writes the output guard intercepted
53
+ * this session — surfaced by /debug. Optional so the SDK's MutableRuntimeState
54
+ * (which has no such field) stays assignable; the guard sets it lazily on the
55
+ * shared runtime object. (UX-B item 1a.)
56
+ */
57
+ terminalWritesIntercepted?: number;
51
58
  }
52
59
 
53
60
  /**
@@ -115,7 +122,7 @@ export interface CommandShellUiOpeners {
115
122
  openSelection?: (
116
123
  title: string,
117
124
  items: SelectionItem[],
118
- opts: { preSelectId?: string; allowSearch?: boolean; customActions?: Map<string, SelectionAction> } | undefined,
125
+ opts: { preSelectId?: string; allowSearch?: boolean; customActions?: Map<string, SelectionAction>; primaryVerbLabel?: string } | undefined,
119
126
  callback: (result: SelectionResult | null) => void,
120
127
  ) => void;
121
128
  openSettingsModal?: (target?: string) => void;
@@ -131,7 +138,16 @@ export interface CommandShellUiOpeners {
131
138
  openShortcutsOverlay?: () => void;
132
139
  getScrollTop?: () => number;
133
140
  openPanelPicker?: () => void;
134
- showPanel?: (panelId: string, pane?: 'top' | 'bottom') => void;
141
+ /**
142
+ * Open (and optionally focus) a panel. UX-C focus rule: the command path is
143
+ * "the user is mid-command-flow" — opening a panel this way leaves keyboard
144
+ * focus in the composer by default. Pass `{ focus: true }` for a caller that
145
+ * genuinely wants to grab focus (chords use panelManager.focusPanels()
146
+ * directly instead of this method, so no current call site needs it — but
147
+ * the intent is explicit rather than implicit here). `target` (DEBT-5) is a
148
+ * fleet deep-link jump target forwarded to PanelManager.open.
149
+ */
150
+ showPanel?: (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget, opts?: { focus?: boolean }) => void;
135
151
  focusPanels?: () => void;
136
152
  focusPrompt?: () => void;
137
153
  openOpsPanel?: () => void;
@@ -180,6 +196,10 @@ export interface CommandSessionServices {
180
196
  readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
181
197
  /** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
182
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. */
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`. */
202
+ readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
183
203
  /**
184
204
  * Wave 5 (wo805): the MAIN interactive session's per-turn passive-injection
185
205
  * honesty ring — `Orchestrator.getTurnInjections()`, the main-session
@@ -272,6 +292,23 @@ export interface CommandContext
272
292
  };
273
293
  }
274
294
 
295
+ /**
296
+ * UX-C palette curation (item 4): the "common" first tier the slash-command
297
+ * autocomplete dropdown shows before the alphabetical rest when it opens with
298
+ * no filter typed yet (bare '/') — the "132-command palette unranked"
299
+ * evaluator finding. Typed filtering (any non-empty query) is completely
300
+ * unaffected — fuzzyMatch still searches every registered command exactly as
301
+ * before; this only reorders the empty-query case. Curated for breadth across
302
+ * the product's main workflows rather than raw usage frequency: help/config
303
+ * (orientation), panel/model (workspace + provider), recall/codebase/search
304
+ * (knowledge), workstream/checkpoint (control-plane), imagine (generation),
305
+ * sessions (continuity), quit (exit).
306
+ */
307
+ export const COMMON_COMMAND_NAMES: ReadonlySet<string> = new Set([
308
+ 'help', 'config', 'panel', 'model', 'recall', 'codebase',
309
+ 'workstream', 'checkpoint', 'search', 'imagine', 'sessions', 'quit',
310
+ ]);
311
+
275
312
  /**
276
313
  * SlashCommand - A single slash command definition.
277
314
  */
@@ -374,7 +411,12 @@ export class CommandRegistry {
374
411
  }
375
412
 
376
413
  if (bestScore > 0 || q === '') {
377
- results.push({ command: cmd, score: q === '' ? 1 : bestScore });
414
+ // UX-C: with no query yet, rank the curated common tier (score 2)
415
+ // ahead of everything else (score 1) — the tie-break below then sorts
416
+ // each tier alphabetically, so the result is "common tier, then the
417
+ // alphabetical rest" rather than one flat alphabetical list.
418
+ const score = q === '' ? (COMMON_COMMAND_NAMES.has(cmd.name) ? 2 : 1) : bestScore;
419
+ results.push({ command: cmd, score });
378
420
  }
379
421
  }
380
422
 
@@ -14,11 +14,12 @@
14
14
  // node while it runs), status (counts, skips, degradation state, last
15
15
  // build), search <query> (explicit retrieval, results labeled
16
16
  // 'lexical'|'semantic' honestly — never implied as more precise than they
17
- // are). This is Stage A only: no auto-injection into coding turns (Stage B,
18
- // out of scope see the wo802/W5.3 design doc).
17
+ // are). Stage B (now landed) adds passive auto-injection into coding turns and
18
+ // tool-site incremental reindex; `status` surfaces both honestly (the
19
+ // auto-injection flag+setting gate, and the last reindex activity).
19
20
  // ---------------------------------------------------------------------------
20
21
 
21
- import type { CodeIndexStats, CodeContextResult } from '@pellux/goodvibes-sdk/platform/state';
22
+ import type { CodeIndexStats, CodeContextResult, CodeIndexReindexActivity } from '@pellux/goodvibes-sdk/platform/state';
22
23
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
23
24
  import { isCodeIndexAutoStartEnabled, CODE_INDEX_MAX_FILES, CODE_INDEX_MAX_FILE_BYTES, CODE_INDEX_MAX_TOTAL_BYTES } from '../../runtime/code-index-services.ts';
24
25
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
@@ -33,7 +34,40 @@ function formatBytes(bytes: number): string {
33
34
  return `${bytes}B`;
34
35
  }
35
36
 
36
- function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<ConfigManager, 'get'>): string {
37
+ /**
38
+ * Honest one-line auto-injection state (Stage B): both the (default-off)
39
+ * `agent-passive-code-injection` feature flag AND storage.codeIndexEnabled must
40
+ * be on for passive code injection to run — state which is off, and why.
41
+ */
42
+ function renderAutoInjectionState(flagEnabled: boolean, settingEnabled: boolean): string {
43
+ if (flagEnabled && settingEnabled) {
44
+ return ' auto-injection: on (code hits may be injected into coding turns within the shared knowledge budget)';
45
+ }
46
+ const reasons: string[] = [];
47
+ if (!flagEnabled) reasons.push('agent-passive-code-injection flag off (default off) — enable in the settings modal flags section');
48
+ if (!settingEnabled) reasons.push('storage.codeIndexEnabled off — /config to change');
49
+ return ` auto-injection: off (${reasons.join('; ')})`;
50
+ }
51
+
52
+ /** Honest last-reindex activity line (Stage B tool-site incremental reindex). */
53
+ function renderReindexActivity(activity: CodeIndexReindexActivity | null, now: number): string {
54
+ if (!activity) return ' last reindex: none this session (writes/edits reindex touched files once the index is built)';
55
+ const agoSec = Math.max(0, Math.round((now - activity.at) / 1000));
56
+ const detail = activity.status === 'error'
57
+ ? `error: ${activity.error ?? 'unknown'}`
58
+ : activity.status === 'skipped'
59
+ ? `skipped (${activity.mode ?? 'empty'})`
60
+ : `indexed (${activity.mode ?? 'symbols'})`;
61
+ return ` last reindex: ${activity.path} — ${detail}, ${agoSec}s ago`;
62
+ }
63
+
64
+ interface CodeIndexStatusExtras {
65
+ readonly flagEnabled: boolean;
66
+ readonly reindexActivity: CodeIndexReindexActivity | null;
67
+ readonly now?: number;
68
+ }
69
+
70
+ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<ConfigManager, 'get'>, extras: CodeIndexStatusExtras): string {
37
71
  const lines: string[] = [];
38
72
  lines.push(`Code index — backend: ${stats.backend}, available: ${stats.available ? 'yes' : 'no'}`);
39
73
  lines.push(` path: ${stats.path}`);
@@ -48,6 +82,9 @@ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<Config
48
82
  lines.push(
49
83
  ` auto-build on startup: ${autoStart ? 'on' : 'off'} (storage.codeIndexEnabled, default off — /config to change)`,
50
84
  );
85
+ // Stage B surfacing: passive code auto-injection state (flag + setting) and the last tool-site reindex.
86
+ lines.push(renderAutoInjectionState(extras.flagEnabled, autoStart));
87
+ lines.push(renderReindexActivity(extras.reindexActivity, extras.now ?? Date.now()));
51
88
  lines.push(
52
89
  ` bounds: max ${CODE_INDEX_MAX_FILES} files (maxFiles), ${formatBytes(CODE_INDEX_MAX_FILE_BYTES)} per file (maxFileBytes),`
53
90
  + ` ${formatBytes(CODE_INDEX_MAX_TOTAL_BYTES)} total per build (maxTotalBytes)`,
@@ -133,7 +170,7 @@ const USAGE = 'Usage:\n'
133
170
  export function registerCodebaseRuntimeCommands(registry: CommandRegistry): void {
134
171
  registry.register({
135
172
  name: 'codebase',
136
- description: 'Repo source-tree code index — build, inspect, and search (Wave-5 W5.3 Stage A)',
173
+ description: 'Repo source-tree code index — build, inspect, and search',
137
174
  usage: 'build | status | search <query...> [--limit n]',
138
175
  argsHint: 'build | status | search <query>',
139
176
  handler(args: string[], ctx: CommandContext) {
@@ -146,7 +183,10 @@ export function registerCodebaseRuntimeCommands(registry: CommandRegistry): void
146
183
  const sub = args[0];
147
184
 
148
185
  if (!sub || sub === 'status') {
149
- ctx.print(renderCodeIndexStatus(store.stats(), ctx.platform.configManager));
186
+ ctx.print(renderCodeIndexStatus(store.stats(), ctx.platform.configManager, {
187
+ flagEnabled: ctx.session.isPassiveCodeInjectionFlagEnabled?.() ?? false,
188
+ reindexActivity: ctx.session.codeIndexReindexScheduler?.lastActivity() ?? null,
189
+ }));
150
190
  return;
151
191
  }
152
192
 
@@ -1,13 +1,45 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
+ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
3
+ import { setActiveThemeMode, type ThemeMode } from '../../renderer/theme.ts';
2
4
 
3
5
  export function registerConfigCommand(registry: CommandRegistry): void {
4
6
  registry.register({
5
7
  name: 'config',
6
8
  aliases: ['cfg'],
7
- description: 'Open the fullscreen configuration workspace',
8
- usage: '[category|key]',
9
- argsHint: '[category|key]',
9
+ description: 'Open the fullscreen configuration workspace, or set a key directly',
10
+ usage: '[category|key] | set <key> <value>',
11
+ argsHint: '[category|key] | set <key> <value>',
10
12
  handler(args, ctx) {
13
+ // Batch replay D2: `/config set <key> <value>` used to fall through to
14
+ // the workspace with the assignment silently ignored — the dishonest-
15
+ // fallthrough class. `set` is now a real verb.
16
+ if (args[0] === 'set') {
17
+ const key = args[1];
18
+ const value = args.slice(2).join(' ');
19
+ if (!key || value === '') {
20
+ ctx.print('Usage: /config set <key> <value> — e.g. /config set display.themeMode light');
21
+ return;
22
+ }
23
+ try {
24
+ const before = ctx.platform.configManager.get(key as ConfigKey);
25
+ ctx.platform.configManager.setDynamic(key as ConfigKey, coerceConfigValue(value));
26
+ const after = ctx.platform.configManager.get(key as ConfigKey);
27
+ ctx.print(`Set ${key}: ${JSON.stringify(before)} → ${JSON.stringify(after)}`);
28
+ // Forced theme modes apply immediately (matches the settings-modal
29
+ // path); 'auto' re-probes on the next launch (stated honestly).
30
+ if (key === 'display.themeMode') {
31
+ if (after === 'dark' || after === 'light') {
32
+ setActiveThemeMode(after as ThemeMode);
33
+ ctx.print('Theme applied. Note: transcript, modal, and header/footer/thinking chrome all flip; only the background colour follows your terminal.');
34
+ } else {
35
+ ctx.print('Theme mode "auto" probes the terminal background on the next launch.');
36
+ }
37
+ }
38
+ } catch (e) {
39
+ ctx.print(`Could not set ${key}: ${e instanceof Error ? e.message : String(e)}`);
40
+ }
41
+ return;
42
+ }
11
43
  if (ctx.openSettingsModal) {
12
44
  ctx.openSettingsModal(args[0]);
13
45
  return;
@@ -16,3 +48,11 @@ export function registerConfigCommand(registry: CommandRegistry): void {
16
48
  },
17
49
  });
18
50
  }
51
+
52
+ /** Coerce CLI text to the JSON-ish scalar the config schema expects. */
53
+ function coerceConfigValue(raw: string): unknown {
54
+ if (raw === 'true') return true;
55
+ if (raw === 'false') return false;
56
+ if (raw !== '' && Number.isFinite(Number(raw))) return Number(raw);
57
+ return raw;
58
+ }
@@ -39,7 +39,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
39
39
  name: 'health',
40
40
  aliases: ['doctor'],
41
41
  description: 'Health workspace for startup posture, service readiness, sandbox posture, and provider health',
42
- usage: '[open|review|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|term|repair [domain]]',
42
+ usage: '[report|review|open|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|term|repair [domain]] — bare and report stay a cross-domain transcript report (see also /health provider for the providers modal)',
43
43
  async handler(args, ctx) {
44
44
  const sub = (args[0] ?? 'review').toLowerCase();
45
45
  const readModels = requireReadModels(ctx);
@@ -424,6 +424,14 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
424
424
 
425
425
  ctx.print([
426
426
  'Health Review',
427
+ // DEBT-5 item 3: /health stays a cross-domain transcript report (it
428
+ // genuinely spans settings/accounts/auth/sandbox/etc — no single
429
+ // modal owns this data), but the providers domain DOES have one, so
430
+ // point at it honestly. "/provider" (no domain) is a DIFFERENT
431
+ // command (switch/manage custom providers) — the real front door to
432
+ // the providers modal is /health provider (see sub === 'provider'
433
+ // above), so that is what this hint names.
434
+ ' see also: the providers modal — /health provider',
427
435
  ` session: ${snapshot.sessionId}`,
428
436
  ` setup issues: ${snapshot.issues.length}`,
429
437
  ` service issues: ${snapshot.serviceIssues.length}`,
@@ -22,16 +22,77 @@ import { VALID_CLASSES, VALID_REVIEW_STATES, VALID_SCOPES } from './recall-share
22
22
 
23
23
  // ── Top-level command ─────────────────────────────────────────────────────────
24
24
 
25
+ /**
26
+ * DEBT-5 item 3 divergence note (historical): the work order that shipped
27
+ * this command named its front door "/memory", but at the time `/memory` was
28
+ * already a distinct, unrelated command (session-pinned sticky notes,
29
+ * src/input/commands/session-content.ts) with no modal surface — so that
30
+ * work order deliberately did NOT touch /memory and used /recall instead.
31
+ *
32
+ * W6-C3 update (Wave 6 core-verb pass, MEMORY fragmentation — worst-class
33
+ * collision #2): the agent's own `/memory` command was a plain alias for its
34
+ * `/recall`-equivalent the whole time, meaning "/memory" meant two unrelated
35
+ * things depending which surface you were on. The session-notes command was
36
+ * renamed to `/note` (session-content.ts) to free the word, and `/memory` is
37
+ * now registered here as a real alias of `/recall` — the word means the same
38
+ * durable Project Memory Substrate on both surfaces. The modal that exists
39
+ * for this data — the Project Memory Substrate — is still `memory-modal.ts`,
40
+ * owned by THIS command, confirmed by the panel-id redirect
41
+ * `registerModalRedirect('memory', 'memory-modal')` in builtin-modals.ts.
42
+ */
43
+ function printRecallUsage(context: CommandContext): void {
44
+ const usage = [
45
+ 'Usage: /recall <subcommand>',
46
+ ' add <class> <summary> [--scope <session|project|team>] [--detail <text>] [--tags <t,t>] [--session <id>] [--task <id>] [--file <path>]',
47
+ ` classes: ${VALID_CLASSES.join(', ')}`,
48
+ ' capture incident <id|latest> — Capture a forensics incident as durable memory',
49
+ ' capture policy — Capture the latest policy preflight review as durable memory',
50
+ ' capture mcp <server> — Capture MCP trust/quarantine posture as durable memory',
51
+ ' capture plugin <name> — Capture plugin trust/quarantine posture as durable memory',
52
+ ' search [query] [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>] — Full-text or sqlite-vec semantic search',
53
+ ' vector [status|doctor|rebuild] — Inspect or rebuild the sqlite-vec memory index',
54
+ ' get <id> — Show record with provenance + links',
55
+ ' link <fromId> <toId> <relation> — Create a directed relation between records',
56
+ ' queue [limit] — Show the operator review queue',
57
+ ' review <id> <state> [--confidence <n>] [--by <name>] [--reason <text>]',
58
+ ' stale <id> [reason...] — Mark a record stale with an operator reason',
59
+ ' contradict <id> [reason...] — Mark a record contradicted with an operator reason',
60
+ ' explain <task...> [--scope <path> ...] — Show the knowledge records that would be injected for a task',
61
+ ' injections [agentId] — Show per-turn passive knowledge injection records; no id shows the main session, an id shows that spawned agent',
62
+ ' promote <id> <scope> — Promote a memory record into session|project|team scope',
63
+ ' export <path> [--scope <scope>] [--cls <class>] — Export a durable knowledge bundle',
64
+ ' import <path> — Import a durable knowledge bundle',
65
+ ' handoff-export <path> [--scope <scope>] — Export a reviewable handoff bundle for team/shared use',
66
+ ' handoff-inspect <path> — Inspect a handoff bundle before import',
67
+ ' handoff-import <path> — Import a handoff bundle into durable memory',
68
+ ' list [class] [--scope <scope>] — List all records grouped by class',
69
+ ' remove <id> — Delete a record',
70
+ ].join('\n');
71
+ context.print(usage);
72
+ }
73
+
25
74
  export const recallCommand: SlashCommand = {
26
75
  name: 'recall',
27
- aliases: ['rc'],
28
- description: 'Project memory: add decisions, constraints, incidents, and patterns with provenance',
29
- usage: '<subcommand> [args]',
30
- argsHint: 'add|search|link|get|list|remove',
76
+ aliases: ['rc', 'memory', 'mem'],
77
+ description: 'Bare opens the Memory modal; project memory subcommands add decisions, constraints, incidents, and patterns with provenance',
78
+ usage: '[<subcommand> [args]] — bare opens the modal; report prints the subcommand usage text',
79
+ argsHint: 'add|search|link|get|list|remove|report',
31
80
  handler: async (args: string[], context: CommandContext): Promise<void> => {
32
81
  const [sub, ...rest] = args;
33
82
 
83
+ // DEBT-5 item 3: bare `/recall` opens the memory-modal surface — the old
84
+ // bare/unknown-subcommand usage block moved to an explicit `report`
85
+ // subcommand (scriptability preserved: /recall report).
86
+ if (sub === undefined) {
87
+ context.openModal?.('memory-modal');
88
+ return;
89
+ }
90
+
34
91
  switch (sub) {
92
+ case 'report':
93
+ printRecallUsage(context);
94
+ break;
95
+
35
96
  case 'add':
36
97
  await handleRecallAdd(rest, context);
37
98
  break;
@@ -120,37 +181,9 @@ export const recallCommand: SlashCommand = {
120
181
  await handleRecallHandoffImport(rest, context);
121
182
  break;
122
183
 
123
- default: {
124
- const usage = [
125
- 'Usage: /recall <subcommand>',
126
- ' add <class> <summary> [--scope <session|project|team>] [--detail <text>] [--tags <t,t>] [--session <id>] [--task <id>] [--file <path>]',
127
- ` classes: ${VALID_CLASSES.join(', ')}`,
128
- ' capture incident <id|latest> — Capture a forensics incident as durable memory',
129
- ' capture policy — Capture the latest policy preflight review as durable memory',
130
- ' capture mcp <server> — Capture MCP trust/quarantine posture as durable memory',
131
- ' capture plugin <name> — Capture plugin trust/quarantine posture as durable memory',
132
- ' search [query] [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>] — Full-text or sqlite-vec semantic search',
133
- ' vector [status|doctor|rebuild] — Inspect or rebuild the sqlite-vec memory index',
134
- ' get <id> — Show record with provenance + links',
135
- ' link <fromId> <toId> <relation> — Create a directed relation between records',
136
- ' queue [limit] — Show the operator review queue',
137
- ' review <id> <state> [--confidence <n>] [--by <name>] [--reason <text>]',
138
- ' stale <id> [reason...] — Mark a record stale with an operator reason',
139
- ' contradict <id> [reason...] — Mark a record contradicted with an operator reason',
140
- ' explain <task...> [--scope <path> ...] — Show the knowledge records that would be injected for a task',
141
- ' injections [agentId] — Show per-turn passive knowledge injection records (Wave-5); no id shows the main session, an id shows that spawned agent',
142
- ' promote <id> <scope> — Promote a memory record into session|project|team scope',
143
- ' export <path> [--scope <scope>] [--cls <class>] — Export a durable knowledge bundle',
144
- ' import <path> — Import a durable knowledge bundle',
145
- ' handoff-export <path> [--scope <scope>] — Export a reviewable handoff bundle for team/shared use',
146
- ' handoff-inspect <path> — Inspect a handoff bundle before import',
147
- ' handoff-import <path> — Import a handoff bundle into durable memory',
148
- ' list [class] [--scope <scope>] — List all records grouped by class',
149
- ' remove <id> — Delete a record',
150
- ].join('\n');
151
- context.print(usage);
152
- break;
153
- }
184
+ default:
185
+ printRecallUsage(context);
186
+ break;
154
187
  }
155
188
  },
156
189
  };
@@ -1,13 +1,32 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
+ import type { PanelDeepLinkTarget } from '../../panels/panel-manager.ts';
2
3
  import { requirePanelManager } from './runtime-services.ts';
3
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
5
 
6
+ /**
7
+ * Parse `--target <id>[:<kind>]` out of the args following `<panel-id>
8
+ * [top|bottom]` (DEBT-5 item 4). Splits the args array in place (removing the
9
+ * flag + its value) so positional `pane` parsing downstream is unaffected by
10
+ * where the flag appears. Only `open fleet` currently has a deep-link
11
+ * consumer (FleetPanel.receiveDeepLink) — other panel ids just ignore an
12
+ * unused target via PanelManager's optional-chaining delivery.
13
+ */
14
+ function extractTargetFlag(rest: string[]): PanelDeepLinkTarget | undefined {
15
+ const flagIdx = rest.indexOf('--target');
16
+ if (flagIdx < 0) return undefined;
17
+ const raw = rest[flagIdx + 1];
18
+ rest.splice(flagIdx, 2);
19
+ if (!raw) return undefined;
20
+ const sep = raw.indexOf(':');
21
+ return sep >= 0 ? { id: raw.slice(0, sep), kind: raw.slice(sep + 1) } : { id: raw };
22
+ }
23
+
5
24
  export function registerOperatorPanelCommand(registry: CommandRegistry): void {
6
25
  registry.register({
7
26
  name: 'panel',
8
27
  aliases: ['panels'],
9
28
  description: 'Open, place, resize, or list panels. Usage: /panel [open <id> [top|bottom]|close <id>|list|toggle|move|focus|split|width|height]',
10
- usage: '[open <id> [top|bottom]|close <id>|list|toggle|move <top|bottom|other> [id]|focus <top|bottom|toggle>|split [show|hide|toggle]|width <left|right|reset>|height <up|down|reset>]',
29
+ usage: '[open <id> [top|bottom] [--target <id>[:<kind>]]|close <id>|list|toggle|move <top|bottom|other> [id]|focus <top|bottom|toggle>|split [show|hide|toggle]|width <left|right|reset>|height <up|down|reset>]',
11
30
  argsHint: '<open|close|list|toggle|move|focus|split|width|height> [id]',
12
31
  handler(args, ctx) {
13
32
  const pm = requirePanelManager(ctx);
@@ -38,10 +57,12 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
38
57
  ctx.print(lines.length > 0 ? lines.join('\n') : 'No panels registered.');
39
58
  } else if (sub === 'open') {
40
59
  const id = args[1];
41
- const pane = args[2]?.toLowerCase();
60
+ const rest = args.slice(2);
61
+ const target = extractTargetFlag(rest);
62
+ const pane = rest[0]?.toLowerCase();
42
63
  if (!id) { ctx.print('Usage: /panel open <panel-id>'); return; }
43
64
  if (pane && pane !== 'top' && pane !== 'bottom') {
44
- ctx.print('Usage: /panel open <panel-id> [top|bottom]');
65
+ ctx.print('Usage: /panel open <panel-id> [top|bottom] [--target <id>[:<kind>]]');
45
66
  return;
46
67
  }
47
68
  try {
@@ -50,11 +71,13 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
50
71
  // no panel lands in the workspace. Report that honestly rather than
51
72
  // claiming "Panel opened: <id>".
52
73
  const redirectTarget = pm.getModalRedirect(id);
53
- if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined);
74
+ // UX-C focus rule 1a: the command path leaves focus in the composer
75
+ // ("mid-command-flow") — showPanel does not grab panel focus here.
76
+ // DEBT-5: forward the deep-link target so the panel lands on the row.
77
+ if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined, target);
54
78
  else {
55
- pm.open(id, pane as 'top' | 'bottom' | undefined);
79
+ pm.open(id, pane as 'top' | 'bottom' | undefined, target);
56
80
  pm.show();
57
- ctx.focusPanels?.();
58
81
  ctx.renderRequest();
59
82
  }
60
83
  if (redirectTarget) {
@@ -146,11 +169,12 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
146
169
  } else {
147
170
  const id = args[0]!;
148
171
  try {
172
+ // UX-C: bare `/panel <id>` is the same command path as `/panel open
173
+ // <id>` — composer stays focused.
149
174
  if (ctx.showPanel) ctx.showPanel(id);
150
175
  else {
151
176
  pm.open(id);
152
177
  pm.show();
153
- ctx.focusPanels?.();
154
178
  ctx.renderRequest();
155
179
  }
156
180
  } catch {
@@ -8,12 +8,15 @@ import type { CommandRegistry } from '../command-registry.ts';
8
8
  import { openModalCommand, requirePlanManager, requireSessionLineageTracker } from './runtime-services.ts';
9
9
 
10
10
  /**
11
- * Single-token verbs that look like a `/plan` subcommand but are not real ones
12
- * (some, like `dismiss`, were dispatched by UI that assumed a subcommand that
13
- * never existed). A lone one of these is refused rather than seeded as a goal,
14
- * so a stray verb can never overwrite the project goal with itself.
11
+ * Single-token verbs that look like a `/plan` subcommand but are not real ones.
12
+ * A lone one of these is refused rather than seeded as a goal, so a stray verb
13
+ * can never overwrite the project goal with itself.
14
+ *
15
+ * DEBT-3: `dismiss` and `answer` are now REAL subcommands (handled above this
16
+ * guard), so they were removed from the refuse-list. `pause`/`stop`/`cancel`
17
+ * remain here — they still have no backing verb and must not seed a goal.
15
18
  */
16
- const PSEUDO_SUBCOMMAND_VERBS = new Set(['dismiss', 'answer', 'pause', 'stop', 'cancel']);
19
+ const PSEUDO_SUBCOMMAND_VERBS = new Set(['pause', 'stop', 'cancel']);
17
20
 
18
21
  function recordNextQuestion(
19
22
  state: Partial<ProjectPlanningState>,
@@ -59,7 +62,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
59
62
  registry.register({
60
63
  name: 'plan',
61
64
  description: 'Inspect or seed TUI-owned project planning state',
62
- usage: '[panel | approve | list | show <id> | mode | explain | override <strategy> | status | clear | <planning goal>]',
65
+ usage: '[panel | approve | dismiss | answer <n> <text> | list | show <id> | mode | explain | override <strategy> | status | clear | <planning goal>]',
63
66
  argsHint: '[panel|approve|status|<goal>]',
64
67
  async handler(args, ctx) {
65
68
  const planManager = requirePlanManager(ctx);
@@ -173,6 +176,92 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
173
176
  return;
174
177
  }
175
178
 
179
+ // DEBT-3: /plan dismiss — archive the current plan. Dismisses the active
180
+ // execution plan (ExecutionPlanManager.dismiss, honest per-state) AND
181
+ // deactivates the project-planning interview state shown in the modal so a
182
+ // later /plan <goal> starts fresh. Mid-execution is refused outright.
183
+ if (args[0] === 'dismiss') {
184
+ const dismissal = planManager.dismiss(ctx.session.runtime.sessionId);
185
+ if (dismissal.outcome === 'requires-cancel') {
186
+ ctx.print(
187
+ `Plan "${dismissal.blockedBy?.title ?? 'active plan'}" is mid-execution and was not dismissed. ` +
188
+ `Run /workstream cancel to stop it first, then /plan dismiss.`,
189
+ );
190
+ return;
191
+ }
192
+ let planningNote = '';
193
+ if (projectPlanningService && projectId) {
194
+ const current = await projectPlanningService.getState({ projectId });
195
+ if (current.state && current.state.metadata?.['active'] === true) {
196
+ await projectPlanningService.upsertState({
197
+ projectId,
198
+ state: {
199
+ ...current.state,
200
+ metadata: {
201
+ ...(current.state.metadata ?? {}),
202
+ active: false,
203
+ dismissedAt: Date.now(),
204
+ dismissedFrom: 'plan-command',
205
+ },
206
+ },
207
+ });
208
+ planningNote = ' Project planning interview marked inactive.';
209
+ }
210
+ }
211
+ if (dismissal.outcome === 'dismissed') {
212
+ ctx.print(
213
+ `Dismissed plan "${dismissal.plan?.title ?? 'active plan'}" ` +
214
+ `(archived as dismissed; retained in /plan list; /plan <goal> starts fresh).${planningNote}`,
215
+ );
216
+ } else if (planningNote) {
217
+ ctx.print(`No active execution plan to dismiss.${planningNote}`);
218
+ } else {
219
+ ctx.print('No active plan or planning state to dismiss.');
220
+ }
221
+ return;
222
+ }
223
+
224
+ // DEBT-3: /plan answer <n|question-id> <text> — record a real answer to an
225
+ // open planning question (moves open → answered, consumed on next refine).
226
+ if (args[0] === 'answer') {
227
+ if (!projectPlanningService || !projectId) {
228
+ ctx.print('Project planning service is not available in this runtime.');
229
+ return;
230
+ }
231
+ const ref = args[1];
232
+ const answerText = args.slice(2).join(' ').trim();
233
+ if (!ref || !answerText) {
234
+ ctx.print('Usage: /plan answer <question-number|question-id> <your answer>');
235
+ return;
236
+ }
237
+ const asNum = Number(ref);
238
+ const selector = Number.isInteger(asNum) && asNum >= 1
239
+ ? { questionIndex: asNum - 1 }
240
+ : { questionId: ref };
241
+ const answerResult = await projectPlanningService.answerQuestion({ projectId, ...selector, answer: answerText });
242
+ if (!answerResult.answered) {
243
+ if (answerResult.reason === 'no-state') {
244
+ ctx.print('No project planning state exists yet. Seed it with /plan <goal>.');
245
+ } else if (answerResult.reason === 'question-not-found') {
246
+ const open = answerResult.openQuestions;
247
+ const listing = open.length > 0
248
+ ? open.map((question, index) => ` ${index + 1}. ${question.prompt} (${question.id})`).join('\n')
249
+ : ' (no open questions)';
250
+ ctx.print(`No open question matched "${ref}". Open questions:\n${listing}`);
251
+ } else {
252
+ ctx.print('Usage: /plan answer <question-number|question-id> <your answer>');
253
+ }
254
+ return;
255
+ }
256
+ openProjectPlanningPanel();
257
+ ctx.print(
258
+ `Recorded answer to: ${answerResult.question?.prompt ?? 'question'}\n` +
259
+ `Readiness: ${answerResult.evaluation.readiness}\n` +
260
+ formatNextQuestion(answerResult.evaluation.nextQuestion),
261
+ );
262
+ return;
263
+ }
264
+
176
265
  // Defense (Wave 6 review): a single verb-looking token is almost never a
177
266
  // real planning goal — it is a mistyped or removed subcommand. The
178
267
  // Planning modal used to dispatch `/plan dismiss`, which has no
@@ -1,18 +1,38 @@
1
+ import { join } from 'node:path';
1
2
  import type { CommandRegistry } from '../command-registry.ts';
2
- import { openModalCommand } from './runtime-services.ts';
3
+ import { openModalCommand, requireShellPaths } from './runtime-services.ts';
4
+ import { regenerateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
3
5
 
4
6
  /**
5
7
  * Register the /qrcode command.
6
8
  *
7
- * Opens the QR Code panel which displays a scannable QR code for
8
- * companion app pairing, along with connection URL, token, and username.
9
+ * `/qrcode` (no args) opens the companion-app pairing modal (QR code + URL/
10
+ * token/username). `/qrcode regenerate` rotates the companion pairing token:
11
+ * it re-keys the shared operator-token store, so the previously-issued token is
12
+ * rejected on the next auth and a fresh QR must be scanned to reconnect.
9
13
  */
10
14
  export function registerQrcodeRuntimeCommands(registry: CommandRegistry): void {
11
15
  registry.register({
12
16
  name: 'qrcode',
13
17
  aliases: ['qr', 'pair'],
14
- description: 'Open the companion-app pairing modal (QR code)',
15
- handler(_args, ctx) {
18
+ description: 'Open the companion-app pairing modal (QR code), or regenerate the pairing token',
19
+ usage: '[regenerate]',
20
+ argsHint: '[regenerate]',
21
+ handler(args, ctx) {
22
+ if (args[0]?.toLowerCase() === 'regenerate') {
23
+ const shellPaths = requireShellPaths(ctx);
24
+ const daemonHomeDir = join(shellPaths.homeDirectory, '.goodvibes', 'daemon');
25
+ try {
26
+ const rotated = regenerateCompanionToken({ daemonHomeDir });
27
+ ctx.print(
28
+ 'Pairing token rotated. The previous token is now rejected — ' +
29
+ `re-scan the QR (new peer ${rotated.peerId.slice(0, 8)}…) to reconnect the companion app.`,
30
+ );
31
+ } catch (error) {
32
+ ctx.print(`Could not regenerate the pairing token: ${String(error)}`);
33
+ }
34
+ return;
35
+ }
16
36
  openModalCommand(ctx, 'pairing-modal');
17
37
  },
18
38
  });