@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
@@ -1,6 +1,20 @@
1
- import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
1
+ import {
2
+ copyFileSync,
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
2
11
  import { dirname, resolve } from 'node:path';
3
- import { HARNESS_CONFIG_FILE_NAME, resolveHarnessConfigDirectory } from './config-core.ts';
12
+ import {
13
+ DEFAULT_HARNESS_CONFIG,
14
+ HARNESS_CONFIG_FILE_NAME,
15
+ parseHarnessConfigText,
16
+ resolveHarnessConfigDirectory,
17
+ } from './config-core.ts';
4
18
  import {
5
19
  resolveHarnessWorkspaceDirectory,
6
20
  resolveLegacyHarnessDirectory,
@@ -8,6 +22,7 @@ import {
8
22
 
9
23
  const LEGACY_SECRETS_FILE_NAME = 'secrets.env';
10
24
  const MIGRATION_MARKER_FILE_NAME = '.legacy-layout-migration-v1';
25
+ const MIGRATION_CONFIG_BACKUP_FILE_NAME = `${HARNESS_CONFIG_FILE_NAME}.pre-migration.bak`;
11
26
  const LEGACY_RUNTIME_EXCLUDE_NAMES = new Set([
12
27
  HARNESS_CONFIG_FILE_NAME,
13
28
  LEGACY_SECRETS_FILE_NAME,
@@ -18,9 +33,18 @@ interface HarnessLegacyLayoutMigrationResult {
18
33
  readonly migrated: boolean;
19
34
  readonly migratedEntries: number;
20
35
  readonly configCopied: boolean;
36
+ readonly configReplacedExisting: boolean;
37
+ readonly configBackupPath: string | null;
21
38
  readonly secretsCopied: boolean;
22
39
  readonly skipped: boolean;
23
40
  readonly markerPath: string;
41
+ readonly legacyRootRemoved: boolean;
42
+ }
43
+
44
+ interface ConfigCopyResult {
45
+ readonly copied: boolean;
46
+ readonly replacedExisting: boolean;
47
+ readonly backupPath: string | null;
24
48
  }
25
49
 
26
50
  function copyFileIfMissing(sourcePath: string, targetPath: string): boolean {
@@ -46,11 +70,96 @@ function copyEntryIfMissing(sourcePath: string, targetPath: string): boolean {
46
70
  return !targetExisted;
47
71
  }
48
72
 
73
+ function configEqualsDefaultConfig(text: string): boolean {
74
+ try {
75
+ const parsed = parseHarnessConfigText(text);
76
+ return JSON.stringify(parsed) === JSON.stringify(DEFAULT_HARNESS_CONFIG);
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function copyConfigIfGlobalUninitialized(sourcePath: string, targetPath: string): ConfigCopyResult {
83
+ if (!existsSync(sourcePath)) {
84
+ return {
85
+ copied: false,
86
+ replacedExisting: false,
87
+ backupPath: null,
88
+ };
89
+ }
90
+ if (!existsSync(targetPath)) {
91
+ mkdirSync(dirname(targetPath), { recursive: true });
92
+ copyFileSync(sourcePath, targetPath);
93
+ return {
94
+ copied: true,
95
+ replacedExisting: false,
96
+ backupPath: null,
97
+ };
98
+ }
99
+
100
+ const targetText = readFileSync(targetPath, 'utf8');
101
+ const targetUninitialized =
102
+ targetText.trim().length === 0 || configEqualsDefaultConfig(targetText);
103
+ if (!targetUninitialized) {
104
+ return {
105
+ copied: false,
106
+ replacedExisting: false,
107
+ backupPath: null,
108
+ };
109
+ }
110
+
111
+ const backupPath = resolve(dirname(targetPath), MIGRATION_CONFIG_BACKUP_FILE_NAME);
112
+ if (!existsSync(backupPath)) {
113
+ copyFileSync(targetPath, backupPath);
114
+ }
115
+ copyFileSync(sourcePath, targetPath);
116
+ return {
117
+ copied: true,
118
+ replacedExisting: true,
119
+ backupPath,
120
+ };
121
+ }
122
+
49
123
  function writeMigrationMarker(markerPath: string): void {
50
124
  mkdirSync(dirname(markerPath), { recursive: true });
51
125
  writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8');
52
126
  }
53
127
 
128
+ function removeLegacyRootIfSafe(
129
+ legacyRoot: string,
130
+ configDirectory: string,
131
+ workspaceDirectory: string,
132
+ ): boolean {
133
+ if (!existsSync(legacyRoot) || resolve(configDirectory) === legacyRoot) {
134
+ return false;
135
+ }
136
+
137
+ const legacyEntries = readdirSync(legacyRoot, { withFileTypes: true }).map((entry) => entry.name);
138
+ for (const entryName of legacyEntries) {
139
+ if (entryName === HARNESS_CONFIG_FILE_NAME) {
140
+ if (!existsSync(resolve(configDirectory, HARNESS_CONFIG_FILE_NAME))) {
141
+ return false;
142
+ }
143
+ continue;
144
+ }
145
+ if (entryName === LEGACY_SECRETS_FILE_NAME) {
146
+ if (!existsSync(resolve(configDirectory, LEGACY_SECRETS_FILE_NAME))) {
147
+ return false;
148
+ }
149
+ continue;
150
+ }
151
+ if (entryName === 'workspaces') {
152
+ continue;
153
+ }
154
+ if (!existsSync(resolve(workspaceDirectory, entryName))) {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ rmSync(legacyRoot, { recursive: true, force: true });
160
+ return !existsSync(legacyRoot);
161
+ }
162
+
54
163
  export function migrateLegacyHarnessLayout(
55
164
  invocationDirectory: string,
56
165
  env: NodeJS.ProcessEnv = process.env,
@@ -60,7 +169,7 @@ export function migrateLegacyHarnessLayout(
60
169
  const workspaceDirectory = resolveHarnessWorkspaceDirectory(invocationDirectory, env);
61
170
  const markerPath = resolve(workspaceDirectory, MIGRATION_MARKER_FILE_NAME);
62
171
 
63
- const configCopied = copyFileIfMissing(
172
+ const configCopy = copyConfigIfGlobalUninitialized(
64
173
  resolve(legacyRoot, HARNESS_CONFIG_FILE_NAME),
65
174
  resolve(configDirectory, HARNESS_CONFIG_FILE_NAME),
66
175
  );
@@ -68,38 +177,50 @@ export function migrateLegacyHarnessLayout(
68
177
  resolve(legacyRoot, LEGACY_SECRETS_FILE_NAME),
69
178
  resolve(configDirectory, LEGACY_SECRETS_FILE_NAME),
70
179
  );
180
+ const withCleanupResult = (
181
+ result: Omit<HarnessLegacyLayoutMigrationResult, 'legacyRootRemoved'>,
182
+ ): HarnessLegacyLayoutMigrationResult => ({
183
+ ...result,
184
+ legacyRootRemoved: removeLegacyRootIfSafe(legacyRoot, configDirectory, workspaceDirectory),
185
+ });
71
186
 
72
187
  if (resolve(configDirectory) === legacyRoot) {
73
- return {
74
- migrated: configCopied || secretsCopied,
188
+ return withCleanupResult({
189
+ migrated: configCopy.copied || secretsCopied,
75
190
  migratedEntries: 0,
76
- configCopied,
191
+ configCopied: configCopy.copied,
192
+ configReplacedExisting: configCopy.replacedExisting,
193
+ configBackupPath: configCopy.backupPath,
77
194
  secretsCopied,
78
195
  skipped: true,
79
196
  markerPath,
80
- };
197
+ });
81
198
  }
82
199
 
83
200
  if (!existsSync(legacyRoot)) {
84
- return {
85
- migrated: configCopied || secretsCopied,
201
+ return withCleanupResult({
202
+ migrated: configCopy.copied || secretsCopied,
86
203
  migratedEntries: 0,
87
- configCopied,
204
+ configCopied: configCopy.copied,
205
+ configReplacedExisting: configCopy.replacedExisting,
206
+ configBackupPath: configCopy.backupPath,
88
207
  secretsCopied,
89
208
  skipped: true,
90
209
  markerPath,
91
- };
210
+ });
92
211
  }
93
212
 
94
213
  if (existsSync(markerPath)) {
95
- return {
96
- migrated: configCopied || secretsCopied,
214
+ return withCleanupResult({
215
+ migrated: configCopy.copied || secretsCopied,
97
216
  migratedEntries: 0,
98
- configCopied,
217
+ configCopied: configCopy.copied,
218
+ configReplacedExisting: configCopy.replacedExisting,
219
+ configBackupPath: configCopy.backupPath,
99
220
  secretsCopied,
100
221
  skipped: true,
101
222
  markerPath,
102
- };
223
+ });
103
224
  }
104
225
 
105
226
  const legacyEntries = readdirSync(legacyRoot, { withFileTypes: true })
@@ -119,12 +240,14 @@ export function migrateLegacyHarnessLayout(
119
240
  writeMigrationMarker(markerPath);
120
241
  }
121
242
 
122
- return {
123
- migrated: configCopied || secretsCopied || migratedEntries > 0,
243
+ return withCleanupResult({
244
+ migrated: configCopy.copied || secretsCopied || migratedEntries > 0,
124
245
  migratedEntries,
125
- configCopied,
246
+ configCopied: configCopy.copied,
247
+ configReplacedExisting: configCopy.replacedExisting,
248
+ configBackupPath: configCopy.backupPath,
126
249
  secretsCopied,
127
250
  skipped: false,
128
251
  markerPath,
129
- };
252
+ });
130
253
  }
@@ -6,6 +6,9 @@
6
6
  "paneWidthPercent": null,
7
7
  "repositoriesCollapsed": false,
8
8
  "shortcutsCollapsed": false,
9
+ "startupPane": "home",
10
+ "showTasks": false,
11
+ "showDebugBar": false,
9
12
  "theme": null,
10
13
  },
11
14
  "git": {
@@ -17,6 +20,13 @@
17
20
  "triggerDebounceMs": 180,
18
21
  "maxConcurrency": 1,
19
22
  },
23
+ "openIn": {
24
+ "targets": {},
25
+ "links": {
26
+ "browserCommand": null,
27
+ "fileCommand": null,
28
+ },
29
+ },
20
30
  },
21
31
  "github": {
22
32
  "enabled": true,
@@ -27,6 +37,14 @@
27
37
  "branchStrategy": "pinned-then-current",
28
38
  "viewerLogin": null,
29
39
  },
40
+ "gateway": {
41
+ "host": "127.0.0.1",
42
+ },
43
+ "linear": {
44
+ "enabled": true,
45
+ "apiBaseUrl": "https://api.linear.app/graphql",
46
+ "tokenEnvVar": "LINEAR_API_KEY",
47
+ },
30
48
  "debug": {
31
49
  "enabled": true,
32
50
  "overwriteArtifactsOnStart": true,
@@ -90,6 +108,21 @@
90
108
  "command": "bun add --global critique@latest",
91
109
  },
92
110
  },
111
+ "storage": {
112
+ "lifecycle": {
113
+ "eventRetentionMs": 259200000,
114
+ "telemetryRetentionMs": 259200000,
115
+ "maintenanceIntervalMs": 5000,
116
+ "pruneBatchSize": 500,
117
+ "compactFreelistPages": 256,
118
+ "copyForwardBatchSize": 250,
119
+ "copyForwardFinalizeTailRows": 500,
120
+ "telemetryPayloadMaxBytes": 16384,
121
+ "textDeltaPayloadMaxBytes": 32768,
122
+ "textDeltaCoalesceWindowMs": 1200,
123
+ "busyTimeoutMs": 5000,
124
+ },
125
+ },
93
126
  "hooks": {
94
127
  "lifecycle": {
95
128
  "enabled": false,
@@ -1,9 +1,7 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
3
  import { resolveHarnessConfigDirectory } from './config-core.ts';
4
4
 
5
- export const HARNESS_SECRETS_FILE_PATH = '.harness/secrets.env';
6
-
7
5
  interface HarnessSecretEntry {
8
6
  readonly key: string;
9
7
  readonly value: string;
@@ -23,6 +21,20 @@ interface LoadedHarnessSecrets {
23
21
  readonly skippedKeys: readonly string[];
24
22
  }
25
23
 
24
+ interface UpsertHarnessSecretOptions {
25
+ readonly key: string;
26
+ readonly value: string;
27
+ readonly cwd?: string;
28
+ readonly filePath?: string;
29
+ readonly env?: NodeJS.ProcessEnv;
30
+ }
31
+
32
+ interface UpsertHarnessSecretResult {
33
+ readonly filePath: string;
34
+ readonly createdFile: boolean;
35
+ readonly replacedExisting: boolean;
36
+ }
37
+
26
38
  function isValidSecretKey(value: string): boolean {
27
39
  return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(value);
28
40
  }
@@ -107,6 +119,38 @@ function parseLineValue(rawValue: string, lineNumber: number): string {
107
119
  return rawValue.replace(/\s+#.*$/u, '').trim();
108
120
  }
109
121
 
122
+ function parseSecretLineKey(line: string): string | null {
123
+ const trimmed = line.trim();
124
+ if (trimmed.length === 0 || trimmed.startsWith('#')) {
125
+ return null;
126
+ }
127
+ const withoutExport = trimmed.startsWith('export ')
128
+ ? trimmed.slice('export '.length).trimStart()
129
+ : trimmed;
130
+ const equalIndex = withoutExport.indexOf('=');
131
+ if (equalIndex <= 0) {
132
+ return null;
133
+ }
134
+ const key = withoutExport.slice(0, equalIndex).trim();
135
+ return isValidSecretKey(key) ? key : null;
136
+ }
137
+
138
+ function encodeSecretValue(value: string): string {
139
+ if (value.length === 0) {
140
+ return '""';
141
+ }
142
+ if (/^[A-Za-z0-9._:@/+,-]+$/u.test(value)) {
143
+ return value;
144
+ }
145
+ const escaped = value
146
+ .replaceAll('\\', '\\\\')
147
+ .replaceAll('"', '\\"')
148
+ .replaceAll('\n', '\\n')
149
+ .replaceAll('\r', '\\r')
150
+ .replaceAll('\t', '\\t');
151
+ return `"${escaped}"`;
152
+ }
153
+
110
154
  function parseHarnessSecretLine(line: string, lineNumber: number): HarnessSecretEntry | null {
111
155
  const trimmed = line.trim();
112
156
  if (trimmed.length === 0 || trimmed.startsWith('#')) {
@@ -186,3 +230,47 @@ export function loadHarnessSecrets(options: LoadHarnessSecretsOptions = {}): Loa
186
230
  skippedKeys,
187
231
  };
188
232
  }
233
+
234
+ export function upsertHarnessSecret(
235
+ options: UpsertHarnessSecretOptions,
236
+ ): UpsertHarnessSecretResult {
237
+ const key = options.key.trim();
238
+ if (!isValidSecretKey(key)) {
239
+ throw new Error(`invalid secret key: ${options.key}`);
240
+ }
241
+ const cwd = options.cwd ?? process.cwd();
242
+ const env = options.env ?? process.env;
243
+ const filePath = resolveHarnessSecretsPath(cwd, options.filePath, env);
244
+ const hadFile = existsSync(filePath);
245
+ const existingText = hadFile ? readFileSync(filePath, 'utf8') : '';
246
+ const sourceLines = existingText.split(/\r?\n/u);
247
+ if (sourceLines[sourceLines.length - 1] === '') {
248
+ sourceLines.pop();
249
+ }
250
+ const nextLines: string[] = [];
251
+ const encoded = encodeSecretValue(options.value);
252
+ let replacedExisting = false;
253
+ for (const line of sourceLines) {
254
+ const lineKey = parseSecretLineKey(line);
255
+ if (lineKey !== key) {
256
+ nextLines.push(line);
257
+ continue;
258
+ }
259
+ if (!replacedExisting) {
260
+ nextLines.push(`${key}=${encoded}`);
261
+ replacedExisting = true;
262
+ }
263
+ }
264
+ if (!replacedExisting) {
265
+ nextLines.push(`${key}=${encoded}`);
266
+ }
267
+ mkdirSync(dirname(filePath), { recursive: true });
268
+ const tempPath = `${filePath}.tmp.${String(process.pid)}`;
269
+ writeFileSync(tempPath, `${nextLines.join('\n')}\n`, 'utf8');
270
+ renameSync(tempPath, filePath);
271
+ return {
272
+ filePath,
273
+ createdFile: !hadFile,
274
+ replacedExisting,
275
+ };
276
+ }