@jmoyers/harness 0.1.11 → 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 (232) hide show
  1. package/README.md +31 -39
  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/packages/harness-ui/src/modal-manager.ts +222 -0
  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 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  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 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -1,10 +1,12 @@
1
1
  import type { PtyExit } from '../pty/pty_host.ts';
2
- import type {
3
- StreamSessionController,
4
- StreamSessionControllerType,
5
- StreamSessionRuntimeStatus,
6
- StreamSessionStatusModel,
7
- StreamTelemetrySummary,
2
+ import {
3
+ isStreamSessionRuntimeStatus,
4
+ parseStreamSessionStatusModel,
5
+ type StreamSessionController,
6
+ type StreamSessionControllerType,
7
+ type StreamSessionRuntimeStatus,
8
+ type StreamSessionStatusModel,
9
+ type StreamTelemetrySummary,
8
10
  } from './stream-protocol.ts';
9
11
 
10
12
  interface StreamSessionSummary {
@@ -187,79 +189,6 @@ function readSessionController(value: unknown): StreamSessionController | null |
187
189
  };
188
190
  }
189
191
 
190
- function readSessionStatusModel(value: unknown): StreamSessionStatusModel | null | undefined {
191
- if (value === undefined) {
192
- return undefined;
193
- }
194
- if (value === null) {
195
- return null;
196
- }
197
- const record = asRecord(value);
198
- if (record === null) {
199
- return undefined;
200
- }
201
- const runtimeStatus = readString(record['runtimeStatus']);
202
- const phase = readString(record['phase']);
203
- const glyph = readString(record['glyph']);
204
- const badge = readString(record['badge']);
205
- const detailText = readString(record['detailText']);
206
- const attentionReason = readNullableString(record['attentionReason']);
207
- const lastKnownWork = readNullableString(record['lastKnownWork']);
208
- const lastKnownWorkAt = readNullableString(record['lastKnownWorkAt']);
209
- const phaseHintRaw = readNullableString(record['phaseHint']);
210
- const observedAt = readString(record['observedAt']);
211
- if (
212
- runtimeStatus === null ||
213
- !isRuntimeStatus(runtimeStatus) ||
214
- phase === null ||
215
- (phase !== 'needs-action' &&
216
- phase !== 'starting' &&
217
- phase !== 'working' &&
218
- phase !== 'idle' &&
219
- phase !== 'exited') ||
220
- glyph === null ||
221
- (glyph !== '▲' && glyph !== '◔' && glyph !== '◆' && glyph !== '○' && glyph !== '■') ||
222
- badge === null ||
223
- (badge !== 'NEED' && badge !== 'RUN ' && badge !== 'DONE' && badge !== 'EXIT') ||
224
- detailText === null ||
225
- attentionReason === undefined ||
226
- lastKnownWork === undefined ||
227
- lastKnownWorkAt === undefined ||
228
- phaseHintRaw === undefined ||
229
- observedAt === null
230
- ) {
231
- return undefined;
232
- }
233
- const phaseHint =
234
- phaseHintRaw === null ||
235
- phaseHintRaw === 'needs-action' ||
236
- phaseHintRaw === 'working' ||
237
- phaseHintRaw === 'idle'
238
- ? phaseHintRaw
239
- : undefined;
240
- if (phaseHint === undefined) {
241
- return undefined;
242
- }
243
- return {
244
- runtimeStatus,
245
- phase,
246
- glyph,
247
- badge,
248
- detailText,
249
- attentionReason,
250
- lastKnownWork,
251
- lastKnownWorkAt,
252
- phaseHint,
253
- observedAt,
254
- };
255
- }
256
-
257
- function isRuntimeStatus(value: string): value is StreamSessionRuntimeStatus {
258
- return (
259
- value === 'running' || value === 'needs-input' || value === 'completed' || value === 'exited'
260
- );
261
- }
262
-
263
192
  export function parseSessionSummaryRecord(value: unknown): StreamSessionSummary | null {
264
193
  const record = asRecord(value);
265
194
  if (record === null) {
@@ -280,12 +209,12 @@ export function parseSessionSummaryRecord(value: unknown): StreamSessionSummary
280
209
  workspaceId === null ||
281
210
  worktreeId === null ||
282
211
  status === null ||
283
- !isRuntimeStatus(status)
212
+ !isStreamSessionRuntimeStatus(status)
284
213
  ) {
285
214
  return null;
286
215
  }
287
216
  const attentionReason = readNullableString(record['attentionReason']);
288
- const statusModel = readSessionStatusModel(record['statusModel']);
217
+ const statusModel = parseStreamSessionStatusModel(record['statusModel']);
289
218
  const latestCursor = readNullableNumber(record['latestCursor']);
290
219
  const processId = readNullableNumber(record['processId']);
291
220
  const attachedClients = readNumber(record['attachedClients']);
@@ -8,7 +8,7 @@ import type { AgentStatusProjectionInput, AgentStatusReducer } from './agent-sta
8
8
 
9
9
  interface WorkProjection {
10
10
  readonly text: string | null;
11
- readonly phaseHint: 'needs-action' | 'working' | 'idle' | null;
11
+ readonly activityHint: 'needs-action' | 'working' | 'idle' | null;
12
12
  }
13
13
 
14
14
  function normalizeText(value: string | null): string | null {
@@ -40,7 +40,7 @@ function eventIsNewer(observedAt: string, previousObservedAt: string | null): bo
40
40
 
41
41
  function phaseFromRuntimeStatus(
42
42
  runtimeStatus: StreamSessionRuntimeStatus,
43
- phaseHint: WorkProjection['phaseHint'],
43
+ activityHint: WorkProjection['activityHint'],
44
44
  ): StreamSessionDisplayPhase {
45
45
  if (runtimeStatus === 'needs-input') {
46
46
  return 'needs-action';
@@ -48,13 +48,13 @@ function phaseFromRuntimeStatus(
48
48
  if (runtimeStatus === 'exited') {
49
49
  return 'exited';
50
50
  }
51
- if (phaseHint === 'working') {
51
+ if (activityHint === 'working') {
52
52
  return 'working';
53
53
  }
54
- if (phaseHint === 'needs-action') {
54
+ if (activityHint === 'needs-action') {
55
55
  return 'needs-action';
56
56
  }
57
- if (phaseHint === 'idle') {
57
+ if (activityHint === 'idle') {
58
58
  return 'idle';
59
59
  }
60
60
  if (runtimeStatus === 'running') {
@@ -118,14 +118,14 @@ export abstract class BaseAgentStatusReducer implements AgentStatusReducer {
118
118
  project(input: AgentStatusProjectionInput): StreamSessionStatusModel | null {
119
119
  const previous = input.previous;
120
120
  let workText = previous?.lastKnownWork ?? null;
121
- let workPhaseHint = previous?.phaseHint ?? null;
121
+ let workActivityHint = previous?.activityHint ?? null;
122
122
  let workObservedAt = previous?.lastKnownWorkAt ?? null;
123
123
 
124
124
  if (input.telemetry !== null && eventIsNewer(input.telemetry.observedAt, workObservedAt)) {
125
125
  const projected = this.projectFromTelemetry(input.telemetry);
126
126
  if (projected !== null) {
127
127
  workText = projected.text;
128
- workPhaseHint = projected.phaseHint;
128
+ workActivityHint = projected.activityHint;
129
129
  workObservedAt = input.telemetry.observedAt;
130
130
  }
131
131
  }
@@ -133,19 +133,19 @@ export abstract class BaseAgentStatusReducer implements AgentStatusReducer {
133
133
  if (
134
134
  input.runtimeStatus === 'completed' &&
135
135
  eventIsNewer(input.observedAt, workObservedAt) &&
136
- workPhaseHint !== 'needs-action'
136
+ workActivityHint !== 'needs-action'
137
137
  ) {
138
138
  workText = 'inactive';
139
- workPhaseHint = 'idle';
139
+ workActivityHint = 'idle';
140
140
  workObservedAt = input.observedAt;
141
141
  }
142
142
  if (input.runtimeStatus === 'exited' && eventIsNewer(input.observedAt, workObservedAt)) {
143
143
  workText = 'exited';
144
- workPhaseHint = 'idle';
144
+ workActivityHint = 'idle';
145
145
  workObservedAt = input.observedAt;
146
146
  }
147
147
 
148
- const phase = phaseFromRuntimeStatus(input.runtimeStatus, workPhaseHint);
148
+ const phase = phaseFromRuntimeStatus(input.runtimeStatus, workActivityHint);
149
149
  const normalizedAttentionReason = normalizeText(input.attentionReason);
150
150
  const detailText =
151
151
  (input.runtimeStatus === 'needs-input' ? normalizedAttentionReason : null) ??
@@ -162,7 +162,7 @@ export abstract class BaseAgentStatusReducer implements AgentStatusReducer {
162
162
  attentionReason: normalizedAttentionReason,
163
163
  lastKnownWork: workText,
164
164
  lastKnownWorkAt: workObservedAt,
165
- phaseHint: workPhaseHint,
165
+ activityHint: workActivityHint,
166
166
  observedAt: input.observedAt,
167
167
  };
168
168
  }
@@ -14,12 +14,12 @@ export class ClaudeStatusReducer extends BaseAgentStatusReducer {
14
14
 
15
15
  protected override projectFromTelemetry(
16
16
  telemetry: StreamTelemetrySummary,
17
- ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
17
+ ): { text: string | null; activityHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
18
  const eventName = normalize(telemetry.eventName);
19
19
  if (eventName === 'claude.userpromptsubmit' || eventName === 'claude.pretooluse') {
20
20
  return {
21
21
  text: 'active',
22
- phaseHint: 'working',
22
+ activityHint: 'working',
23
23
  };
24
24
  }
25
25
  if (
@@ -29,7 +29,7 @@ export class ClaudeStatusReducer extends BaseAgentStatusReducer {
29
29
  ) {
30
30
  return {
31
31
  text: 'inactive',
32
- phaseHint: 'idle',
32
+ activityHint: 'idle',
33
33
  };
34
34
  }
35
35
  return null;
@@ -14,19 +14,19 @@ export class CodexStatusReducer extends BaseAgentStatusReducer {
14
14
 
15
15
  protected override projectFromTelemetry(
16
16
  telemetry: StreamTelemetrySummary,
17
- ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
17
+ ): { text: string | null; activityHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
18
  const eventName = normalize(telemetry.eventName);
19
19
  const summary = normalize(telemetry.summary);
20
20
  if (eventName === 'codex.user_prompt') {
21
21
  return {
22
22
  text: 'active',
23
- phaseHint: 'working',
23
+ activityHint: 'working',
24
24
  };
25
25
  }
26
26
  if (eventName === 'codex.turn.e2e_duration_ms') {
27
27
  return {
28
28
  text: 'inactive',
29
- phaseHint: 'idle',
29
+ activityHint: 'idle',
30
30
  };
31
31
  }
32
32
  if (eventName === 'codex.sse_event') {
@@ -39,7 +39,7 @@ export class CodexStatusReducer extends BaseAgentStatusReducer {
39
39
  ) {
40
40
  return {
41
41
  text: 'active',
42
- phaseHint: 'working',
42
+ activityHint: 'working',
43
43
  };
44
44
  }
45
45
  }
@@ -14,7 +14,7 @@ export class CursorStatusReducer extends BaseAgentStatusReducer {
14
14
 
15
15
  protected override projectFromTelemetry(
16
16
  telemetry: StreamTelemetrySummary,
17
- ): { text: string | null; phaseHint: 'needs-action' | 'working' | 'idle' | null } | null {
17
+ ): { text: string | null; activityHint: 'needs-action' | 'working' | 'idle' | null } | null {
18
18
  const eventName = normalize(telemetry.eventName);
19
19
  if (
20
20
  eventName === 'cursor.beforesubmitprompt' ||
@@ -23,13 +23,13 @@ export class CursorStatusReducer extends BaseAgentStatusReducer {
23
23
  ) {
24
24
  return {
25
25
  text: 'active',
26
- phaseHint: 'working',
26
+ activityHint: 'working',
27
27
  };
28
28
  }
29
29
  if (eventName === 'cursor.stop' || eventName === 'cursor.sessionend') {
30
30
  return {
31
31
  text: 'inactive',
32
- phaseHint: 'idle',
32
+ activityHint: 'idle',
33
33
  };
34
34
  }
35
35
  return null;
@@ -26,7 +26,17 @@ interface PendingCommand {
26
26
  reject: (error: Error) => void;
27
27
  }
28
28
 
29
- export class ControlPlaneStreamClient {
29
+ export interface ControlPlaneStreamClient {
30
+ onEnvelope(listener: (envelope: StreamServerEnvelope) => void): () => void;
31
+ sendCommand(command: StreamCommand): Promise<Record<string, unknown>>;
32
+ authenticate(token: string): Promise<void>;
33
+ sendInput(sessionId: string, data: Buffer): void;
34
+ sendResize(sessionId: string, cols: number, rows: number): void;
35
+ sendSignal(sessionId: string, signal: StreamSignal): void;
36
+ close(): void;
37
+ }
38
+
39
+ class ControlPlaneStreamClientImpl implements ControlPlaneStreamClient {
30
40
  private readonly socket: Socket;
31
41
  private readonly listeners = new Set<(envelope: StreamServerEnvelope) => void>();
32
42
  private readonly pending = new Map<string, PendingCommand>();
@@ -380,7 +390,7 @@ export async function connectControlPlaneStreamClient(
380
390
  }
381
391
  }
382
392
 
383
- const client = new ControlPlaneStreamClient(socket);
393
+ const client = new ControlPlaneStreamClientImpl(socket);
384
394
  if (typeof options.authToken === 'string') {
385
395
  await client.authenticate(options.authToken);
386
396
  }
@@ -3,7 +3,6 @@ import type { StreamCommand } from './stream-protocol.ts';
3
3
  type StreamCommandType = StreamCommand['type'];
4
4
  type CommandRecord = Record<string, unknown>;
5
5
  type CommandParser = (record: CommandRecord) => StreamCommand | null;
6
- type ParsedTaskLinearInput = NonNullable<Extract<StreamCommand, { type: 'task.create' }>['linear']>;
7
6
  const INVALID_OPTIONAL = Symbol('invalid-optional');
8
7
 
9
8
  function asRecord(value: unknown): CommandRecord | null {
@@ -95,115 +94,6 @@ function readOptionalNullableString(
95
94
  return value;
96
95
  }
97
96
 
98
- function readOptionalNullableNonNegativeInteger(
99
- record: CommandRecord,
100
- field: string,
101
- ): number | null | undefined | typeof INVALID_OPTIONAL {
102
- const value = record[field];
103
- if (value === undefined) {
104
- return undefined;
105
- }
106
- if (value === null) {
107
- return null;
108
- }
109
- if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
110
- return INVALID_OPTIONAL;
111
- }
112
- return value;
113
- }
114
-
115
- function parseTaskLinearInput(value: unknown): ParsedTaskLinearInput | null {
116
- const record = asRecord(value);
117
- if (record === null) {
118
- return null;
119
- }
120
- const issueId = readOptionalNullableString(record, 'issueId');
121
- const identifier = readOptionalNullableString(record, 'identifier');
122
- const url = readOptionalNullableString(record, 'url');
123
- const teamId = readOptionalNullableString(record, 'teamId');
124
- const projectId = readOptionalNullableString(record, 'projectId');
125
- const projectMilestoneId = readOptionalNullableString(record, 'projectMilestoneId');
126
- const cycleId = readOptionalNullableString(record, 'cycleId');
127
- const stateId = readOptionalNullableString(record, 'stateId');
128
- const assigneeId = readOptionalNullableString(record, 'assigneeId');
129
- const dueDate = readOptionalNullableString(record, 'dueDate');
130
- const priority = readOptionalNullableNonNegativeInteger(record, 'priority');
131
- const estimate = readOptionalNullableNonNegativeInteger(record, 'estimate');
132
- const labelsRaw = record['labelIds'];
133
- let labelIds: string[] | null | undefined;
134
- if (labelsRaw !== undefined) {
135
- if (labelsRaw === null) {
136
- labelIds = null;
137
- } else if (Array.isArray(labelsRaw) && labelsRaw.every((entry) => typeof entry === 'string')) {
138
- labelIds = [...labelsRaw];
139
- } else {
140
- return null;
141
- }
142
- }
143
-
144
- if (
145
- issueId === INVALID_OPTIONAL ||
146
- identifier === INVALID_OPTIONAL ||
147
- url === INVALID_OPTIONAL ||
148
- teamId === INVALID_OPTIONAL ||
149
- projectId === INVALID_OPTIONAL ||
150
- projectMilestoneId === INVALID_OPTIONAL ||
151
- cycleId === INVALID_OPTIONAL ||
152
- stateId === INVALID_OPTIONAL ||
153
- assigneeId === INVALID_OPTIONAL ||
154
- dueDate === INVALID_OPTIONAL ||
155
- priority === INVALID_OPTIONAL ||
156
- estimate === INVALID_OPTIONAL
157
- ) {
158
- return null;
159
- }
160
- if (priority !== undefined && priority !== null && priority > 4) {
161
- return null;
162
- }
163
-
164
- const out: ParsedTaskLinearInput = {};
165
- if (issueId !== undefined) {
166
- out.issueId = issueId;
167
- }
168
- if (identifier !== undefined) {
169
- out.identifier = identifier;
170
- }
171
- if (url !== undefined) {
172
- out.url = url;
173
- }
174
- if (teamId !== undefined) {
175
- out.teamId = teamId;
176
- }
177
- if (projectId !== undefined) {
178
- out.projectId = projectId;
179
- }
180
- if (projectMilestoneId !== undefined) {
181
- out.projectMilestoneId = projectMilestoneId;
182
- }
183
- if (cycleId !== undefined) {
184
- out.cycleId = cycleId;
185
- }
186
- if (stateId !== undefined) {
187
- out.stateId = stateId;
188
- }
189
- if (assigneeId !== undefined) {
190
- out.assigneeId = assigneeId;
191
- }
192
- if (priority !== undefined) {
193
- out.priority = priority as 0 | 1 | 2 | 3 | 4 | null;
194
- }
195
- if (estimate !== undefined) {
196
- out.estimate = estimate;
197
- }
198
- if (dueDate !== undefined) {
199
- out.dueDate = dueDate;
200
- }
201
- if (labelIds !== undefined) {
202
- out.labelIds = labelIds;
203
- }
204
- return out;
205
- }
206
-
207
97
  function parseSessionControllerType(value: unknown): 'human' | 'agent' | 'automation' | null {
208
98
  if (value === 'human' || value === 'agent' || value === 'automation') {
209
99
  return value;
@@ -586,32 +476,38 @@ function parseRepositoryArchive(record: CommandRecord): StreamCommand | null {
586
476
  }
587
477
 
588
478
  function parseTaskCreate(record: CommandRecord): StreamCommand | null {
589
- const title = readString(record['title']);
590
- if (title === null) {
479
+ const body = readString(record['body']);
480
+ if (body === null) {
591
481
  return null;
592
482
  }
483
+ const title = readOptionalNullableString(record, 'title');
593
484
  const taskId = readOptionalString(record, 'taskId');
594
485
  const tenantId = readOptionalString(record, 'tenantId');
595
486
  const userId = readOptionalString(record, 'userId');
596
487
  const workspaceId = readOptionalString(record, 'workspaceId');
597
488
  const repositoryId = readOptionalString(record, 'repositoryId');
598
489
  const projectId = readOptionalString(record, 'projectId');
599
- const description = readOptionalString(record, 'description');
600
490
  if (
491
+ title === INVALID_OPTIONAL ||
601
492
  taskId === undefined ||
602
493
  tenantId === undefined ||
603
494
  userId === undefined ||
604
495
  workspaceId === undefined ||
605
496
  repositoryId === undefined ||
606
- projectId === undefined ||
607
- description === undefined
497
+ projectId === undefined
608
498
  ) {
609
499
  return null;
610
500
  }
501
+ if (repositoryId === null && projectId === null) {
502
+ return null;
503
+ }
611
504
  const command: StreamCommand = {
612
505
  type: 'task.create',
613
- title,
506
+ body,
614
507
  };
508
+ if (title !== undefined) {
509
+ command.title = title;
510
+ }
615
511
  if (taskId !== null) {
616
512
  command.taskId = taskId;
617
513
  }
@@ -630,16 +526,6 @@ function parseTaskCreate(record: CommandRecord): StreamCommand | null {
630
526
  if (projectId !== null) {
631
527
  command.projectId = projectId;
632
528
  }
633
- if (description !== null) {
634
- command.description = description;
635
- }
636
- if (record['linear'] !== undefined) {
637
- const linear = parseTaskLinearInput(record['linear']);
638
- if (linear === null) {
639
- return null;
640
- }
641
- command.linear = linear;
642
- }
643
529
  return command;
644
530
  }
645
531
 
@@ -728,9 +614,9 @@ function parseTaskUpdate(record: CommandRecord): StreamCommand | null {
728
614
  if (taskId === null) {
729
615
  return null;
730
616
  }
731
- const title = readOptionalString(record, 'title');
732
- const description = readOptionalString(record, 'description');
733
- if (title === undefined || description === undefined) {
617
+ const title = readOptionalNullableString(record, 'title');
618
+ const body = readOptionalString(record, 'body');
619
+ if (title === INVALID_OPTIONAL || body === undefined) {
734
620
  return null;
735
621
  }
736
622
  let repositoryId: string | null | undefined;
@@ -753,16 +639,19 @@ function parseTaskUpdate(record: CommandRecord): StreamCommand | null {
753
639
  } else {
754
640
  return null;
755
641
  }
642
+ if (repositoryId === null && projectId === null) {
643
+ return null;
644
+ }
756
645
 
757
646
  const command: StreamCommand = {
758
647
  type: 'task.update',
759
648
  taskId,
760
649
  };
761
- if (title !== null) {
650
+ if (title !== undefined) {
762
651
  command.title = title;
763
652
  }
764
- if (description !== null) {
765
- command.description = description;
653
+ if (body !== null) {
654
+ command.body = body;
766
655
  }
767
656
  if (repositoryId !== undefined) {
768
657
  command.repositoryId = repositoryId;
@@ -770,17 +659,6 @@ function parseTaskUpdate(record: CommandRecord): StreamCommand | null {
770
659
  if (projectId !== undefined) {
771
660
  command.projectId = projectId;
772
661
  }
773
- if (record['linear'] !== undefined) {
774
- if (record['linear'] === null) {
775
- command.linear = null;
776
- } else {
777
- const linear = parseTaskLinearInput(record['linear']);
778
- if (linear === null) {
779
- return null;
780
- }
781
- command.linear = linear;
782
- }
783
- }
784
662
  return command;
785
663
  }
786
664
 
@@ -1109,6 +987,25 @@ function parseGitHubProjectPr(record: CommandRecord): StreamCommand | null {
1109
987
  };
1110
988
  }
1111
989
 
990
+ function parseGitHubProjectReview(record: CommandRecord): StreamCommand | null {
991
+ const directoryId = readString(record['directoryId']);
992
+ const forceRefresh = readOptionalBoolean(record, 'forceRefresh');
993
+ if (directoryId === null) {
994
+ return null;
995
+ }
996
+ if (forceRefresh === undefined) {
997
+ return null;
998
+ }
999
+ const command: StreamCommand = {
1000
+ type: 'github.project-review',
1001
+ directoryId,
1002
+ };
1003
+ if (forceRefresh !== null) {
1004
+ command.forceRefresh = forceRefresh;
1005
+ }
1006
+ return command;
1007
+ }
1008
+
1112
1009
  function parseGitHubPrList(record: CommandRecord): StreamCommand | null {
1113
1010
  const tenantId = readOptionalString(record, 'tenantId');
1114
1011
  const userId = readOptionalString(record, 'userId');
@@ -1256,6 +1153,47 @@ function parseGitHubRepoMyPrsUrl(record: CommandRecord): StreamCommand | null {
1256
1153
  };
1257
1154
  }
1258
1155
 
1156
+ function parseLinearIssueImport(record: CommandRecord): StreamCommand | null {
1157
+ const url = readString(record['url']);
1158
+ if (url === null) {
1159
+ return null;
1160
+ }
1161
+ const tenantId = readOptionalString(record, 'tenantId');
1162
+ const userId = readOptionalString(record, 'userId');
1163
+ const workspaceId = readOptionalString(record, 'workspaceId');
1164
+ const repositoryId = readOptionalString(record, 'repositoryId');
1165
+ const projectId = readOptionalString(record, 'projectId');
1166
+ if (
1167
+ tenantId === undefined ||
1168
+ userId === undefined ||
1169
+ workspaceId === undefined ||
1170
+ repositoryId === undefined ||
1171
+ projectId === undefined
1172
+ ) {
1173
+ return null;
1174
+ }
1175
+ const command: StreamCommand = {
1176
+ type: 'linear.issue.import',
1177
+ url,
1178
+ };
1179
+ if (tenantId !== null) {
1180
+ command.tenantId = tenantId;
1181
+ }
1182
+ if (userId !== null) {
1183
+ command.userId = userId;
1184
+ }
1185
+ if (workspaceId !== null) {
1186
+ command.workspaceId = workspaceId;
1187
+ }
1188
+ if (repositoryId !== null) {
1189
+ command.repositoryId = repositoryId;
1190
+ }
1191
+ if (projectId !== null) {
1192
+ command.projectId = projectId;
1193
+ }
1194
+ return command;
1195
+ }
1196
+
1259
1197
  function parseStreamSubscribe(record: CommandRecord): StreamCommand | null {
1260
1198
  const tenantId = readOptionalString(record, 'tenantId');
1261
1199
  const userId = readOptionalString(record, 'userId');
@@ -1664,10 +1602,12 @@ export const DEFAULT_STREAM_COMMAND_PARSERS: StreamCommandParserRegistry = {
1664
1602
  'automation.policy-get': parseAutomationPolicyGet,
1665
1603
  'automation.policy-set': parseAutomationPolicySet,
1666
1604
  'github.project-pr': parseGitHubProjectPr,
1605
+ 'github.project-review': parseGitHubProjectReview,
1667
1606
  'github.pr-list': parseGitHubPrList,
1668
1607
  'github.pr-create': parseGitHubPrCreate,
1669
1608
  'github.pr-jobs-list': parseGitHubPrJobsList,
1670
1609
  'github.repo-my-prs-url': parseGitHubRepoMyPrsUrl,
1610
+ 'linear.issue.import': parseLinearIssueImport,
1671
1611
  'stream.subscribe': parseStreamSubscribe,
1672
1612
  'stream.unsubscribe': parseStreamUnsubscribe,
1673
1613
  'session.list': parseSessionList,