@jmoyers/harness 0.1.9 → 0.1.11

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 (54) hide show
  1. package/README.md +36 -155
  2. package/package.json +3 -1
  3. package/packages/harness-ai/src/anthropic-client.ts +99 -0
  4. package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
  5. package/packages/harness-ai/src/anthropic-provider.ts +82 -0
  6. package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
  7. package/packages/harness-ai/src/index.ts +36 -0
  8. package/packages/harness-ai/src/json-parse.ts +66 -0
  9. package/packages/harness-ai/src/sse.ts +80 -0
  10. package/packages/harness-ai/src/stream-object.ts +96 -0
  11. package/packages/harness-ai/src/stream-text.ts +1340 -0
  12. package/packages/harness-ai/src/types.ts +330 -0
  13. package/packages/harness-ai/src/ui-stream.ts +217 -0
  14. package/scripts/codex-live-mux-runtime.ts +265 -14
  15. package/scripts/control-plane-daemon.ts +33 -5
  16. package/scripts/harness.ts +579 -134
  17. package/src/cli/default-gateway-pointer.ts +193 -0
  18. package/src/cli/gateway-record.ts +16 -1
  19. package/src/config/config-core.ts +13 -2
  20. package/src/config/harness-paths.ts +4 -7
  21. package/src/config/harness-runtime-migration.ts +142 -19
  22. package/src/config/secrets-core.ts +92 -4
  23. package/src/control-plane/prompt/thread-title-namer.ts +316 -0
  24. package/src/control-plane/stream-command-parser.ts +12 -0
  25. package/src/control-plane/stream-protocol.ts +6 -0
  26. package/src/control-plane/stream-server-background.ts +18 -2
  27. package/src/control-plane/stream-server-command.ts +14 -0
  28. package/src/control-plane/stream-server.ts +460 -28
  29. package/src/domain/conversations.ts +11 -7
  30. package/src/domain/workspace.ts +9 -0
  31. package/src/mux/input-shortcuts.ts +38 -1
  32. package/src/mux/live-mux/git-parsing.ts +40 -0
  33. package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
  34. package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
  35. package/src/mux/live-mux/modal-input-reducers.ts +34 -1
  36. package/src/mux/live-mux/modal-overlays.ts +45 -0
  37. package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
  38. package/src/mux/render-frame.ts +1 -1
  39. package/src/mux/task-screen-keybindings.ts +29 -1
  40. package/src/services/control-plane.ts +22 -0
  41. package/src/services/runtime-control-actions.ts +69 -0
  42. package/src/services/runtime-conversation-activation.ts +25 -0
  43. package/src/services/runtime-conversation-starter.ts +31 -7
  44. package/src/services/runtime-input-router.ts +6 -0
  45. package/src/services/runtime-modal-input.ts +18 -0
  46. package/src/services/runtime-navigation-input.ts +4 -0
  47. package/src/services/runtime-rail-input.ts +5 -0
  48. package/src/services/runtime-repository-actions.ts +2 -0
  49. package/src/services/runtime-workspace-actions.ts +5 -0
  50. package/src/store/control-plane-store.ts +36 -0
  51. package/src/store/event-store.ts +36 -0
  52. package/src/ui/global-shortcut-input.ts +2 -0
  53. package/src/ui/input.ts +31 -0
  54. package/src/ui/modals/manager.ts +26 -0
@@ -25,7 +25,7 @@ import {
25
25
  } from '../src/config/config-core.ts';
26
26
  import { resolveHarnessRuntimePath } from '../src/config/harness-paths.ts';
27
27
  import { migrateLegacyHarnessLayout } from '../src/config/harness-runtime-migration.ts';
28
- import { loadHarnessSecrets } from '../src/config/secrets-core.ts';
28
+ import { loadHarnessSecrets, upsertHarnessSecret } from '../src/config/secrets-core.ts';
29
29
  import { detectMuxGlobalShortcut, resolveMuxShortcutBindings } from '../src/mux/input-shortcuts.ts';
30
30
  import { createMuxInputModeManager } from '../src/mux/terminal-input-modes.ts';
31
31
  import type { buildWorkspaceRailViewRows } from '../src/mux/workspace-rail-model.ts';
@@ -92,7 +92,9 @@ import {
92
92
  } from '../src/mux/live-mux/layout.ts';
93
93
  import {
94
94
  normalizeGitHubRemoteUrl,
95
+ resolveGitHubDefaultBranchForActions,
95
96
  repositoryNameFromGitHubRemoteUrl,
97
+ resolveGitHubTrackedBranchForActions,
96
98
  shouldShowGitHubPrActions,
97
99
  } from '../src/mux/live-mux/git-parsing.ts';
