@jmoyers/harness 0.1.10 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -7,8 +7,13 @@ import {
7
7
  unlinkSync,
8
8
  writeFileSync,
9
9
  } from 'node:fs';
10
+ import { homedir } from 'node:os';
10
11
  import { dirname, resolve } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
13
+ import {
14
+ DEFAULT_STORAGE_LIFECYCLE_POLICY,
15
+ type StorageLifecyclePolicy,
16
+ } from '../storage/storage-lifecycle-core.ts';
12
17
 
13
18
  export const HARNESS_CONFIG_FILE_NAME = 'harness.config.jsonc';
14
19
  export const HARNESS_CONFIG_VERSION = 1;
@@ -40,13 +45,46 @@ const HARNESS_LIFECYCLE_EVENT_TYPES = [
40
45
 
41
46
  export type HarnessLifecycleEventType = (typeof HARNESS_LIFECYCLE_EVENT_TYPES)[number];
42
47
 
48
+ export const HARNESS_MUX_OPEN_IN_TARGET_IDS = [
49
+ 'iterm2',
50
+ 'ghostty',
51
+ 'zed',
52
+ 'cursor',
53
+ 'vscode',
54
+ 'warp',
55
+ 'finder',
56
+ ] as const;
57
+
58
+ export type HarnessMuxOpenInTargetId = (typeof HARNESS_MUX_OPEN_IN_TARGET_IDS)[number];
59
+
60
+ export interface HarnessMuxOpenInTargetOverrideConfig {
61
+ readonly enabled?: boolean;
62
+ readonly appName?: string;
63
+ readonly detectCommand?: string | null;
64
+ readonly launchCommand?: readonly string[];
65
+ }
66
+
67
+ interface HarnessMuxOpenInLinkConfig {
68
+ readonly browserCommand: readonly string[] | null;
69
+ readonly fileCommand: readonly string[] | null;
70
+ }
71
+
72
+ interface HarnessMuxOpenInConfig {
73
+ readonly targets: Readonly<
74
+ Partial<Record<HarnessMuxOpenInTargetId, HarnessMuxOpenInTargetOverrideConfig>>
75
+ >;
76
+ readonly links: HarnessMuxOpenInLinkConfig;
77
+ }
78
+
43
79
  interface HarnessMuxConfig {
44
80
  readonly keybindings: Readonly<Record<string, readonly string[]>>;
45
81
  readonly ui: HarnessMuxUiConfig;
46
82
  readonly git: HarnessMuxGitConfig;
83
+ readonly openIn: HarnessMuxOpenInConfig;
47
84
  }
48
85
 
49
86
  type HarnessMuxThemeMode = 'dark' | 'light';
87
+ type HarnessMuxStartupPane = 'home' | 'nim';
50
88
 
51
89
  export interface HarnessMuxThemeConfig {
52
90
  readonly preset: string;
@@ -58,6 +96,9 @@ interface HarnessMuxUiConfig {
58
96
  readonly paneWidthPercent: number | null;
59
97
  readonly repositoriesCollapsed: boolean;
60
98
  readonly shortcutsCollapsed: boolean;
99
+ readonly startupPane: HarnessMuxStartupPane;
100
+ readonly showTasks: boolean;
101
+ readonly showDebugBar: boolean;
61
102
  readonly theme: HarnessMuxThemeConfig | null;
62
103
  }
63
104
 
@@ -83,6 +124,16 @@ interface HarnessGitHubConfig {
83
124
  readonly viewerLogin: string | null;
84
125
  }
85
126
 
127
+ interface HarnessLinearConfig {
128
+ readonly enabled: boolean;
129
+ readonly apiBaseUrl: string;
130
+ readonly tokenEnvVar: string;
131
+ }
132
+
133
+ interface HarnessGatewayConfig {
134
+ readonly host: string;
135
+ }
136
+
86
137
  interface HarnessPerfConfig {
87
138
  readonly enabled: boolean;
88
139
  readonly filePath: string;
@@ -215,15 +266,24 @@ interface HarnessHooksConfig {
215
266
  readonly lifecycle: HarnessLifecycleHooksConfig;
216
267
  }
217
268
 
269
+ export type HarnessStorageLifecycleConfig = StorageLifecyclePolicy;
270
+
271
+ interface HarnessStorageConfig {
272
+ readonly lifecycle: HarnessStorageLifecycleConfig;
273
+ }
274
+
218
275
  interface HarnessConfig {
219
276
  readonly configVersion: number;
220
277
  readonly mux: HarnessMuxConfig;
221
278
  readonly github: HarnessGitHubConfig;
279
+ readonly gateway: HarnessGatewayConfig;
280
+ readonly linear: HarnessLinearConfig;
222
281
  readonly debug: HarnessDebugConfig;
223
282
  readonly codex: HarnessCodexConfig;
224
283
  readonly claude: HarnessClaudeConfig;
225
284
  readonly cursor: HarnessCursorConfig;
226
285
  readonly critique: HarnessCritiqueConfig;
286
+ readonly storage: HarnessStorageConfig;
227
287
  readonly hooks: HarnessHooksConfig;
228
288
  }
229
289
 
@@ -242,6 +302,9 @@ export const DEFAULT_HARNESS_CONFIG: HarnessConfig = {
242
302
  paneWidthPercent: null,
243
303
  repositoriesCollapsed: false,
244
304
  shortcutsCollapsed: false,
305
+ startupPane: 'home',
306
+ showTasks: false,
307
+ showDebugBar: false,
245
308
  theme: null,
246
309
  },
247
310
  git: {
@@ -253,6 +316,13 @@ export const DEFAULT_HARNESS_CONFIG: HarnessConfig = {
253
316
  triggerDebounceMs: 180,
254
317
  maxConcurrency: 1,
255
318
  },
319
+ openIn: {
320
+ targets: {},
321
+ links: {
322
+ browserCommand: null,
323
+ fileCommand: null,
324
+ },
325
+ },
256
326
  },
257
327
  github: {
258
328
  enabled: true,
@@ -263,6 +333,14 @@ export const DEFAULT_HARNESS_CONFIG: HarnessConfig = {
263
333
  branchStrategy: 'pinned-then-current',
264
334
  viewerLogin: null,
265
335
  },
336
+ gateway: {
337
+ host: '127.0.0.1',
338
+ },
339
+ linear: {
340
+ enabled: true,
341
+ apiBaseUrl: 'https://api.linear.app/graphql',
342
+ tokenEnvVar: 'LINEAR_API_KEY',
343
+ },
266
344
  debug: {
267
345
  enabled: true,
268
346
  overwriteArtifactsOnStart: true,
@@ -335,6 +413,11 @@ export const DEFAULT_HARNESS_CONFIG: HarnessConfig = {
335
413
  command: 'bun add --global critique@latest',
336
414
  },
337
415
  },
416
+ storage: {
417
+ lifecycle: {
418
+ ...DEFAULT_STORAGE_LIFECYCLE_POLICY,
419
+ },
420
+ },
338
421
  hooks: {
339
422
  lifecycle: {
340
423
  enabled: false,
@@ -538,6 +621,16 @@ function normalizeMuxThemeMode(value: unknown, fallback: HarnessMuxThemeMode): H
538
621
  return fallback;
539
622
  }
540
623
 
624
+ function normalizeMuxStartupPane(
625
+ value: unknown,
626
+ fallback: HarnessMuxStartupPane,
627
+ ): HarnessMuxStartupPane {
628
+ if (value === 'home' || value === 'nim') {
629
+ return value;
630
+ }
631
+ return fallback;
632
+ }
633
+
541
634
  function normalizeMuxThemeConfig(input: unknown): HarnessMuxThemeConfig | null {
542
635
  if (input === null || input === false) {
543
636
  return null;
@@ -573,18 +666,33 @@ function normalizeMuxUiConfig(input: unknown): HarnessMuxUiConfig {
573
666
  record['paneWidthPercent'],
574
667
  DEFAULT_HARNESS_CONFIG.mux.ui.paneWidthPercent,
575
668
  );
576
- const shortcutsCollapsed =
577
- typeof record['shortcutsCollapsed'] === 'boolean'
578
- ? record['shortcutsCollapsed']
579
- : DEFAULT_HARNESS_CONFIG.mux.ui.shortcutsCollapsed;
580
669
  const repositoriesCollapsed =
581
670
  typeof record['repositoriesCollapsed'] === 'boolean'
582
671
  ? record['repositoriesCollapsed']
583
672
  : DEFAULT_HARNESS_CONFIG.mux.ui.repositoriesCollapsed;
673
+ const shortcutsCollapsed =
674
+ typeof record['shortcutsCollapsed'] === 'boolean'
675
+ ? record['shortcutsCollapsed']
676
+ : DEFAULT_HARNESS_CONFIG.mux.ui.shortcutsCollapsed;
677
+ const startupPane = normalizeMuxStartupPane(
678
+ record['startupPane'],
679
+ DEFAULT_HARNESS_CONFIG.mux.ui.startupPane,
680
+ );
681
+ const showTasks =
682
+ typeof record['showTasks'] === 'boolean'
683
+ ? record['showTasks']
684
+ : DEFAULT_HARNESS_CONFIG.mux.ui.showTasks;
685
+ const showDebugBar =
686
+ typeof record['showDebugBar'] === 'boolean'
687
+ ? record['showDebugBar']
688
+ : DEFAULT_HARNESS_CONFIG.mux.ui.showDebugBar;
584
689
  return {
585
690
  paneWidthPercent,
586
691
  repositoriesCollapsed,
587
692
  shortcutsCollapsed,
693
+ startupPane,
694
+ showTasks,
695
+ showDebugBar,
588
696
  theme: normalizeMuxThemeConfig(record['theme']),
589
697
  };
590
698
  }
@@ -629,6 +737,113 @@ function normalizeMuxGitConfig(input: unknown): HarnessMuxGitConfig {
629
737
  };
630
738
  }
631
739
 
740
+ function normalizeMuxOpenInTargetOverride(
741
+ input: unknown,
742
+ ): HarnessMuxOpenInTargetOverrideConfig | null {
743
+ const record = asRecord(input);
744
+ if (record === null) {
745
+ return null;
746
+ }
747
+ const enabled = typeof record['enabled'] === 'boolean' ? record['enabled'] : undefined;
748
+ const appName =
749
+ typeof record['appName'] === 'string' && record['appName'].trim().length > 0
750
+ ? record['appName'].trim()
751
+ : undefined;
752
+ const detectCommand =
753
+ record['detectCommand'] === null
754
+ ? null
755
+ : typeof record['detectCommand'] === 'string' && record['detectCommand'].trim().length > 0
756
+ ? record['detectCommand'].trim()
757
+ : undefined;
758
+ const launchCommandFromArray = Array.isArray(record['launchCommand'])
759
+ ? record['launchCommand']
760
+ .flatMap((entry) => (typeof entry === 'string' ? [entry.trim()] : []))
761
+ .filter((entry) => entry.length > 0)
762
+ : null;
763
+ const launchCommand =
764
+ launchCommandFromArray !== null
765
+ ? launchCommandFromArray.length > 0
766
+ ? launchCommandFromArray
767
+ : undefined
768
+ : typeof record['launchCommand'] === 'string' && record['launchCommand'].trim().length > 0
769
+ ? [record['launchCommand'].trim()]
770
+ : undefined;
771
+ if (
772
+ enabled === undefined &&
773
+ appName === undefined &&
774
+ detectCommand === undefined &&
775
+ launchCommand === undefined
776
+ ) {
777
+ return null;
778
+ }
779
+ return {
780
+ ...(enabled === undefined ? {} : { enabled }),
781
+ ...(appName === undefined ? {} : { appName }),
782
+ ...(detectCommand === undefined ? {} : { detectCommand }),
783
+ ...(launchCommand === undefined ? {} : { launchCommand }),
784
+ };
785
+ }
786
+
787
+ function normalizeOpenInLinkCommand(
788
+ input: unknown,
789
+ fallback: readonly string[] | null,
790
+ ): readonly string[] | null {
791
+ if (input === null) {
792
+ return null;
793
+ }
794
+ if (Array.isArray(input)) {
795
+ const normalized = input
796
+ .flatMap((entry) => (typeof entry === 'string' ? [entry.trim()] : []))
797
+ .filter((entry) => entry.length > 0);
798
+ return normalized.length > 0 ? normalized : null;
799
+ }
800
+ if (typeof input === 'string' && input.trim().length > 0) {
801
+ return [input.trim()];
802
+ }
803
+ return fallback;
804
+ }
805
+
806
+ function normalizeMuxOpenInLinksConfig(input: unknown): HarnessMuxOpenInLinkConfig {
807
+ const record = asRecord(input);
808
+ if (record === null) {
809
+ return DEFAULT_HARNESS_CONFIG.mux.openIn.links;
810
+ }
811
+ return {
812
+ browserCommand: normalizeOpenInLinkCommand(
813
+ record['browserCommand'],
814
+ DEFAULT_HARNESS_CONFIG.mux.openIn.links.browserCommand,
815
+ ),
816
+ fileCommand: normalizeOpenInLinkCommand(
817
+ record['fileCommand'],
818
+ DEFAULT_HARNESS_CONFIG.mux.openIn.links.fileCommand,
819
+ ),
820
+ };
821
+ }
822
+
823
+ function normalizeMuxOpenInConfig(input: unknown): HarnessMuxOpenInConfig {
824
+ const record = asRecord(input);
825
+ if (record === null) {
826
+ return DEFAULT_HARNESS_CONFIG.mux.openIn;
827
+ }
828
+ const targetsRecord = asRecord(record['targets']);
829
+ const normalizedTargets: Partial<
830
+ Record<HarnessMuxOpenInTargetId, HarnessMuxOpenInTargetOverrideConfig>
831
+ > = {};
832
+ if (targetsRecord !== null) {
833
+ for (const targetId of HARNESS_MUX_OPEN_IN_TARGET_IDS) {
834
+ const raw = targetsRecord[targetId];
835
+ const normalized = normalizeMuxOpenInTargetOverride(raw);
836
+ if (normalized !== null) {
837
+ normalizedTargets[targetId] = normalized;
838
+ }
839
+ }
840
+ }
841
+ return {
842
+ targets: normalizedTargets,
843
+ links: normalizeMuxOpenInLinksConfig(record['links']),
844
+ };
845
+ }
846
+
632
847
  function normalizeGitHubBranchStrategy(value: unknown): HarnessGitHubBranchStrategy {
633
848
  if (value === 'current-only' || value === 'pinned-only' || value === 'pinned-then-current') {
634
849
  return value;
@@ -679,6 +894,41 @@ function normalizeGitHubConfig(input: unknown): HarnessGitHubConfig {
679
894
  };
680
895
  }
681
896
 
897
+ function normalizeGatewayConfig(input: unknown): HarnessGatewayConfig {
898
+ const record = asRecord(input);
899
+ if (record === null) {
900
+ return DEFAULT_HARNESS_CONFIG.gateway;
901
+ }
902
+ return {
903
+ host: normalizeHost(record['host'], DEFAULT_HARNESS_CONFIG.gateway.host),
904
+ };
905
+ }
906
+
907
+ function normalizeLinearConfig(input: unknown): HarnessLinearConfig {
908
+ const record = asRecord(input);
909
+ if (record === null) {
910
+ return DEFAULT_HARNESS_CONFIG.linear;
911
+ }
912
+ const tokenEnvVarRaw = record['tokenEnvVar'];
913
+ const tokenEnvVar =
914
+ typeof tokenEnvVarRaw === 'string' && tokenEnvVarRaw.trim().length > 0
915
+ ? tokenEnvVarRaw.trim()
916
+ : DEFAULT_HARNESS_CONFIG.linear.tokenEnvVar;
917
+ const apiBaseUrlRaw = record['apiBaseUrl'];
918
+ const apiBaseUrl =
919
+ typeof apiBaseUrlRaw === 'string' && apiBaseUrlRaw.trim().length > 0
920
+ ? apiBaseUrlRaw.trim().replace(/\/+$/u, '')
921
+ : DEFAULT_HARNESS_CONFIG.linear.apiBaseUrl;
922
+ return {
923
+ enabled:
924
+ typeof record['enabled'] === 'boolean'
925
+ ? record['enabled']
926
+ : DEFAULT_HARNESS_CONFIG.linear.enabled,
927
+ apiBaseUrl,
928
+ tokenEnvVar,
929
+ };
930
+ }
931
+
682
932
  function normalizePerfConfig(input: unknown): HarnessPerfConfig {
683
933
  const record = asRecord(input);
684
934
  if (record === null) {
@@ -709,6 +959,14 @@ function normalizeNonNegativeInt(value: unknown, fallback: number): number {
709
959
  return normalized;
710
960
  }
711
961
 
962
+ function normalizePositiveInt(value: unknown, fallback: number): number {
963
+ const normalized = normalizeNonNegativeInt(value, fallback);
964
+ if (normalized < 1) {
965
+ return fallback;
966
+ }
967
+ return normalized;
968
+ }
969
+
712
970
  function normalizeDebugMuxConfig(input: unknown): HarnessDebugMuxConfig {
713
971
  const record = asRecord(input);
714
972
  if (record === null) {
@@ -1304,6 +1562,69 @@ function normalizeLifecycleWebhookConfig(
1304
1562
  };
1305
1563
  }
1306
1564
 
1565
+ function normalizeStorageLifecycleConfig(input: unknown): HarnessStorageLifecycleConfig {
1566
+ const record = asRecord(input);
1567
+ if (record === null) {
1568
+ return DEFAULT_HARNESS_CONFIG.storage.lifecycle;
1569
+ }
1570
+ return {
1571
+ eventRetentionMs: normalizePositiveInt(
1572
+ record['eventRetentionMs'],
1573
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.eventRetentionMs,
1574
+ ),
1575
+ telemetryRetentionMs: normalizePositiveInt(
1576
+ record['telemetryRetentionMs'],
1577
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.telemetryRetentionMs,
1578
+ ),
1579
+ maintenanceIntervalMs: normalizePositiveInt(
1580
+ record['maintenanceIntervalMs'],
1581
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.maintenanceIntervalMs,
1582
+ ),
1583
+ pruneBatchSize: normalizePositiveInt(
1584
+ record['pruneBatchSize'],
1585
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.pruneBatchSize,
1586
+ ),
1587
+ compactFreelistPages: normalizePositiveInt(
1588
+ record['compactFreelistPages'],
1589
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.compactFreelistPages,
1590
+ ),
1591
+ copyForwardBatchSize: normalizePositiveInt(
1592
+ record['copyForwardBatchSize'],
1593
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.copyForwardBatchSize,
1594
+ ),
1595
+ copyForwardFinalizeTailRows: normalizePositiveInt(
1596
+ record['copyForwardFinalizeTailRows'],
1597
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.copyForwardFinalizeTailRows,
1598
+ ),
1599
+ telemetryPayloadMaxBytes: normalizePositiveInt(
1600
+ record['telemetryPayloadMaxBytes'],
1601
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.telemetryPayloadMaxBytes,
1602
+ ),
1603
+ textDeltaPayloadMaxBytes: normalizePositiveInt(
1604
+ record['textDeltaPayloadMaxBytes'],
1605
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.textDeltaPayloadMaxBytes,
1606
+ ),
1607
+ textDeltaCoalesceWindowMs: normalizePositiveInt(
1608
+ record['textDeltaCoalesceWindowMs'],
1609
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.textDeltaCoalesceWindowMs,
1610
+ ),
1611
+ busyTimeoutMs: normalizePositiveInt(
1612
+ record['busyTimeoutMs'],
1613
+ DEFAULT_HARNESS_CONFIG.storage.lifecycle.busyTimeoutMs,
1614
+ ),
1615
+ };
1616
+ }
1617
+
1618
+ function normalizeStorageConfig(input: unknown): HarnessStorageConfig {
1619
+ const record = asRecord(input);
1620
+ if (record === null) {
1621
+ return DEFAULT_HARNESS_CONFIG.storage;
1622
+ }
1623
+ return {
1624
+ lifecycle: normalizeStorageLifecycleConfig(record['lifecycle']),
1625
+ };
1626
+ }
1627
+
1307
1628
  function normalizeLifecycleHooksConfig(input: unknown): HarnessLifecycleHooksConfig {
1308
1629
  const record = asRecord(input);
1309
1630
  if (record === null) {
@@ -1371,12 +1692,15 @@ export function parseHarnessConfigText(text: string): HarnessConfig {
1371
1692
 
1372
1693
  const mux = asRecord(migratedRoot['mux']);
1373
1694
  const github = normalizeGitHubConfig(migratedRoot['github']);
1695
+ const gateway = normalizeGatewayConfig(migratedRoot['gateway']);
1696
+ const linear = normalizeLinearConfig(migratedRoot['linear']);
1374
1697
  const legacyPerf = normalizePerfConfig(migratedRoot['perf']);
1375
1698
  const debug = normalizeDebugConfig(migratedRoot['debug'], legacyPerf);
1376
1699
  const codex = normalizeCodexConfig(migratedRoot['codex']);
1377
1700
  const claude = normalizeClaudeConfig(migratedRoot['claude']);
1378
1701
  const cursor = normalizeCursorConfig(migratedRoot['cursor']);
1379
1702
  const critique = normalizeCritiqueConfig(migratedRoot['critique']);
1703
+ const storage = normalizeStorageConfig(migratedRoot['storage']);
1380
1704
  const hooks = normalizeLifecycleHooksConfig(asRecord(migratedRoot['hooks'])?.['lifecycle']);
1381
1705
 
1382
1706
  return {
@@ -1385,13 +1709,18 @@ export function parseHarnessConfigText(text: string): HarnessConfig {
1385
1709
  keybindings: mux === null ? {} : normalizeKeybindings(mux['keybindings']),
1386
1710
  ui: mux === null ? DEFAULT_HARNESS_CONFIG.mux.ui : normalizeMuxUiConfig(mux['ui']),
1387
1711
  git: mux === null ? DEFAULT_HARNESS_CONFIG.mux.git : normalizeMuxGitConfig(mux['git']),
1712
+ openIn:
1713
+ mux === null ? DEFAULT_HARNESS_CONFIG.mux.openIn : normalizeMuxOpenInConfig(mux['openIn']),
1388
1714
  },
1389
1715
  github,
1716
+ gateway,
1717
+ linear,
1390
1718
  debug,
1391
1719
  codex,
1392
1720
  claude,
1393
1721
  cursor,
1394
1722
  critique,
1723
+ storage,
1395
1724
  hooks: {
1396
1725
  lifecycle: hooks,
1397
1726
  },
@@ -1435,14 +1764,24 @@ export function resolveHarnessConfigDirectory(
1435
1764
  env: NodeJS.ProcessEnv = process.env,
1436
1765
  ): string {
1437
1766
  const xdgConfigHome = readNonEmptyEnvPath(env.XDG_CONFIG_HOME);
1767
+ const homeDirectory = readNonEmptyEnvPath(env.HOME) ?? readNonEmptyEnvPath(homedir());
1768
+ return resolveHarnessConfigDirectoryFromRoots(cwd, xdgConfigHome, homeDirectory);
1769
+ }
1770
+
1771
+ export function resolveHarnessConfigDirectoryFromRoots(
1772
+ cwd: string,
1773
+ xdgConfigHome: string | null,
1774
+ homeDirectory: string | null,
1775
+ ): string {
1438
1776
  if (xdgConfigHome !== null) {
1439
1777
  return resolve(xdgConfigHome, HARNESS_CONFIG_XDG_DIRECTORY_NAME);
1440
1778
  }
1441
- const homeDirectory = readNonEmptyEnvPath(env.HOME);
1442
1779
  if (homeDirectory !== null) {
1443
1780
  return resolve(homeDirectory, HARNESS_CONFIG_HOME_DIRECTORY_NAME);
1444
1781
  }
1445
- return resolve(cwd, HARNESS_CONFIG_HOME_DIRECTORY_NAME);
1782
+ throw new Error(
1783
+ `unable to resolve harness config directory: HOME and XDG_CONFIG_HOME are unset (cwd=${cwd})`,
1784
+ );
1446
1785
  }
1447
1786
 
1448
1787
  export function resolveHarnessConfigPath(
@@ -1544,6 +1883,9 @@ export function updateHarnessMuxUiConfig(
1544
1883
  paneWidthPercent: number | null;
1545
1884
  repositoriesCollapsed: boolean;
1546
1885
  shortcutsCollapsed: boolean;
1886
+ startupPane: HarnessMuxStartupPane;
1887
+ showTasks: boolean;
1888
+ showDebugBar: boolean;
1547
1889
  }>,
1548
1890
  options?: {
1549
1891
  cwd?: string;
@@ -1562,14 +1904,20 @@ export function updateHarnessMuxUiConfig(
1562
1904
  update.paneWidthPercent === undefined
1563
1905
  ? current.mux.ui.paneWidthPercent
1564
1906
  : normalizePaneWidthPercent(update.paneWidthPercent, null);
1565
- const nextShortcutsCollapsed =
1566
- update.shortcutsCollapsed === undefined
1567
- ? current.mux.ui.shortcutsCollapsed
1568
- : update.shortcutsCollapsed;
1569
1907
  const nextRepositoriesCollapsed =
1570
1908
  update.repositoriesCollapsed === undefined
1571
1909
  ? current.mux.ui.repositoriesCollapsed
1572
1910
  : update.repositoriesCollapsed;
1911
+ const nextShortcutsCollapsed =
1912
+ update.shortcutsCollapsed === undefined
1913
+ ? current.mux.ui.shortcutsCollapsed
1914
+ : update.shortcutsCollapsed;
1915
+ const nextStartupPane =
1916
+ update.startupPane === undefined ? current.mux.ui.startupPane : update.startupPane;
1917
+ const nextShowTasks =
1918
+ update.showTasks === undefined ? current.mux.ui.showTasks : update.showTasks;
1919
+ const nextShowDebugBar =
1920
+ update.showDebugBar === undefined ? current.mux.ui.showDebugBar : update.showDebugBar;
1573
1921
  return {
1574
1922
  ...current,
1575
1923
  mux: {
@@ -1579,6 +1927,9 @@ export function updateHarnessMuxUiConfig(
1579
1927
  nextPaneWidthPercent === null ? null : roundUiPercent(nextPaneWidthPercent),
1580
1928
  repositoriesCollapsed: nextRepositoriesCollapsed,
1581
1929
  shortcutsCollapsed: nextShortcutsCollapsed,
1930
+ startupPane: nextStartupPane,
1931
+ showTasks: nextShowTasks,
1932
+ showDebugBar: nextShowDebugBar,
1582
1933
  theme: current.mux.ui.theme,
1583
1934
  },
1584
1935
  },
@@ -39,11 +39,7 @@ export function resolveHarnessWorkspaceDirectory(
39
39
  invocationDirectory: string,
40
40
  env: NodeJS.ProcessEnv = process.env,
41
41
  ): string {
42
- const legacyWorkspaceDirectory = resolveLegacyHarnessDirectory(invocationDirectory);
43
42
  const configDirectory = resolveHarnessConfigDirectory(invocationDirectory, env);
44
- if (resolve(configDirectory) === legacyWorkspaceDirectory) {
45
- return legacyWorkspaceDirectory;
46
- }
47
43
  return resolve(
48
44
  configDirectory,
49
45
  HARNESS_WORKSPACES_DIRECTORY,
@@ -74,13 +70,14 @@ export function resolveHarnessRuntimePath(
74
70
  pathValue: string,
75
71
  env: NodeJS.ProcessEnv = process.env,
76
72
  ): string {
73
+ const workspaceRuntimeDirectory = resolveHarnessWorkspaceDirectory(invocationDirectory, env);
77
74
  const normalizedPath = pathValue.trim();
78
75
  if (normalizedPath.length === 0 || normalizedPath === HARNESS_LEGACY_RELATIVE_ROOT) {
79
- return resolveHarnessWorkspaceDirectory(invocationDirectory, env);
76
+ return workspaceRuntimeDirectory;
80
77
  }
81
78
  if (normalizedPath.startsWith(`${HARNESS_LEGACY_RELATIVE_ROOT}/`)) {
82
79
  return resolve(
83
- resolveHarnessWorkspaceDirectory(invocationDirectory, env),
80
+ workspaceRuntimeDirectory,
84
81
  normalizedPath.slice(`${HARNESS_LEGACY_RELATIVE_ROOT}/`.length),
85
82
  );
86
83
  }
@@ -88,5 +85,5 @@ export function resolveHarnessRuntimePath(
88
85
  if (expandedHomePath !== null) {
89
86
  return expandedHomePath;
90
87
  }
91
- return resolve(invocationDirectory, normalizedPath);
88
+ return resolve(workspaceRuntimeDirectory, normalizedPath);
92
89
  }