@jmoyers/harness 0.1.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,760 @@
1
+ import type {
2
+ StreamSessionRuntimeStatus,
3
+ StreamSessionStatusModel,
4
+ } from '../control-plane/stream-protocol.ts';
5
+ import type { CodexTelemetrySource } from '../control-plane/codex-telemetry.ts';
6
+ import type {
7
+ ControlPlaneAutomationPolicyRecord,
8
+ ControlPlaneAutomationPolicyScope,
9
+ ControlPlaneConversationRecord,
10
+ ControlPlaneDirectoryRecord,
11
+ ControlPlaneGitHubCiRollup,
12
+ ControlPlaneGitHubPrJobRecord,
13
+ ControlPlaneGitHubPrState,
14
+ ControlPlaneGitHubPullRequestRecord,
15
+ ControlPlaneGitHubSyncStateRecord,
16
+ ControlPlaneProjectSettingsRecord,
17
+ ControlPlaneProjectTaskFocusMode,
18
+ ControlPlaneProjectThreadSpawnMode,
19
+ ControlPlaneRepositoryRecord,
20
+ ControlPlaneTaskLinearPriority,
21
+ ControlPlaneTaskLinearRecord,
22
+ ControlPlaneTaskRecord,
23
+ ControlPlaneTaskScopeKind,
24
+ ControlPlaneTaskStatus,
25
+ ControlPlaneTelemetryRecord,
26
+ TaskLinearInput,
27
+ } from './control-plane-store-types.ts';
28
+
29
+ const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
30
+
31
+ export function asRecord(value: unknown): Record<string, unknown> {
32
+ if (typeof value !== 'object' || value === null) {
33
+ throw new Error('expected object row');
34
+ }
35
+ return value as Record<string, unknown>;
36
+ }
37
+
38
+ export function asString(value: unknown, field: string): string {
39
+ if (typeof value !== 'string') {
40
+ throw new Error(`expected string for ${field}`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ export function asStringOrNull(value: unknown, field: string): string | null {
46
+ if (value === null) {
47
+ return null;
48
+ }
49
+ return asString(value, field);
50
+ }
51
+
52
+ export function asNumberOrNull(value: unknown, field: string): number | null {
53
+ if (value === null) {
54
+ return null;
55
+ }
56
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
57
+ throw new Error(`expected finite number for ${field}`);
58
+ }
59
+ return value;
60
+ }
61
+
62
+ function asBooleanFromInt(value: unknown, field: string): boolean {
63
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
64
+ throw new Error(`expected integer flag for ${field}`);
65
+ }
66
+ if (value === 0) {
67
+ return false;
68
+ }
69
+ if (value === 1) {
70
+ return true;
71
+ }
72
+ throw new Error(`unexpected flag value for ${field}`);
73
+ }
74
+
75
+ export function sqliteStatementChanges(value: unknown): number {
76
+ if (typeof value !== 'object' || value === null) {
77
+ return 0;
78
+ }
79
+ const candidate = value as Record<string, unknown>;
80
+ return typeof candidate.changes === 'number' ? candidate.changes : 0;
81
+ }
82
+
83
+ function normalizeAdapterState(value: unknown): Record<string, unknown> {
84
+ if (typeof value !== 'string') {
85
+ throw new Error('expected string for adapter_state_json');
86
+ }
87
+ try {
88
+ const parsed = JSON.parse(value) as unknown;
89
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
90
+ return {};
91
+ }
92
+ return parsed as Record<string, unknown>;
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+
98
+ function normalizeRuntimeStatus(value: unknown): StreamSessionRuntimeStatus {
99
+ const status = asString(value, 'runtime_status');
100
+ if (
101
+ status === 'running' ||
102
+ status === 'needs-input' ||
103
+ status === 'completed' ||
104
+ status === 'exited'
105
+ ) {
106
+ return status;
107
+ }
108
+ throw new Error('expected runtime_status enum value');
109
+ }
110
+
111
+ function normalizeRuntimeStatusModel(value: unknown): StreamSessionStatusModel | null {
112
+ if (value === null) {
113
+ return null;
114
+ }
115
+ if (typeof value !== 'string') {
116
+ throw new Error('expected string for runtime_status_model_json');
117
+ }
118
+ const parsed = JSON.parse(value) as unknown;
119
+ if (parsed === null) {
120
+ return null;
121
+ }
122
+ const model = asRecord(parsed);
123
+ const runtimeStatusRaw = asString(model.runtimeStatus, 'runtimeStatus');
124
+ if (
125
+ runtimeStatusRaw !== 'running' &&
126
+ runtimeStatusRaw !== 'needs-input' &&
127
+ runtimeStatusRaw !== 'completed' &&
128
+ runtimeStatusRaw !== 'exited'
129
+ ) {
130
+ throw new Error('expected runtimeStatus enum value');
131
+ }
132
+ const phaseRaw = asString(model.phase, 'phase');
133
+ if (
134
+ phaseRaw !== 'needs-action' &&
135
+ phaseRaw !== 'starting' &&
136
+ phaseRaw !== 'working' &&
137
+ phaseRaw !== 'idle' &&
138
+ phaseRaw !== 'exited'
139
+ ) {
140
+ throw new Error('expected phase enum value');
141
+ }
142
+ const glyph = asString(model.glyph, 'glyph') as StreamSessionStatusModel['glyph'];
143
+ if (glyph !== '▲' && glyph !== '◔' && glyph !== '◆' && glyph !== '○' && glyph !== '■') {
144
+ throw new Error('expected glyph enum value');
145
+ }
146
+ const badge = asString(model.badge, 'badge') as StreamSessionStatusModel['badge'];
147
+ if (badge !== 'NEED' && badge !== 'RUN ' && badge !== 'DONE' && badge !== 'EXIT') {
148
+ throw new Error('expected badge enum value');
149
+ }
150
+ const detailText = asString(model.detailText, 'detailText');
151
+ const attentionReason = asStringOrNull(model.attentionReason, 'attentionReason');
152
+ const lastKnownWork = asStringOrNull(model.lastKnownWork, 'lastKnownWork');
153
+ const lastKnownWorkAt = asStringOrNull(model.lastKnownWorkAt, 'lastKnownWorkAt');
154
+ const phaseHintRaw = asStringOrNull(model.phaseHint, 'phaseHint');
155
+ const observedAt = asString(model.observedAt, 'observedAt');
156
+ if (
157
+ phaseHintRaw !== null &&
158
+ phaseHintRaw !== 'needs-action' &&
159
+ phaseHintRaw !== 'working' &&
160
+ phaseHintRaw !== 'idle'
161
+ ) {
162
+ throw new Error('expected phaseHint enum value');
163
+ }
164
+ return {
165
+ runtimeStatus: runtimeStatusRaw,
166
+ phase: phaseRaw,
167
+ glyph,
168
+ badge,
169
+ detailText,
170
+ attentionReason,
171
+ lastKnownWork,
172
+ lastKnownWorkAt,
173
+ phaseHint: phaseHintRaw,
174
+ observedAt,
175
+ };
176
+ }
177
+
178
+ export function normalizeStoredDirectoryRow(value: unknown): ControlPlaneDirectoryRecord {
179
+ const row = asRecord(value);
180
+ return {
181
+ directoryId: asString(row.directory_id, 'directory_id'),
182
+ tenantId: asString(row.tenant_id, 'tenant_id'),
183
+ userId: asString(row.user_id, 'user_id'),
184
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
185
+ path: asString(row.path, 'path'),
186
+ createdAt: asString(row.created_at, 'created_at'),
187
+ archivedAt: asStringOrNull(row.archived_at, 'archived_at'),
188
+ };
189
+ }
190
+
191
+ export function normalizeStoredConversationRow(value: unknown): ControlPlaneConversationRecord {
192
+ const row = asRecord(value);
193
+ const runtimeStatus = normalizeRuntimeStatus(row.runtime_status);
194
+ const runtimeAttentionReason = asStringOrNull(
195
+ row.runtime_attention_reason,
196
+ 'runtime_attention_reason',
197
+ );
198
+ const runtimeLastEventAt = asStringOrNull(row.runtime_last_event_at, 'runtime_last_event_at');
199
+ const lastExitSignal = asStringOrNull(row.runtime_last_exit_signal, 'runtime_last_exit_signal');
200
+ if (lastExitSignal !== null && !/^SIG[A-Z0-9]+(?:_[A-Z0-9]+)*$/.test(lastExitSignal)) {
201
+ throw new Error('expected runtime_last_exit_signal to be a signal name');
202
+ }
203
+ return {
204
+ conversationId: asString(row.conversation_id, 'conversation_id'),
205
+ directoryId: asString(row.directory_id, 'directory_id'),
206
+ tenantId: asString(row.tenant_id, 'tenant_id'),
207
+ userId: asString(row.user_id, 'user_id'),
208
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
209
+ title: asString(row.title, 'title'),
210
+ agentType: asString(row.agent_type, 'agent_type'),
211
+ createdAt: asString(row.created_at, 'created_at'),
212
+ archivedAt: asStringOrNull(row.archived_at, 'archived_at'),
213
+ runtimeStatus,
214
+ runtimeStatusModel: normalizeRuntimeStatusModel(row.runtime_status_model_json),
215
+ runtimeLive: asBooleanFromInt(row.runtime_live, 'runtime_live'),
216
+ runtimeAttentionReason,
217
+ runtimeProcessId: asNumberOrNull(row.runtime_process_id, 'runtime_process_id'),
218
+ runtimeLastEventAt,
219
+ runtimeLastExit:
220
+ row.runtime_last_exit_code === null && row.runtime_last_exit_signal === null
221
+ ? null
222
+ : {
223
+ code: asNumberOrNull(row.runtime_last_exit_code, 'runtime_last_exit_code'),
224
+ signal: lastExitSignal as NodeJS.Signals | null,
225
+ },
226
+ adapterState: normalizeAdapterState(row.adapter_state_json),
227
+ };
228
+ }
229
+
230
+ export function normalizeTelemetrySource(value: unknown): CodexTelemetrySource {
231
+ const source = asString(value, 'source');
232
+ if (
233
+ source === 'otlp-log' ||
234
+ source === 'otlp-metric' ||
235
+ source === 'otlp-trace' ||
236
+ source === 'history'
237
+ ) {
238
+ return source;
239
+ }
240
+ throw new Error('expected telemetry source enum value');
241
+ }
242
+
243
+ function normalizePayloadJson(value: unknown): Record<string, unknown> {
244
+ if (typeof value !== 'string') {
245
+ throw new Error('expected string for payload_json');
246
+ }
247
+ try {
248
+ const parsed = JSON.parse(value) as unknown;
249
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
250
+ return {};
251
+ }
252
+ return parsed as Record<string, unknown>;
253
+ } catch {
254
+ return {};
255
+ }
256
+ }
257
+
258
+ export function normalizeTelemetryRow(value: unknown): ControlPlaneTelemetryRecord {
259
+ const row = asRecord(value);
260
+ return {
261
+ telemetryId: asNumberOrNull(row.telemetry_id, 'telemetry_id') as number,
262
+ source: normalizeTelemetrySource(row.source),
263
+ sessionId: asStringOrNull(row.session_id, 'session_id'),
264
+ providerThreadId: asStringOrNull(row.provider_thread_id, 'provider_thread_id'),
265
+ eventName: asStringOrNull(row.event_name, 'event_name'),
266
+ severity: asStringOrNull(row.severity, 'severity'),
267
+ summary: asStringOrNull(row.summary, 'summary'),
268
+ observedAt: asString(row.observed_at, 'observed_at'),
269
+ ingestedAt: asString(row.ingested_at, 'ingested_at'),
270
+ payload: normalizePayloadJson(row.payload_json),
271
+ fingerprint: asString(row.fingerprint, 'fingerprint'),
272
+ };
273
+ }
274
+
275
+ function normalizeRepositoryMetadata(value: unknown): Record<string, unknown> {
276
+ if (typeof value !== 'string') {
277
+ throw new Error('expected string for metadata_json');
278
+ }
279
+ try {
280
+ const parsed = JSON.parse(value) as unknown;
281
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
282
+ return {};
283
+ }
284
+ return parsed as Record<string, unknown>;
285
+ } catch {
286
+ return {};
287
+ }
288
+ }
289
+
290
+ export function defaultTaskLinearRecord(): ControlPlaneTaskLinearRecord {
291
+ return {
292
+ issueId: null,
293
+ identifier: null,
294
+ url: null,
295
+ teamId: null,
296
+ projectId: null,
297
+ projectMilestoneId: null,
298
+ cycleId: null,
299
+ stateId: null,
300
+ assigneeId: null,
301
+ priority: null,
302
+ estimate: null,
303
+ dueDate: null,
304
+ labelIds: [],
305
+ };
306
+ }
307
+
308
+ function normalizeOptionalTaskLinearString(value: string | null, field: string): string | null {
309
+ if (value === null) {
310
+ return null;
311
+ }
312
+ return normalizeNonEmptyLabel(value, field);
313
+ }
314
+
315
+ function normalizeTaskLinearPriority(
316
+ value: number | null,
317
+ field: string,
318
+ ): ControlPlaneTaskLinearPriority | null {
319
+ if (value === null) {
320
+ return null;
321
+ }
322
+ if (!Number.isInteger(value) || value < 0 || value > 4) {
323
+ throw new Error(`expected integer [0..4] for ${field}`);
324
+ }
325
+ return value as ControlPlaneTaskLinearPriority;
326
+ }
327
+
328
+ function normalizeTaskLinearEstimate(value: number | null, field: string): number | null {
329
+ if (value === null) {
330
+ return null;
331
+ }
332
+ if (!Number.isInteger(value) || value < 0) {
333
+ throw new Error(`expected non-negative integer for ${field}`);
334
+ }
335
+ return value;
336
+ }
337
+
338
+ function normalizeTaskLinearDueDate(value: string | null, field: string): string | null {
339
+ if (value === null) {
340
+ return null;
341
+ }
342
+ const normalized = value.trim();
343
+ if (!DATE_ONLY_PATTERN.test(normalized)) {
344
+ throw new Error(`expected YYYY-MM-DD for ${field}`);
345
+ }
346
+ return normalized;
347
+ }
348
+
349
+ function normalizeTaskLinearLabelIds(
350
+ value: readonly string[] | null,
351
+ field: string,
352
+ ): readonly string[] {
353
+ if (value === null) {
354
+ return [];
355
+ }
356
+ return uniqueValues(
357
+ value.map((entry, idx) => normalizeNonEmptyLabel(entry, `${field}[${String(idx)}]`)),
358
+ );
359
+ }
360
+
361
+ function parseTaskLinearInputRecord(
362
+ record: Record<string, unknown>,
363
+ field: string,
364
+ ): TaskLinearInput {
365
+ const parsed: TaskLinearInput = {};
366
+ if ('issueId' in record) {
367
+ parsed.issueId = asStringOrNull(record.issueId, `${field}.issueId`);
368
+ }
369
+ if ('identifier' in record) {
370
+ parsed.identifier = asStringOrNull(record.identifier, `${field}.identifier`);
371
+ }
372
+ if ('url' in record) {
373
+ parsed.url = asStringOrNull(record.url, `${field}.url`);
374
+ }
375
+ if ('teamId' in record) {
376
+ parsed.teamId = asStringOrNull(record.teamId, `${field}.teamId`);
377
+ }
378
+ if ('projectId' in record) {
379
+ parsed.projectId = asStringOrNull(record.projectId, `${field}.projectId`);
380
+ }
381
+ if ('projectMilestoneId' in record) {
382
+ parsed.projectMilestoneId = asStringOrNull(
383
+ record.projectMilestoneId,
384
+ `${field}.projectMilestoneId`,
385
+ );
386
+ }
387
+ if ('cycleId' in record) {
388
+ parsed.cycleId = asStringOrNull(record.cycleId, `${field}.cycleId`);
389
+ }
390
+ if ('stateId' in record) {
391
+ parsed.stateId = asStringOrNull(record.stateId, `${field}.stateId`);
392
+ }
393
+ if ('assigneeId' in record) {
394
+ parsed.assigneeId = asStringOrNull(record.assigneeId, `${field}.assigneeId`);
395
+ }
396
+ if ('priority' in record) {
397
+ parsed.priority = asNumberOrNull(record.priority, `${field}.priority`);
398
+ }
399
+ if ('estimate' in record) {
400
+ parsed.estimate = asNumberOrNull(record.estimate, `${field}.estimate`);
401
+ }
402
+ if ('dueDate' in record) {
403
+ parsed.dueDate = asStringOrNull(record.dueDate, `${field}.dueDate`);
404
+ }
405
+ if ('labelIds' in record) {
406
+ const raw = record.labelIds;
407
+ if (raw === null) {
408
+ parsed.labelIds = null;
409
+ } else if (Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')) {
410
+ parsed.labelIds = raw;
411
+ } else {
412
+ throw new Error(`expected string array or null for ${field}.labelIds`);
413
+ }
414
+ }
415
+ return parsed;
416
+ }
417
+
418
+ export function applyTaskLinearInput(
419
+ base: ControlPlaneTaskLinearRecord,
420
+ input: TaskLinearInput,
421
+ ): ControlPlaneTaskLinearRecord {
422
+ return {
423
+ issueId:
424
+ input.issueId === undefined
425
+ ? base.issueId
426
+ : normalizeOptionalTaskLinearString(input.issueId, 'linear.issueId'),
427
+ identifier:
428
+ input.identifier === undefined
429
+ ? base.identifier
430
+ : normalizeOptionalTaskLinearString(input.identifier, 'linear.identifier'),
431
+ url:
432
+ input.url === undefined
433
+ ? base.url
434
+ : normalizeOptionalTaskLinearString(input.url, 'linear.url'),
435
+ teamId:
436
+ input.teamId === undefined
437
+ ? base.teamId
438
+ : normalizeOptionalTaskLinearString(input.teamId, 'linear.teamId'),
439
+ projectId:
440
+ input.projectId === undefined
441
+ ? base.projectId
442
+ : normalizeOptionalTaskLinearString(input.projectId, 'linear.projectId'),
443
+ projectMilestoneId:
444
+ input.projectMilestoneId === undefined
445
+ ? base.projectMilestoneId
446
+ : normalizeOptionalTaskLinearString(input.projectMilestoneId, 'linear.projectMilestoneId'),
447
+ cycleId:
448
+ input.cycleId === undefined
449
+ ? base.cycleId
450
+ : normalizeOptionalTaskLinearString(input.cycleId, 'linear.cycleId'),
451
+ stateId:
452
+ input.stateId === undefined
453
+ ? base.stateId
454
+ : normalizeOptionalTaskLinearString(input.stateId, 'linear.stateId'),
455
+ assigneeId:
456
+ input.assigneeId === undefined
457
+ ? base.assigneeId
458
+ : normalizeOptionalTaskLinearString(input.assigneeId, 'linear.assigneeId'),
459
+ priority:
460
+ input.priority === undefined
461
+ ? base.priority
462
+ : normalizeTaskLinearPriority(input.priority, 'linear.priority'),
463
+ estimate:
464
+ input.estimate === undefined
465
+ ? base.estimate
466
+ : normalizeTaskLinearEstimate(input.estimate, 'linear.estimate'),
467
+ dueDate:
468
+ input.dueDate === undefined
469
+ ? base.dueDate
470
+ : normalizeTaskLinearDueDate(input.dueDate, 'linear.dueDate'),
471
+ labelIds:
472
+ input.labelIds === undefined
473
+ ? base.labelIds
474
+ : normalizeTaskLinearLabelIds(input.labelIds, 'linear.labelIds'),
475
+ };
476
+ }
477
+
478
+ export function serializeTaskLinear(linear: ControlPlaneTaskLinearRecord): string {
479
+ return JSON.stringify({
480
+ issueId: linear.issueId,
481
+ identifier: linear.identifier,
482
+ url: linear.url,
483
+ teamId: linear.teamId,
484
+ projectId: linear.projectId,
485
+ projectMilestoneId: linear.projectMilestoneId,
486
+ cycleId: linear.cycleId,
487
+ stateId: linear.stateId,
488
+ assigneeId: linear.assigneeId,
489
+ priority: linear.priority,
490
+ estimate: linear.estimate,
491
+ dueDate: linear.dueDate,
492
+ labelIds: [...linear.labelIds],
493
+ });
494
+ }
495
+
496
+ function normalizeTaskLinear(value: unknown): ControlPlaneTaskLinearRecord {
497
+ if (typeof value !== 'string') {
498
+ throw new Error('expected string for linear_json');
499
+ }
500
+ let parsed: unknown;
501
+ try {
502
+ parsed = JSON.parse(value) as unknown;
503
+ } catch {
504
+ return defaultTaskLinearRecord();
505
+ }
506
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
507
+ return defaultTaskLinearRecord();
508
+ }
509
+ return applyTaskLinearInput(
510
+ defaultTaskLinearRecord(),
511
+ parseTaskLinearInputRecord(parsed as Record<string, unknown>, 'linear_json'),
512
+ );
513
+ }
514
+
515
+ function normalizeTaskStatus(value: unknown): ControlPlaneTaskStatus {
516
+ const status = asString(value, 'status');
517
+ if (
518
+ status === 'draft' ||
519
+ status === 'ready' ||
520
+ status === 'in-progress' ||
521
+ status === 'completed'
522
+ ) {
523
+ return status;
524
+ }
525
+ if (status === 'queued') {
526
+ return 'ready';
527
+ }
528
+ throw new Error('expected task status enum value');
529
+ }
530
+
531
+ function normalizeTaskScopeKind(
532
+ value: unknown,
533
+ repositoryId: string | null,
534
+ projectId: string | null,
535
+ ): ControlPlaneTaskScopeKind {
536
+ if (typeof value === 'string') {
537
+ if (value === 'global' || value === 'repository' || value === 'project') {
538
+ return value;
539
+ }
540
+ throw new Error('expected task scope enum value');
541
+ }
542
+ if (projectId !== null) {
543
+ return 'project';
544
+ }
545
+ if (repositoryId !== null) {
546
+ return 'repository';
547
+ }
548
+ return 'global';
549
+ }
550
+
551
+ function normalizeProjectTaskFocusMode(value: unknown): ControlPlaneProjectTaskFocusMode {
552
+ const mode = asString(value, 'task_focus_mode');
553
+ if (mode === 'balanced' || mode === 'own-only') {
554
+ return mode;
555
+ }
556
+ throw new Error('expected project task focus enum value');
557
+ }
558
+
559
+ function normalizeProjectThreadSpawnMode(value: unknown): ControlPlaneProjectThreadSpawnMode {
560
+ const mode = asString(value, 'thread_spawn_mode');
561
+ if (mode === 'new-thread' || mode === 'reuse-thread') {
562
+ return mode;
563
+ }
564
+ throw new Error('expected project thread spawn enum value');
565
+ }
566
+
567
+ function normalizeAutomationPolicyScope(value: unknown): ControlPlaneAutomationPolicyScope {
568
+ const scope = asString(value, 'scope_type');
569
+ if (scope === 'global' || scope === 'repository' || scope === 'project') {
570
+ return scope;
571
+ }
572
+ throw new Error('expected automation policy scope enum value');
573
+ }
574
+
575
+ function normalizeGitHubPrState(value: unknown): ControlPlaneGitHubPrState {
576
+ const state = asString(value, 'state');
577
+ if (state === 'open' || state === 'closed') {
578
+ return state;
579
+ }
580
+ throw new Error('expected github pr state enum value');
581
+ }
582
+
583
+ function normalizeGitHubCiRollup(value: unknown): ControlPlaneGitHubCiRollup {
584
+ const rollup = asString(value, 'ci_rollup');
585
+ if (
586
+ rollup === 'pending' ||
587
+ rollup === 'success' ||
588
+ rollup === 'failure' ||
589
+ rollup === 'cancelled' ||
590
+ rollup === 'neutral' ||
591
+ rollup === 'none'
592
+ ) {
593
+ return rollup;
594
+ }
595
+ throw new Error('expected github ci rollup enum value');
596
+ }
597
+
598
+ export function normalizeRepositoryRow(value: unknown): ControlPlaneRepositoryRecord {
599
+ const row = asRecord(value);
600
+ return {
601
+ repositoryId: asString(row.repository_id, 'repository_id'),
602
+ tenantId: asString(row.tenant_id, 'tenant_id'),
603
+ userId: asString(row.user_id, 'user_id'),
604
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
605
+ name: asString(row.name, 'name'),
606
+ remoteUrl: asString(row.remote_url, 'remote_url'),
607
+ defaultBranch: asString(row.default_branch, 'default_branch'),
608
+ metadata: normalizeRepositoryMetadata(row.metadata_json),
609
+ createdAt: asString(row.created_at, 'created_at'),
610
+ archivedAt: asStringOrNull(row.archived_at, 'archived_at'),
611
+ };
612
+ }
613
+
614
+ export function normalizeTaskRow(value: unknown): ControlPlaneTaskRecord {
615
+ const row = asRecord(value);
616
+ const repositoryId = asStringOrNull(row.repository_id, 'repository_id');
617
+ const projectId = asStringOrNull(row.project_id, 'project_id');
618
+ return {
619
+ taskId: asString(row.task_id, 'task_id'),
620
+ tenantId: asString(row.tenant_id, 'tenant_id'),
621
+ userId: asString(row.user_id, 'user_id'),
622
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
623
+ repositoryId,
624
+ scopeKind: normalizeTaskScopeKind(row.scope_kind, repositoryId, projectId),
625
+ projectId,
626
+ title: asString(row.title, 'title'),
627
+ description: asString(row.description, 'description'),
628
+ status: normalizeTaskStatus(row.status),
629
+ orderIndex: asNumberOrNull(row.order_index, 'order_index') as number,
630
+ claimedByControllerId: asStringOrNull(row.claimed_by_controller_id, 'claimed_by_controller_id'),
631
+ claimedByDirectoryId: asStringOrNull(row.claimed_by_directory_id, 'claimed_by_directory_id'),
632
+ branchName: asStringOrNull(row.branch_name, 'branch_name'),
633
+ baseBranch: asStringOrNull(row.base_branch, 'base_branch'),
634
+ claimedAt: asStringOrNull(row.claimed_at, 'claimed_at'),
635
+ completedAt: asStringOrNull(row.completed_at, 'completed_at'),
636
+ linear: normalizeTaskLinear(row.linear_json),
637
+ createdAt: asString(row.created_at, 'created_at'),
638
+ updatedAt: asString(row.updated_at, 'updated_at'),
639
+ };
640
+ }
641
+
642
+ export function normalizeProjectSettingsRow(value: unknown): ControlPlaneProjectSettingsRecord {
643
+ const row = asRecord(value);
644
+ return {
645
+ directoryId: asString(row.directory_id, 'directory_id'),
646
+ tenantId: asString(row.tenant_id, 'tenant_id'),
647
+ userId: asString(row.user_id, 'user_id'),
648
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
649
+ pinnedBranch: asStringOrNull(row.pinned_branch, 'pinned_branch'),
650
+ taskFocusMode: normalizeProjectTaskFocusMode(row.task_focus_mode),
651
+ threadSpawnMode: normalizeProjectThreadSpawnMode(row.thread_spawn_mode),
652
+ createdAt: asString(row.created_at, 'created_at'),
653
+ updatedAt: asString(row.updated_at, 'updated_at'),
654
+ };
655
+ }
656
+
657
+ export function normalizeAutomationPolicyRow(value: unknown): ControlPlaneAutomationPolicyRecord {
658
+ const row = asRecord(value);
659
+ const scope = normalizeAutomationPolicyScope(row.scope_type);
660
+ const scopeId = asStringOrNull(row.scope_id, 'scope_id');
661
+ return {
662
+ policyId: asString(row.policy_id, 'policy_id'),
663
+ tenantId: asString(row.tenant_id, 'tenant_id'),
664
+ userId: asString(row.user_id, 'user_id'),
665
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
666
+ scope,
667
+ scopeId: scope === 'global' ? null : normalizeNonEmptyLabel(scopeId ?? '', 'scope_id'),
668
+ automationEnabled: asBooleanFromInt(row.automation_enabled, 'automation_enabled'),
669
+ frozen: asBooleanFromInt(row.frozen, 'frozen'),
670
+ createdAt: asString(row.created_at, 'created_at'),
671
+ updatedAt: asString(row.updated_at, 'updated_at'),
672
+ };
673
+ }
674
+
675
+ export function normalizeGitHubPullRequestRow(value: unknown): ControlPlaneGitHubPullRequestRecord {
676
+ const row = asRecord(value);
677
+ return {
678
+ prRecordId: asString(row.pr_record_id, 'pr_record_id'),
679
+ tenantId: asString(row.tenant_id, 'tenant_id'),
680
+ userId: asString(row.user_id, 'user_id'),
681
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
682
+ repositoryId: asString(row.repository_id, 'repository_id'),
683
+ directoryId: asStringOrNull(row.directory_id, 'directory_id'),
684
+ owner: asString(row.owner, 'owner'),
685
+ repo: asString(row.repo, 'repo'),
686
+ number: asNumberOrNull(row.number, 'number') as number,
687
+ title: asString(row.title, 'title'),
688
+ url: asString(row.url, 'url'),
689
+ authorLogin: asStringOrNull(row.author_login, 'author_login'),
690
+ headBranch: asString(row.head_branch, 'head_branch'),
691
+ headSha: asString(row.head_sha, 'head_sha'),
692
+ baseBranch: asString(row.base_branch, 'base_branch'),
693
+ state: normalizeGitHubPrState(row.state),
694
+ isDraft: asBooleanFromInt(row.is_draft, 'is_draft'),
695
+ ciRollup: normalizeGitHubCiRollup(row.ci_rollup),
696
+ createdAt: asString(row.created_at, 'created_at'),
697
+ updatedAt: asString(row.updated_at, 'updated_at'),
698
+ closedAt: asStringOrNull(row.closed_at, 'closed_at'),
699
+ observedAt: asString(row.observed_at, 'observed_at'),
700
+ };
701
+ }
702
+
703
+ function normalizeGitHubPrJobProvider(value: unknown): ControlPlaneGitHubPrJobRecord['provider'] {
704
+ const provider = asString(value, 'provider');
705
+ if (provider === 'check-run' || provider === 'status-context') {
706
+ return provider;
707
+ }
708
+ throw new Error('expected github pr job provider enum value');
709
+ }
710
+
711
+ export function normalizeGitHubPrJobRow(value: unknown): ControlPlaneGitHubPrJobRecord {
712
+ const row = asRecord(value);
713
+ return {
714
+ jobRecordId: asString(row.job_record_id, 'job_record_id'),
715
+ tenantId: asString(row.tenant_id, 'tenant_id'),
716
+ userId: asString(row.user_id, 'user_id'),
717
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
718
+ repositoryId: asString(row.repository_id, 'repository_id'),
719
+ prRecordId: asString(row.pr_record_id, 'pr_record_id'),
720
+ provider: normalizeGitHubPrJobProvider(row.provider),
721
+ externalId: asString(row.external_id, 'external_id'),
722
+ name: asString(row.name, 'name'),
723
+ status: asString(row.status, 'status'),
724
+ conclusion: asStringOrNull(row.conclusion, 'conclusion'),
725
+ url: asStringOrNull(row.url, 'url'),
726
+ startedAt: asStringOrNull(row.started_at, 'started_at'),
727
+ completedAt: asStringOrNull(row.completed_at, 'completed_at'),
728
+ observedAt: asString(row.observed_at, 'observed_at'),
729
+ updatedAt: asString(row.updated_at, 'updated_at'),
730
+ };
731
+ }
732
+
733
+ export function normalizeGitHubSyncStateRow(value: unknown): ControlPlaneGitHubSyncStateRecord {
734
+ const row = asRecord(value);
735
+ return {
736
+ stateId: asString(row.state_id, 'state_id'),
737
+ tenantId: asString(row.tenant_id, 'tenant_id'),
738
+ userId: asString(row.user_id, 'user_id'),
739
+ workspaceId: asString(row.workspace_id, 'workspace_id'),
740
+ repositoryId: asString(row.repository_id, 'repository_id'),
741
+ directoryId: asStringOrNull(row.directory_id, 'directory_id'),
742
+ branchName: asString(row.branch_name, 'branch_name'),
743
+ lastSyncAt: asString(row.last_sync_at, 'last_sync_at'),
744
+ lastSuccessAt: asStringOrNull(row.last_success_at, 'last_success_at'),
745
+ lastError: asStringOrNull(row.last_error, 'last_error'),
746
+ lastErrorAt: asStringOrNull(row.last_error_at, 'last_error_at'),
747
+ };
748
+ }
749
+
750
+ export function normalizeNonEmptyLabel(value: string, field: string): string {
751
+ const normalized = value.trim();
752
+ if (normalized.length === 0) {
753
+ throw new Error(`expected non-empty ${field}`);
754
+ }
755
+ return normalized;
756
+ }
757
+
758
+ export function uniqueValues(values: readonly string[]): readonly string[] {
759
+ return [...new Set(values)];
760
+ }