98
100
  import { readProcessUsageSample, runGitCommand } from '../src/mux/live-mux/git-snapshot.ts';
@@ -243,6 +245,7 @@ interface RuntimeCommandMenuContext {
243
245
  readonly leftNavSelectionKind: WorkspaceModel['leftNavSelection']['kind'];
244
246
  readonly profileRunning: boolean;
245
247
  readonly statusTimelineRunning: boolean;
248
+ readonly githubRepositoryId: string | null;
246
249
  readonly githubRepositoryUrl: string | null;
247
250
  readonly githubDefaultBranch: string | null;
248
251
  readonly githubTrackedBranch: string | null;
@@ -257,12 +260,26 @@ interface CommandMenuGitHubProjectPrState {
257
260
  readonly loading: boolean;
258
261
  }
259
262
 
263
+ interface GitHubDebugAuthState {
264
+ enabled: boolean;
265
+ token: 'env' | 'gh' | 'none';
266
+ auth: 'ok' | 'no' | 'er' | 'na' | 'uk';
267
+ projectPr: 'ok' | 'er' | 'na';
268
+ }
269
+
260
270
  interface ThemePickerSessionState {
261
271
  readonly initialThemeConfig: HarnessMuxThemeConfig | null;
262
272
  committed: boolean;
263
273
  previewActionId: string | null;
264
274
  }
265
275
 
276
+ interface AllowedCommandMenuApiKey {
277
+ readonly actionIdSuffix: string;
278
+ readonly envVar: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY';
279
+ readonly displayName: string;
280
+ readonly aliases: readonly string[];
281
+ }
282
+
266
283
  const DEFAULT_RESIZE_MIN_INTERVAL_MS = 33;
267
284
  const DEFAULT_PTY_RESIZE_SETTLE_MS = 75;
268
285
  const DEFAULT_STARTUP_SETTLE_QUIET_MS = 300;
@@ -282,6 +299,21 @@ const REPOSITORY_COLLAPSE_ALL_CHORD_PREFIX = Buffer.from([0x0b]);
282
299
  const UNTRACKED_REPOSITORY_GROUP_ID = 'untracked';
283
300
  const THEME_PICKER_SCOPE = 'theme-select';
284
301
  const THEME_ACTION_ID_PREFIX = 'theme.set.';
302
+ const API_KEY_ACTION_ID_PREFIX = 'api-key.set.';
303
+ const COMMAND_MENU_ALLOWED_API_KEYS: readonly AllowedCommandMenuApiKey[] = [
304
+ {
305
+ actionIdSuffix: 'anthropic',
306
+ envVar: 'ANTHROPIC_API_KEY',
307
+ displayName: 'Anthropic API Key',
308
+ aliases: ['anthropic api key', 'claude api key'],
309
+ },
310
+ {
311
+ actionIdSuffix: 'openai',
312
+ envVar: 'OPENAI_API_KEY',
313
+ displayName: 'OpenAI API Key',
314
+ aliases: ['openai api key', 'codex api key'],
315
+ },
316
+ ];
285
317
  const GIT_SUMMARY_LOADING: GitSummary = {
286
318
  branch: '(loading)',
287
319
  changedFiles: 0,
@@ -331,6 +363,15 @@ function parseGitHubPrUrl(result: Record<string, unknown>): string | null {
331
363
  return typeof url === 'string' ? url : null;
332
364
  }
333
365
 
366
+ function parseGitHubUrl(result: Record<string, unknown>): string | null {
367
+ const url = result['url'];
368
+ if (typeof url !== 'string') {
369
+ return null;
370
+ }
371
+ const trimmed = url.trim();
372
+ return trimmed.length > 0 ? trimmed : null;
373
+ }
374
+
334
375
  function commandMenuProjectPathTail(path: string): string {
335
376
  const normalized = path.trim().replaceAll('\\', '/').replace(/\/+$/u, '');
336
377
  if (normalized.length === 0) {
@@ -404,6 +445,28 @@ function commandExistsOnPath(command: string): boolean {
404
445
  }
405
446
  }
406
447
 
448
+ function readGhAuthTokenForDebug(): string | null {
449
+ try {
450
+ const stdout = execFileSync('gh', ['auth', 'token'], {
451
+ encoding: 'utf8',
452
+ stdio: ['ignore', 'pipe', 'ignore'],
453
+ timeout: 2000,
454
+ windowsHide: true,
455
+ });
456
+ const token = stdout.trim();
457
+ return token.length > 0 ? token : null;
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+
463
+ function formatGitHubDebugTokens(state: GitHubDebugAuthState): string {
464
+ if (!state.enabled) {
465
+ return '[gh:off tk:na au:na pr:na]';
466
+ }
467
+ return `[gh:on tk:${state.token} au:${state.auth} pr:${state.projectPr}]`;
468
+ }
469
+
407
470
  async function main(): Promise<number> {
408
471
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
409
472
  process.stderr.write('codex:live:mux requires a TTY stdin/stdout\n');
@@ -502,6 +565,26 @@ async function main(): Promise<number> {
502
565
  const resolvedMuxTheme = resolveAndApplyRuntimeTheme(runtimeThemeConfig, true);
503
566
  let currentModalTheme = resolvedMuxTheme.theme.modalTheme;
504
567
  const configuredMuxGit = loadedConfig.config.mux.git;
568
+ const githubTokenEnvVar = loadedConfig.config.github.tokenEnvVar;
569
+ const envGitHubTokenRaw = process.env[githubTokenEnvVar];
570
+ const hasEnvGitHubToken =
571
+ typeof envGitHubTokenRaw === 'string' && envGitHubTokenRaw.trim().length > 0;
572
+ const githubDebugAuthState: GitHubDebugAuthState = {
573
+ enabled: loadedConfig.config.github.enabled,
574
+ token: hasEnvGitHubToken ? 'env' : 'none',
575
+ auth: loadedConfig.config.github.enabled ? (hasEnvGitHubToken ? 'ok' : 'uk') : 'na',
576
+ projectPr: 'na',
577
+ };
578
+ if (githubDebugAuthState.enabled && !hasEnvGitHubToken) {
579
+ const ghToken = commandExistsOnPath('gh') ? readGhAuthTokenForDebug() : null;
580
+ if (ghToken !== null) {
581
+ githubDebugAuthState.token = 'gh';
582
+ githubDebugAuthState.auth = 'ok';
583
+ } else {
584
+ githubDebugAuthState.token = 'none';
585
+ githubDebugAuthState.auth = 'no';
586
+ }
587
+ }
505
588
  const configuredCodexLaunch = loadedConfig.config.codex.launch;
506
589
  const configuredCritique = loadedConfig.config.critique;
507
590
  const codexLaunchModeByDirectoryPath: Record<string, 'yolo' | 'standard'> = {};
@@ -635,7 +718,7 @@ async function main(): Promise<number> {
635
718
  await startControlPlaneStreamServer({
636
719
  stateStorePath: resolveHarnessRuntimePath(
637
720
  options.invocationDirectory,
638
- process.env.HARNESS_CONTROL_PLANE_DB_PATH ?? '.harness/control-plane.sqlite',
721
+ '.harness/control-plane.sqlite',
639
722
  ),
640
723
  codexTelemetry: loadedConfig.config.codex.telemetry,
641
724
  codexHistory: loadedConfig.config.codex.history,
@@ -743,7 +826,7 @@ async function main(): Promise<number> {
743
826
  workspace.taskPaneRepositoryEditClickState = null;
744
827
  workspace.homePaneDragState = null;
745
828
 
746
- const sessionEnv = {
829
+ const sessionEnv: Record<string, string> = {
747
830
  ...sanitizeProcessEnv(),
748
831
  TERM: process.env.TERM ?? 'xterm-256color',
749
832
  };
@@ -1408,12 +1491,47 @@ async function main(): Promise<number> {
1408
1491
  activeDirectoryId === null
1409
1492
  ? null
1410
1493
  : (directoryRepositorySnapshotByDirectoryId.get(activeDirectoryId) ?? null);
1494
+ let githubRepositoryId =
1495
+ activeDirectoryId === null
1496
+ ? null
1497
+ : (repositoryAssociationByDirectoryId.get(activeDirectoryId) ?? null);
1498
+ if (githubRepositoryId !== null) {
1499
+ const associatedRepository = repositories.get(githubRepositoryId);
1500
+ if (associatedRepository === undefined || associatedRepository.archivedAt !== null) {
1501
+ githubRepositoryId = null;
1502
+ }
1503
+ }
1504
+ const snapshotRemoteUrl = activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null;
1505
+ if (githubRepositoryId === null && snapshotRemoteUrl !== null) {
1506
+ const snapshotRemote = normalizeGitHubRemoteUrl(snapshotRemoteUrl);
1507
+ if (snapshotRemote !== null) {
1508
+ for (const repository of repositories.values()) {
1509
+ if (repository.archivedAt !== null) {
1510
+ continue;
1511
+ }
1512
+ if (normalizeGitHubRemoteUrl(repository.remoteUrl) === snapshotRemote) {
1513
+ githubRepositoryId = repository.repositoryId;
1514
+ break;
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ const githubRepositoryRecord =
1520
+ githubRepositoryId === null ? null : (repositories.get(githubRepositoryId) ?? null);
1411
1521
  const githubProjectPrState =
1412
1522
  activeDirectoryId !== null &&
1413
1523
  commandMenuGitHubProjectPrState !== null &&
1414
1524
  commandMenuGitHubProjectPrState.directoryId === activeDirectoryId
1415
1525
  ? commandMenuGitHubProjectPrState
1416
1526
  : null;
1527
+ const currentBranchForActions =
1528
+ activeDirectoryId === null
1529
+ ? null
1530
+ : (gitSummaryByDirectoryId.get(activeDirectoryId)?.branch ?? null);
1531
+ const trackedBranchForActions = resolveGitHubTrackedBranchForActions({
1532
+ projectTrackedBranch: githubProjectPrState?.branchName ?? null,
1533
+ currentBranch: currentBranchForActions,
1534
+ });
1417
1535
  const selectedText =
1418
1536
  workspace.selection === null
1419
1537
  ? ''
@@ -1433,9 +1551,13 @@ async function main(): Promise<number> {
1433
1551
  statusTimelineRunning: existsSync(
1434
1552
  resolveStatusTimelineStatePath(options.invocationDirectory, muxSessionName),
1435
1553
  ),
1554
+ githubRepositoryId,
1436
1555
  githubRepositoryUrl: activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null,
1437
- githubDefaultBranch: activeDirectoryRepositorySnapshot?.defaultBranch ?? null,
1438
- githubTrackedBranch: githubProjectPrState?.branchName ?? null,
1556
+ githubDefaultBranch: resolveGitHubDefaultBranchForActions({
1557
+ repositoryDefaultBranch: githubRepositoryRecord?.defaultBranch ?? null,
1558
+ snapshotDefaultBranch: activeDirectoryRepositorySnapshot?.defaultBranch ?? null,
1559
+ }),
1560
+ githubTrackedBranch: trackedBranchForActions,
1439
1561
  githubOpenPrUrl: githubProjectPrState?.openPrUrl ?? null,
1440
1562
  githubProjectPrLoading: githubProjectPrState?.loading ?? false,
1441
1563
  };
@@ -1502,6 +1624,7 @@ async function main(): Promise<number> {
1502
1624
  resolveCommandMenuActions,
1503
1625
  getNewThreadPrompt: () => workspace.newThreadPrompt,
1504
1626
  getAddDirectoryPrompt: () => workspace.addDirectoryPrompt,
1627
+ getApiKeyPrompt: () => workspace.apiKeyPrompt,
1505
1628
  getTaskEditorPrompt: () => workspace.taskEditorPrompt,
1506
1629
  getRepositoryPrompt: () => workspace.repositoryPrompt,
1507
1630
  getConversationTitleEdit: () => workspace.conversationTitleEdit,
@@ -1707,6 +1830,13 @@ async function main(): Promise<number> {
1707
1830
  };
1708
1831
  const githubAuthHintNotice =
1709
1832
  'GitHub PR actions become available after auth (`gh auth login` or `GITHUB_TOKEN`).';
1833
+ const setGitHubDebugAuthState = (
1834
+ update: Partial<Pick<GitHubDebugAuthState, 'token' | 'auth' | 'projectPr'>>,
1835
+ ): void => {
1836
+ githubDebugAuthState.token = update.token ?? githubDebugAuthState.token;
1837
+ githubDebugAuthState.auth = update.auth ?? githubDebugAuthState.auth;
1838
+ githubDebugAuthState.projectPr = update.projectPr ?? githubDebugAuthState.projectPr;
1839
+ };
1710
1840
  const isGitHubAuthUnavailableError = (error: unknown): boolean => {
1711
1841
  const message = formatErrorMessage(error).toLowerCase();
1712
1842
  return (
@@ -1729,7 +1859,14 @@ async function main(): Promise<number> {
1729
1859
  directoryId,
1730
1860
  });
1731
1861
  commandMenuGitHubProjectPrState = parseGitHubProjectPrState(directoryId, result);
1732
- } catch {
1862
+ setGitHubDebugAuthState({
1863
+ projectPr: 'ok',
1864
+ });
1865
+ } catch (error: unknown) {
1866
+ setGitHubDebugAuthState({
1867
+ projectPr: 'er',
1868
+ auth: isGitHubAuthUnavailableError(error) ? 'no' : githubDebugAuthState.auth,
1869
+ });
1733
1870
  commandMenuGitHubProjectPrState = {
1734
1871
  directoryId,
1735
1872
  branchName: null,
@@ -2306,6 +2443,7 @@ async function main(): Promise<number> {
2306
2443
  }
2307
2444
  workspace.newThreadPrompt = null;
2308
2445
  workspace.addDirectoryPrompt = null;
2446
+ workspace.apiKeyPrompt = null;
2309
2447
  workspace.taskEditorPrompt = null;
2310
2448
  workspace.repositoryPrompt = null;
2311
2449
  if (workspace.conversationTitleEdit !== null) {
@@ -2517,6 +2655,12 @@ async function main(): Promise<number> {
2517
2655
  },
2518
2656
  });
2519
2657
  },
2658
+ listConversationIdsForTitleRefresh: () => conversationManager.orderedIds(),
2659
+ conversationAgentTypeForTitleRefresh: (sessionId) =>
2660
+ conversationManager.get(sessionId)?.agentType ?? null,
2661
+ refreshConversationTitle: async (sessionId) => {
2662
+ return await controlPlaneService.refreshConversationTitle(sessionId);
2663
+ },
2520
2664
  });
2521
2665
  const runtimeWorkspaceActions = new RuntimeWorkspaceActions({
2522
2666
  conversationActions: conversationLifecycle,
@@ -2539,6 +2683,7 @@ async function main(): Promise<number> {
2539
2683
  }
2540
2684
  workspace.newThreadPrompt = null;
2541
2685
  workspace.addDirectoryPrompt = null;
2686
+ workspace.apiKeyPrompt = null;
2542
2687
  workspace.taskEditorPrompt = null;
2543
2688
  workspace.repositoryPrompt = null;
2544
2689
  if (workspace.conversationTitleEdit !== null) {
@@ -2558,6 +2703,39 @@ async function main(): Promise<number> {
2558
2703
  markDirty();
2559
2704
  };
2560
2705
 
2706
+ const openApiKeyPrompt = (apiKey: AllowedCommandMenuApiKey): void => {
2707
+ workspace.newThreadPrompt = null;
2708
+ workspace.addDirectoryPrompt = null;
2709
+ workspace.taskEditorPrompt = null;
2710
+ workspace.repositoryPrompt = null;
2711
+ if (workspace.conversationTitleEdit !== null) {
2712
+ stopConversationTitleEdit(true);
2713
+ }
2714
+ workspace.conversationTitleEditClickState = null;
2715
+ const existingRaw = sessionEnv[apiKey.envVar] ?? process.env[apiKey.envVar];
2716
+ const hasExistingValue = typeof existingRaw === 'string' && existingRaw.trim().length > 0;
2717
+ workspace.apiKeyPrompt = {
2718
+ keyName: apiKey.envVar,
2719
+ displayName: apiKey.displayName,
2720
+ value: '',
2721
+ error: null,
2722
+ hasExistingValue,
2723
+ };
2724
+ markDirty();
2725
+ };
2726
+
2727
+ const persistApiKey = (keyName: string, value: string): void => {
2728
+ const result = upsertHarnessSecret({
2729
+ cwd: options.invocationDirectory,
2730
+ key: keyName,
2731
+ value,
2732
+ });
2733
+ sessionEnv[keyName] = value;
2734
+ process.env[keyName] = value;
2735
+ const action = result.replacedExisting ? 'updated' : 'saved';
2736
+ setCommandNotice(`${keyName} ${action}`);
2737
+ };
2738
+
2561
2739
  const startThreadFromCommandMenu = (
2562
2740
  directoryId: string,
2563
2741
  agentType: ReturnType<typeof normalizeThreadAgentType>,
@@ -2661,7 +2839,7 @@ async function main(): Promise<number> {
2661
2839
  return [
2662
2840
  {
2663
2841
  id: 'critique.review.unstaged',
2664
- title: 'Critique AI Review: Unstaged Changes',
2842
+ title: 'Critique AI Review: Unstaged Changes (git)',
2665
2843
  aliases: ['critique unstaged review', 'review unstaged diff', 'ai review unstaged'],
2666
2844
  keywords: ['critique', 'review', 'unstaged', 'diff', 'ai'],
2667
2845
  detail: 'runs critique review',
@@ -2671,7 +2849,7 @@ async function main(): Promise<number> {
2671
2849
  },
2672
2850
  {
2673
2851
  id: 'critique.review.staged',
2674
- title: 'Critique AI Review: Staged Changes',
2852
+ title: 'Critique AI Review: Staged Changes (git)',
2675
2853
  aliases: ['critique staged review', 'review staged diff', 'ai review staged'],
2676
2854
  keywords: ['critique', 'review', 'staged', 'diff', 'ai'],
2677
2855
  detail: 'runs critique review --staged',
@@ -2681,7 +2859,7 @@ async function main(): Promise<number> {
2681
2859
  },
2682
2860
  {
2683
2861
  id: 'critique.review.base-branch',
2684
- title: 'Critique AI Review: Current Branch vs Base',
2862
+ title: 'Critique AI Review: Current Branch vs Base (git)',
2685
2863
  aliases: ['critique base review', 'review against base branch', 'ai review base'],
2686
2864
  keywords: ['critique', 'review', 'base', 'branch', 'diff', 'ai'],
2687
2865
  detail: 'runs critique review <base> HEAD',
@@ -2824,6 +3002,21 @@ async function main(): Promise<number> {
2824
3002
  },
2825
3003
  });
2826
3004
 
3005
+ commandMenuRegistry.registerProvider('api-key.set', () => {
3006
+ return COMMAND_MENU_ALLOWED_API_KEYS.map(
3007
+ (apiKey): RegisteredCommandMenuAction<RuntimeCommandMenuContext> => ({
3008
+ id: `${API_KEY_ACTION_ID_PREFIX}${apiKey.actionIdSuffix}`,
3009
+ title: `Set ${apiKey.displayName}`,
3010
+ aliases: [...apiKey.aliases],
3011
+ keywords: ['api', 'key', 'set', apiKey.envVar.toLowerCase()],
3012
+ detail: apiKey.envVar,
3013
+ run: () => {
3014
+ openApiKeyPrompt(apiKey);
3015
+ },
3016
+ }),
3017
+ );
3018
+ });
3019
+
2827
3020
  commandMenuRegistry.registerProvider('theme.set', () => {
2828
3021
  const selectedThemeName = getActiveMuxTheme().name;
2829
3022
  return muxThemePresetNames().map(
@@ -2873,10 +3066,10 @@ async function main(): Promise<number> {
2873
3066
  if (repositoryUrl === null) {
2874
3067
  return [];
2875
3068
  }
2876
- return [
3069
+ const actions: RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] = [
2877
3070
  {
2878
3071
  id: 'github.repo.open',
2879
- title: 'Open GitHub for This Repo',
3072
+ title: 'Open GitHub for This Repo (git)',
2880
3073
  aliases: ['open github for this repo', 'open github repo', 'open repository on github'],
2881
3074
  keywords: ['github', 'repository', 'repo', 'open'],
2882
3075
  detail: repositoryUrl,
@@ -2890,6 +3083,41 @@ async function main(): Promise<number> {
2890
3083
  },
2891
3084
  },
2892
3085
  ];
3086
+ if (context.githubRepositoryId !== null) {
3087
+ const repositoryId = context.githubRepositoryId;
3088
+ actions.push({
3089
+ id: 'github.repo.my-prs.open',
3090
+ title: 'Show My Open Pull Requests (git)',
3091
+ aliases: ['show my open pull requests', 'my open pull requests', 'show my prs', 'my prs'],
3092
+ keywords: ['github', 'pr', 'pull-request', 'open', 'my'],
3093
+ detail: repositoryUrl,
3094
+ run: async () => {
3095
+ queueControlPlaneOp(async () => {
3096
+ const result = await streamClient.sendCommand({
3097
+ type: 'github.repo-my-prs-url',
3098
+ repositoryId,
3099
+ });
3100
+ const parsedResult = asRecord(result);
3101
+ if (parsedResult === null) {
3102
+ setCommandNotice('github my open pull requests url unavailable');
3103
+ return;
3104
+ }
3105
+ const myPrsUrl = parseGitHubUrl(parsedResult);
3106
+ if (myPrsUrl === null) {
3107
+ setCommandNotice('github my open pull requests url unavailable');
3108
+ return;
3109
+ }
3110
+ const opened = openUrlInBrowser(myPrsUrl);
3111
+ setCommandNotice(
3112
+ opened
3113
+ ? 'opened my open pull requests in browser'
3114
+ : `open pull requests: ${myPrsUrl}`,
3115
+ );
3116
+ }, 'command-menu-open-my-open-prs');
3117
+ },
3118
+ });
3119
+ }
3120
+ return actions;
2893
3121
  });
2894
3122
 
2895
3123
  commandMenuRegistry.registerProvider('github.project-pr', (context) => {
@@ -2908,7 +3136,7 @@ async function main(): Promise<number> {
2908
3136
  return [
2909
3137
  {
2910
3138
  id: 'github.pr.open',
2911
- title: 'Open PR',
3139
+ title: 'Open PR (git)',
2912
3140
  aliases: ['open pull request', 'open pr'],
2913
3141
  keywords: ['github', 'pr', 'open', 'pull-request'],
2914
3142
  detail: context.githubTrackedBranch ?? 'current project',
@@ -2922,9 +3150,17 @@ async function main(): Promise<number> {
2922
3150
  });
2923
3151
  } catch (error: unknown) {
2924
3152
  if (isGitHubAuthUnavailableError(error)) {
3153
+ setGitHubDebugAuthState({
3154
+ auth: 'no',
3155
+ projectPr: 'er',
3156
+ });
2925
3157
  setCommandNotice(githubAuthHintNotice);
2926
3158
  return;
2927
3159
  }
3160
+ setGitHubDebugAuthState({
3161
+ auth: 'er',
3162
+ projectPr: 'er',
3163
+ });
2928
3164
  throw error;
2929
3165
  }
2930
3166
  const parsedResult = asRecord(result);
@@ -2933,6 +3169,9 @@ async function main(): Promise<number> {
2933
3169
  return;
2934
3170
  }
2935
3171
  const state = parseGitHubProjectPrState(directoryId, parsedResult);
3172
+ setGitHubDebugAuthState({
3173
+ projectPr: 'ok',
3174
+ });
2936
3175
  commandMenuGitHubProjectPrState = state;
2937
3176
  if (state.openPrUrl === null) {
2938
3177
  setCommandNotice('no open pull request for tracked branch');
@@ -2951,7 +3190,7 @@ async function main(): Promise<number> {
2951
3190
  return [
2952
3191
  {
2953
3192
  id: 'github.pr.create',
2954
- title: 'Create PR',
3193
+ title: 'Create PR (git)',
2955
3194
  aliases: ['create pull request', 'new pr'],
2956
3195
  keywords: ['github', 'pr', 'create', 'pull-request'],
2957
3196
  detail: context.githubTrackedBranch,
@@ -2965,9 +3204,15 @@ async function main(): Promise<number> {
2965
3204
  });
2966
3205
  } catch (error: unknown) {
2967
3206
  if (isGitHubAuthUnavailableError(error)) {
3207
+ setGitHubDebugAuthState({
3208
+ auth: 'no',
3209
+ });
2968
3210
  setCommandNotice(githubAuthHintNotice);
2969
3211
  return;
2970
3212
  }
3213
+ setGitHubDebugAuthState({
3214
+ auth: 'er',
3215
+ });
2971
3216
  throw error;
2972
3217
  }
2973
3218
  const parsedResult = asRecord(result);
@@ -2979,6 +3224,10 @@ async function main(): Promise<number> {
2979
3224
  if (prUrl === null) {
2980
3225
  throw new Error('github.pr-create returned malformed pr url');
2981
3226
  }
3227
+ setGitHubDebugAuthState({
3228
+ auth: 'ok',
3229
+ projectPr: 'ok',
3230
+ });
2982
3231
  refreshCommandMenuGitHubProjectPrState(directoryId);
2983
3232
  const opened = openUrlInBrowser(prUrl);
2984
3233
  setCommandNotice(
@@ -3087,7 +3336,8 @@ async function main(): Promise<number> {
3087
3336
  >({
3088
3337
  renderFlush: {
3089
3338
  perfNowNs,
3090
- statusFooterForConversation: (conversation) => debugFooterForConversation(conversation),
3339
+ statusFooterForConversation: (conversation) =>
3340
+ `${formatGitHubDebugTokens(githubDebugAuthState)} ${debugFooterForConversation(conversation)}`,
3091
3341
  currentStatusNotice: () => debugFooterNotice.current(),
3092
3342
  currentStatusRow: () => outputLoadSampler.currentStatusRow(),
3093
3343
  onStatusLineComposed: (input) => {
@@ -3394,6 +3644,7 @@ async function main(): Promise<number> {
3394
3644
  scheduleConversationTitlePersist,
3395
3645
  resolveCommandMenuActions,
3396
3646
  executeCommandMenuAction,
3647
+ persistApiKey,
3397
3648
  requestStop,
3398
3649
  resolveDirectoryForAction,
3399
3650
  openNewThreadPrompt,
@@ -1,8 +1,11 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { startCodexLiveSession } from '../src/codex/live-session.ts';
3
3
  import { startControlPlaneStreamServer } from '../src/control-plane/stream-server.ts';
4
- import { loadHarnessConfig } from '../src/config/config-core.ts';
5
- import { resolveHarnessRuntimePath } from '../src/config/harness-paths.ts';
4
+ import { loadHarnessConfig, resolveHarnessConfigPath } from '../src/config/config-core.ts';
5
+ import {
6
+ resolveHarnessRuntimePath,
7
+ resolveHarnessWorkspaceDirectory,
8
+ } from '../src/config/harness-paths.ts';
6
9
  import { migrateLegacyHarnessLayout } from '../src/config/harness-runtime-migration.ts';
7
10
  import { loadHarnessSecrets } from '../src/config/secrets-core.ts';
8
11
  import {
@@ -69,7 +72,7 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
69
72
  const defaultAuthToken = process.env.HARNESS_CONTROL_PLANE_AUTH_TOKEN ?? null;
70
73
  const defaultStateDbPath = resolveHarnessRuntimePath(
71
74
  invocationDirectory,
72
- process.env.HARNESS_CONTROL_PLANE_DB_PATH ?? '.harness/control-plane.sqlite',
75
+ '.harness/control-plane.sqlite',
73
76
  );
74
77
 
75
78
  let host = defaultHost;
@@ -114,7 +117,7 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
114
117
  if (value === undefined) {
115
118
  throw new Error('missing value for --state-db-path');
116
119
  }
117
- stateDbPath = value;
120
+ stateDbPath = resolveHarnessRuntimePath(invocationDirectory, value);
118
121
  idx += 1;
119
122
  continue;
120
123
  }
@@ -129,6 +132,17 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
129
132
  if (!loopbackHosts.has(host) && authToken === null) {
130
133
  throw new Error('non-loopback hosts require --auth-token or HARNESS_CONTROL_PLANE_AUTH_TOKEN');
131
134
  }
135
+ const workspaceRuntimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, process.env);
136
+ const normalizedRuntimeRoot = resolve(workspaceRuntimeRoot);
137
+ const normalizedStateDbPath = resolve(stateDbPath);
138
+ if (
139
+ normalizedStateDbPath !== normalizedRuntimeRoot &&
140
+ !normalizedStateDbPath.startsWith(`${normalizedRuntimeRoot}/`)
141
+ ) {
142
+ throw new Error(
143
+ `invalid --state-db-path: ${stateDbPath}. state db path must be under workspace runtime root ${workspaceRuntimeRoot}`,
144
+ );
145
+ }
132
146
 
133
147
  return {
134
148
  host,
@@ -140,6 +154,11 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
140
154
 
141
155
  async function main(): Promise<number> {
142
156
  const invocationDirectory = resolveInvocationDirectory();
157
+ const gatewayRunIdRaw = process.env.HARNESS_GATEWAY_RUN_ID;
158
+ const gatewayRunId =
159
+ typeof gatewayRunIdRaw === 'string' && gatewayRunIdRaw.trim().length > 0
160
+ ? gatewayRunIdRaw.trim()
161
+ : `gateway-run-${process.pid}`;
143
162
  migrateLegacyHarnessLayout(invocationDirectory, process.env);
144
163
  loadHarnessSecrets({ cwd: invocationDirectory });
145
164
  configureProcessPerf(invocationDirectory);
@@ -159,14 +178,22 @@ async function main(): Promise<number> {
159
178
  }
160
179
  const startupSpan = startPerfSpan('daemon.startup.total', {
161
180
  process: 'daemon',
181
+ gatewayRunId,
162
182
  });
163
183
  recordPerfEvent('daemon.startup.begin', {
164
184
  process: 'daemon',
185
+ gatewayRunId,
165
186
  });
166
187
  const options = parseArgs(process.argv.slice(2), invocationDirectory);
188
+ const workspaceRuntimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, process.env);
189
+ const configPath = resolveHarnessConfigPath(invocationDirectory, process.env);
190
+ process.stdout.write(
191
+ `[control-plane] boot runId=${gatewayRunId} pid=${String(process.pid)} ppid=${String(process.ppid)} workspaceRoot=${invocationDirectory} runtimeRoot=${workspaceRuntimeRoot} config=${configPath} db=${options.stateDbPath}\n`,
192
+ );
167
193
 
168
194
  const listenSpan = startPerfSpan('daemon.startup.listen', {
169
195
  process: 'daemon',
196
+ gatewayRunId,
170
197
  });
171
198
  const serverOptions: Parameters<typeof startControlPlaneStreamServer>[0] = {
172
199
  host: options.host,
@@ -251,13 +278,14 @@ async function main(): Promise<number> {
251
278
  const address = server.address();
252
279
  recordPerfEvent('daemon.startup.listening', {
253
280
  process: 'daemon',
281
+ gatewayRunId,
254
282
  host: address.address,
255
283
  port: address.port,
256
284
  auth: options.authToken === null ? 'off' : 'on',
257
285
  });
258
286
  startupSpan.end({ listening: true });
259
287
  process.stdout.write(
260
- `[control-plane] listening host=${address.address} port=${String(address.port)} auth=${options.authToken === null ? 'off' : 'on'} db=${options.stateDbPath}\n`,
288
+ `[control-plane] listening host=${address.address} port=${String(address.port)} auth=${options.authToken === null ? 'off' : 'on'} db=${options.stateDbPath} runId=${gatewayRunId}\n`,
261
289
  );
262
290
 
263
291
  let stopRequested = false;