@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
@@ -20,6 +20,7 @@ import type {
20
20
  } from '../store/control-plane-store.ts';
21
21
  import type { TerminalBufferTail, TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
22
22
  import type { PtyExit } from '../pty/pty_host.ts';
23
+ import type { StartSessionRuntimeInput } from './stream-session-runtime-types.ts';
23
24
 
24
25
  const DEFAULT_TENANT_ID = 'tenant-local';
25
26
  const DEFAULT_USER_ID = 'user-local';
@@ -54,21 +55,6 @@ interface SessionControllerState extends StreamSessionController {
54
55
  connectionId: string;
55
56
  }
56
57
 
57
- interface StartSessionRuntimeInput {
58
- readonly sessionId: string;
59
- readonly args: readonly string[];
60
- readonly initialCols: number;
61
- readonly initialRows: number;
62
- readonly env?: Record<string, string>;
63
- readonly cwd?: string;
64
- readonly tenantId?: string;
65
- readonly userId?: string;
66
- readonly workspaceId?: string;
67
- readonly worktreeId?: string;
68
- readonly terminalForegroundHex?: string;
69
- readonly terminalBackgroundHex?: string;
70
- }
71
-
72
58
  interface LiveSessionLike {
73
59
  attach(
74
60
  handlers: {
@@ -206,9 +192,8 @@ interface ExecuteCommandContext {
206
192
  workspaceId: string;
207
193
  repositoryId?: string;
208
194
  projectId?: string;
209
- title: string;
210
- description?: string;
211
- linear?: Record<string, unknown>;
195
+ title?: string | null;
196
+ body: string;
212
197
  }): ControlPlaneTaskRecord;
213
198
  getTask(taskId: string): ControlPlaneTaskRecord | null;
214
199
  listTasks(query: {
@@ -224,11 +209,10 @@ interface ExecuteCommandContext {
224
209
  updateTask(
225
210
  taskId: string,
226
211
  input: {
227
- title?: string;
228
- description?: string;
212
+ title?: string | null;
213
+ body?: string;
229
214
  repositoryId?: string | null;
230
215
  projectId?: string | null;
231
- linear?: Record<string, unknown> | null;
232
216
  },
233
217
  ): ControlPlaneTaskRecord | null;
234
218
  deleteTask(taskId: string): void;
@@ -391,10 +375,46 @@ interface ExecuteCommandContext {
391
375
  baseBranch: string;
392
376
  state: 'open' | 'closed';
393
377
  isDraft: boolean;
378
+ mergedAt: string | null;
394
379
  updatedAt: string;
395
380
  createdAt: string;
396
381
  closedAt: string | null;
397
382
  } | null>;
383
+ findPullRequestForBranch(input: { owner: string; repo: string; headBranch: string }): Promise<{
384
+ number: number;
385
+ title: string;
386
+ url: string;
387
+ authorLogin: string | null;
388
+ headBranch: string;
389
+ headSha: string;
390
+ baseBranch: string;
391
+ state: 'open' | 'closed';
392
+ isDraft: boolean;
393
+ mergedAt: string | null;
394
+ updatedAt: string;
395
+ createdAt: string;
396
+ closedAt: string | null;
397
+ } | null>;
398
+ listPullRequestReviewThreads(input: {
399
+ owner: string;
400
+ repo: string;
401
+ pullNumber: number;
402
+ }): Promise<
403
+ readonly {
404
+ threadId: string;
405
+ isResolved: boolean;
406
+ isOutdated: boolean;
407
+ resolvedByLogin: string | null;
408
+ comments: readonly {
409
+ commentId: string;
410
+ authorLogin: string | null;
411
+ body: string;
412
+ url: string | null;
413
+ createdAt: string;
414
+ updatedAt: string;
415
+ }[];
416
+ }[]
417
+ >;
398
418
  createPullRequest(input: {
399
419
  owner: string;
400
420
  repo: string;
@@ -413,11 +433,127 @@ interface ExecuteCommandContext {
413
433
  baseBranch: string;
414
434
  state: 'open' | 'closed';
415
435
  isDraft: boolean;
436
+ mergedAt: string | null;
416
437
  updatedAt: string;
417
438
  createdAt: string;
418
439
  closedAt: string | null;
419
440
  }>;
420
441
  };
442
+ getGitHubProjectReviewCache(input: { repositoryId: string; branchName: string }): {
443
+ repositoryId: string;
444
+ branchName: string;
445
+ pr: {
446
+ number: number;
447
+ title: string;
448
+ url: string;
449
+ authorLogin: string | null;
450
+ headBranch: string;
451
+ headSha: string;
452
+ baseBranch: string;
453
+ state: 'draft' | 'open' | 'merged' | 'closed';
454
+ isDraft: boolean;
455
+ mergedAt: string | null;
456
+ closedAt: string | null;
457
+ updatedAt: string;
458
+ createdAt: string;
459
+ } | null;
460
+ openThreads: readonly {
461
+ threadId: string;
462
+ isResolved: boolean;
463
+ isOutdated: boolean;
464
+ resolvedByLogin: string | null;
465
+ comments: readonly {
466
+ commentId: string;
467
+ authorLogin: string | null;
468
+ body: string;
469
+ url: string | null;
470
+ createdAt: string;
471
+ updatedAt: string;
472
+ }[];
473
+ }[];
474
+ resolvedThreads: readonly {
475
+ threadId: string;
476
+ isResolved: boolean;
477
+ isOutdated: boolean;
478
+ resolvedByLogin: string | null;
479
+ comments: readonly {
480
+ commentId: string;
481
+ authorLogin: string | null;
482
+ body: string;
483
+ url: string | null;
484
+ createdAt: string;
485
+ updatedAt: string;
486
+ }[];
487
+ }[];
488
+ fetchedAtMs: number;
489
+ } | null;
490
+ refreshGitHubProjectReviewCache(input: {
491
+ repositoryId: string;
492
+ owner: string;
493
+ repo: string;
494
+ branchName: string;
495
+ forceRefresh?: boolean;
496
+ }): Promise<{
497
+ repositoryId: string;
498
+ branchName: string;
499
+ pr: {
500
+ number: number;
501
+ title: string;
502
+ url: string;
503
+ authorLogin: string | null;
504
+ headBranch: string;
505
+ headSha: string;
506
+ baseBranch: string;
507
+ state: 'draft' | 'open' | 'merged' | 'closed';
508
+ isDraft: boolean;
509
+ mergedAt: string | null;
510
+ closedAt: string | null;
511
+ updatedAt: string;
512
+ createdAt: string;
513
+ } | null;
514
+ openThreads: readonly {
515
+ threadId: string;
516
+ isResolved: boolean;
517
+ isOutdated: boolean;
518
+ resolvedByLogin: string | null;
519
+ comments: readonly {
520
+ commentId: string;
521
+ authorLogin: string | null;
522
+ body: string;
523
+ url: string | null;
524
+ createdAt: string;
525
+ updatedAt: string;
526
+ }[];
527
+ }[];
528
+ resolvedThreads: readonly {
529
+ threadId: string;
530
+ isResolved: boolean;
531
+ isOutdated: boolean;
532
+ resolvedByLogin: string | null;
533
+ comments: readonly {
534
+ commentId: string;
535
+ authorLogin: string | null;
536
+ body: string;
537
+ url: string | null;
538
+ createdAt: string;
539
+ updatedAt: string;
540
+ }[];
541
+ }[];
542
+ fetchedAtMs: number;
543
+ }>;
544
+ readonly linear: {
545
+ enabled: boolean;
546
+ };
547
+ readonly linearApi: {
548
+ issueByIdentifier(input: { identifier: string }): Promise<{
549
+ identifier: string;
550
+ title: string;
551
+ description: string | null;
552
+ url: string | null;
553
+ stateName: string | null;
554
+ teamKey: string | null;
555
+ } | null>;
556
+ };
421
557
  readonly streamCursor: number;
422
558
  refreshConversationTitle(conversationId: string): Promise<{
423
559
  conversation: ControlPlaneConversationRecord;
@@ -564,6 +700,40 @@ function parseGitHubOwnerRepo(remoteUrl: string): { owner: string; repo: string
564
700
  return null;
565
701
  }
566
702
 
703
+ function parseLinearIssueIdentifierFromUrl(issueUrl: string): {
704
+ identifier: string;
705
+ } | null {
706
+ const trimmed = issueUrl.trim();
707
+ if (trimmed.length === 0) {
708
+ return null;
709
+ }
710
+ let parsedUrl: URL;
711
+ try {
712
+ parsedUrl = new URL(trimmed);
713
+ } catch {
714
+ return null;
715
+ }
716
+ if (parsedUrl.protocol !== 'https:') {
717
+ return null;
718
+ }
719
+ const hostname = parsedUrl.hostname.toLowerCase();
720
+ if (!(hostname === 'linear.app' || hostname.endsWith('.linear.app'))) {
721
+ return null;
722
+ }
723
+ const segments = parsedUrl.pathname.split('/').filter((segment) => segment.length > 0);
724
+ const issueSegmentIndex = segments.findIndex((segment) => segment.toLowerCase() === 'issue');
725
+ if (issueSegmentIndex < 0 || issueSegmentIndex + 1 >= segments.length) {
726
+ return null;
727
+ }
728
+ const identifier = segments[issueSegmentIndex + 1]?.trim().toUpperCase() ?? '';
729
+ if (!/^[A-Z][A-Z0-9]*-\d+$/u.test(identifier)) {
730
+ return null;
731
+ }
732
+ return {
733
+ identifier,
734
+ };
735
+ }
736
+
567
737
  function resolveTrackedBranch(input: {
568
738
  strategy: 'pinned-then-current' | 'current-only' | 'pinned-only';
569
739
  pinnedBranch: string | null;
@@ -647,10 +817,29 @@ function ciRollupFromJobs(
647
817
  return 'neutral';
648
818
  }
649
819
 
820
+ function githubLifecycleStateFromPullRequest(input: {
821
+ state: 'open' | 'closed';
822
+ isDraft: boolean;
823
+ mergedAt: string | null;
824
+ }): 'draft' | 'open' | 'merged' | 'closed' {
825
+ if (input.state === 'open' && input.isDraft) {
826
+ return 'draft';
827
+ }
828
+ if (input.state === 'open') {
829
+ return 'open';
830
+ }
831
+ if (input.mergedAt !== null) {
832
+ return 'merged';
833
+ }
834
+ return 'closed';
835
+ }
836
+
650
837
  export const streamServerCommandTestInternals = {
651
838
  parseGitHubOwnerRepo,
839
+ parseLinearIssueIdentifierFromUrl,
652
840
  resolveTrackedBranch,
653
841
  ciRollupFromJobs,
842
+ githubLifecycleStateFromPullRequest,
654
843
  };
655
844
 
656
845
  export async function executeStreamServerCommand(
@@ -1161,6 +1350,99 @@ export async function executeStreamServerCommand(
1161
1350
  };
1162
1351
  }
1163
1352
 
1353
+ if (command.type === 'github.project-review') {
1354
+ const resolved = resolveProjectGitHubContext(command.directoryId);
1355
+ const storedPr =
1356
+ resolved.repository === null || resolved.trackedBranch === null
1357
+ ? null
1358
+ : (ctx.stateStore.listGitHubPullRequests({
1359
+ repositoryId: resolved.repository.repositoryId,
1360
+ headBranch: resolved.trackedBranch,
1361
+ limit: 1,
1362
+ })[0] ?? null);
1363
+ const storedPrView =
1364
+ storedPr === null
1365
+ ? null
1366
+ : {
1367
+ prRecordId: storedPr.prRecordId,
1368
+ number: storedPr.number,
1369
+ title: storedPr.title,
1370
+ url: storedPr.url,
1371
+ authorLogin: storedPr.authorLogin,
1372
+ headBranch: storedPr.headBranch,
1373
+ headSha: storedPr.headSha,
1374
+ baseBranch: storedPr.baseBranch,
1375
+ state: githubLifecycleStateFromPullRequest({
1376
+ state: storedPr.state,
1377
+ isDraft: storedPr.isDraft,
1378
+ mergedAt: null,
1379
+ }),
1380
+ isDraft: storedPr.isDraft,
1381
+ mergedAt: null,
1382
+ closedAt: storedPr.closedAt,
1383
+ ciRollup: storedPr.ciRollup,
1384
+ updatedAt: storedPr.updatedAt,
1385
+ createdAt: storedPr.createdAt,
1386
+ observedAt: storedPr.observedAt,
1387
+ };
1388
+ if (
1389
+ resolved.repository === null ||
1390
+ resolved.trackedBranch === null ||
1391
+ resolved.ownerRepo === null ||
1392
+ !ctx.github.enabled
1393
+ ) {
1394
+ return {
1395
+ directoryId: resolved.directory.directoryId,
1396
+ repositoryId: resolved.repository?.repositoryId ?? null,
1397
+ branchName: resolved.trackedBranch,
1398
+ branchSource: resolved.trackedBranchSource,
1399
+ pr: storedPrView,
1400
+ openThreads: [],
1401
+ resolvedThreads: [],
1402
+ };
1403
+ }
1404
+ const reviewCache =
1405
+ command.forceRefresh === true
1406
+ ? await ctx.refreshGitHubProjectReviewCache({
1407
+ repositoryId: resolved.repository.repositoryId,
1408
+ owner: resolved.ownerRepo.owner,
1409
+ repo: resolved.ownerRepo.repo,
1410
+ branchName: resolved.trackedBranch,
1411
+ forceRefresh: true,
1412
+ })
1413
+ : ctx.getGitHubProjectReviewCache({
1414
+ repositoryId: resolved.repository.repositoryId,
1415
+ branchName: resolved.trackedBranch,
1416
+ });
1417
+ if (reviewCache === null) {
1418
+ return {
1419
+ directoryId: resolved.directory.directoryId,
1420
+ repositoryId: resolved.repository.repositoryId,
1421
+ branchName: resolved.trackedBranch,
1422
+ branchSource: resolved.trackedBranchSource,
1423
+ pr: storedPrView,
1424
+ openThreads: [],
1425
+ resolvedThreads: [],
1426
+ };
1427
+ }
1428
+ const reviewPr =
1429
+ reviewCache.pr === null
1430
+ ? storedPrView
1431
+ : {
1432
+ ...reviewCache.pr,
1433
+ ciRollup: storedPrView?.ciRollup ?? null,
1434
+ };
1435
+ return {
1436
+ directoryId: resolved.directory.directoryId,
1437
+ repositoryId: resolved.repository.repositoryId,
1438
+ branchName: resolved.trackedBranch,
1439
+ branchSource: resolved.trackedBranchSource,
1440
+ pr: reviewPr,
1441
+ openThreads: reviewCache.openThreads,
1442
+ resolvedThreads: reviewCache.resolvedThreads,
1443
+ };
1444
+ }
1445
+
1164
1446
  if (command.type === 'github.pr-list') {
1165
1447
  const prs = ctx.stateStore.listGitHubPullRequests({
1166
1448
  ...(command.tenantId === undefined ? {} : { tenantId: command.tenantId }),
@@ -1301,6 +1583,66 @@ export async function executeStreamServerCommand(
1301
1583
  };
1302
1584
  }
1303
1585
 
1586
+ if (command.type === 'linear.issue.import') {
1587
+ if (!ctx.linear.enabled) {
1588
+ throw new Error('linear integration is disabled');
1589
+ }
1590
+ const parsedLinearIssue = parseLinearIssueIdentifierFromUrl(command.url);
1591
+ if (parsedLinearIssue === null) {
1592
+ throw new Error('linear issue url required');
1593
+ }
1594
+ const issue = await ctx.linearApi.issueByIdentifier({
1595
+ identifier: parsedLinearIssue.identifier,
1596
+ });
1597
+ if (issue === null) {
1598
+ throw new Error(`linear issue not found: ${parsedLinearIssue.identifier}`);
1599
+ }
1600
+ if (command.repositoryId === undefined && command.projectId === undefined) {
1601
+ throw new Error('linear issue import requires repositoryId or projectId');
1602
+ }
1603
+ const task = ctx.stateStore.createTask({
1604
+ taskId: `task-${randomUUID()}`,
1605
+ tenantId: command.tenantId ?? DEFAULT_TENANT_ID,
1606
+ userId: command.userId ?? DEFAULT_USER_ID,
1607
+ workspaceId: command.workspaceId ?? DEFAULT_WORKSPACE_ID,
1608
+ ...(command.repositoryId === undefined ? {} : { repositoryId: command.repositoryId }),
1609
+ ...(command.projectId === undefined ? {} : { projectId: command.projectId }),
1610
+ title: `${issue.identifier}: ${issue.title}`.trim(),
1611
+ body: [
1612
+ issue.url ?? command.url.trim(),
1613
+ issue.stateName === null ? null : `state: ${issue.stateName}`,
1614
+ issue.teamKey === null ? null : `team: ${issue.teamKey}`,
1615
+ issue.description,
1616
+ ]
1617
+ .filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
1618
+ .join('\n'),
1619
+ });
1620
+ ctx.publishObservedEvent(
1621
+ {
1622
+ tenantId: task.tenantId,
1623
+ userId: task.userId,
1624
+ workspaceId: task.workspaceId,
1625
+ directoryId: null,
1626
+ conversationId: null,
1627
+ },
1628
+ {
1629
+ type: 'task-created',
1630
+ task: ctx.taskRecord(task),
1631
+ },
1632
+ );
1633
+ return {
1634
+ imported: true,
1635
+ issue: {
1636
+ identifier: issue.identifier,
1637
+ title: issue.title,
1638
+ url: issue.url ?? command.url.trim(),
1639
+ stateName: issue.stateName,
1640
+ teamKey: issue.teamKey,
1641
+ },
1642
+ task: ctx.taskRecord(task),
1643
+ };
1644
+ }
1645
+
1304
1646
  if (command.type === 'conversation.create') {
1305
1647
  const conversation = ctx.stateStore.createConversation({
1306
1648
  conversationId: command.conversationId ?? `conversation-${randomUUID()}`,
@@ -1597,42 +1939,24 @@ export async function executeStreamServerCommand(
1597
1939
  workspaceId: string;
1598
1940
  repositoryId?: string;
1599
1941
  projectId?: string;
1600
- title: string;
1601
- description?: string;
1602
- linear?: {
1603
- issueId?: string | null;
1604
- identifier?: string | null;
1605
- url?: string | null;
1606
- teamId?: string | null;
1607
- projectId?: string | null;
1608
- projectMilestoneId?: string | null;
1609
- cycleId?: string | null;
1610
- stateId?: string | null;
1611
- assigneeId?: string | null;
1612
- priority?: number | null;
1613
- estimate?: number | null;
1614
- dueDate?: string | null;
1615
- labelIds?: readonly string[] | null;
1616
- };
1942
+ title?: string | null;
1943
+ body: string;
1617
1944
  } = {
1618
1945
  taskId: command.taskId ?? `task-${randomUUID()}`,
1619
1946
  tenantId: command.tenantId ?? DEFAULT_TENANT_ID,
1620
1947
  userId: command.userId ?? DEFAULT_USER_ID,
1621
1948
  workspaceId: command.workspaceId ?? DEFAULT_WORKSPACE_ID,
1622
- title: command.title,
1949
+ body: command.body,
1623
1950
  };
1951
+ if (command.title !== undefined) {
1952
+ input.title = command.title;
1953
+ }
1624
1954
  if (command.repositoryId !== undefined) {
1625
1955
  input.repositoryId = command.repositoryId;
1626
1956
  }
1627
1957
  if (command.projectId !== undefined) {
1628
1958
  input.projectId = command.projectId;
1629
1959
  }
1630
- if (command.description !== undefined) {
1631
- input.description = command.description;
1632
- }
1633
- if (command.linear !== undefined) {
1634
- input.linear = command.linear;
1635
- }
1636
1960
  const task = ctx.stateStore.createTask(input);
1637
1961
  ctx.publishObservedEvent(
1638
1962
  {
@@ -1705,31 +2029,16 @@ export async function executeStreamServerCommand(
1705
2029
 
1706
2030
  if (command.type === 'task.update') {
1707
2031
  const update: {
1708
- title?: string;
1709
- description?: string;
2032
+ title?: string | null;
2033
+ body?: string;
1710
2034
  repositoryId?: string | null;
1711
2035
  projectId?: string | null;
1712
- linear?: {
1713
- issueId?: string | null;
1714
- identifier?: string | null;
1715
- url?: string | null;
1716
- teamId?: string | null;
1717
- projectId?: string | null;
1718
- projectMilestoneId?: string | null;
1719
- cycleId?: string | null;
1720
- stateId?: string | null;
1721
- assigneeId?: string | null;
1722
- priority?: number | null;
1723
- estimate?: number | null;
1724
- dueDate?: string | null;
1725
- labelIds?: readonly string[] | null;
1726
- } | null;
1727
2036
  } = {};
1728
2037
  if (command.title !== undefined) {
1729
2038
  update.title = command.title;
1730
2039
  }
1731
- if (command.description !== undefined) {
1732
- update.description = command.description;
2040
+ if (command.body !== undefined) {
2041
+ update.body = command.body;
1733
2042
  }
1734
2043
  if (command.repositoryId !== undefined) {
1735
2044
  update.repositoryId = command.repositoryId;
@@ -1737,9 +2046,6 @@ export async function executeStreamServerCommand(
1737
2046
  if (command.projectId !== undefined) {
1738
2047
  update.projectId = command.projectId;
1739
2048
  }
1740
- if (command.linear !== undefined) {
1741
- update.linear = command.linear;
1742
- }
1743
2049
  const updated = ctx.stateStore.updateTask(command.taskId, update);
1744
2050
  if (updated === null) {
1745
2051
  throw new Error(`task not found: ${command.taskId}`);
@@ -2494,13 +2800,14 @@ export async function executeStreamServerCommand(
2494
2800
  const attachmentId = state.session.attach(
2495
2801
  {
2496
2802
  onData: (event) => {
2803
+ const chunkBase64 = event.chunk.toString('base64');
2497
2804
  ctx.sendToConnection(
2498
2805
  connection.id,
2499
2806
  {
2500
2807
  kind: 'pty.output',
2501
2808
  sessionId: command.sessionId,
2502
2809
  cursor: event.cursor,
2503
- chunkBase64: Buffer.from(event.chunk).toString('base64'),
2810
+ chunkBase64,
2504
2811
  },
2505
2812
  command.sessionId,
2506
2813
  );
@@ -2514,7 +2821,7 @@ export async function executeStreamServerCommand(
2514
2821
  type: 'session-output',
2515
2822
  sessionId: command.sessionId,
2516
2823
  outputCursor: event.cursor,
2517
- chunkBase64: Buffer.from(event.chunk).toString('base64'),
2824
+ chunkBase64,
2518
2825
  ts: new Date().toISOString(),
2519
2826
  directoryId: sessionState.directoryId,
2520
2827
  conversationId: sessionState.id,
@@ -530,7 +530,8 @@ export function handleSessionEvent(
530
530
 
531
531
  const mapped = mapSessionEvent(event);
532
532
  if (mapped !== null && event.type !== 'terminal-output') {
533
- const observedAt = mapped.type === 'session-exit' ? new Date().toISOString() : mapped.record.ts;
533
+ const nowIso = new Date().toISOString();
534
+ const observedAt = mapped.type === 'session-exit' ? nowIso : mapped.record.ts;
534
535
  for (const connectionId of sessionState.eventSubscriberConnectionIds) {
535
536
  ctx.sendToConnection(
536
537
  connectionId,
@@ -546,7 +547,7 @@ export function handleSessionEvent(
546
547
  type: 'session-event',
547
548
  sessionId,
548
549
  event: mapped,
549
- ts: new Date().toISOString(),
550
+ ts: nowIso,
550
551
  directoryId: sessionState.directoryId,
551
552
  conversationId: sessionState.id,
552
553
  });