@vladimirven/openswe 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 (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Session database operations
3
+ *
4
+ * CRUD operations for managing sessions in the database.
5
+ */
6
+
7
+ import { getDatabase, nowISO } from "./db"
8
+ import { generateId } from "../utils/id"
9
+ import type {
10
+ Session,
11
+ CreateSessionInput,
12
+ UpdateSessionInput,
13
+ Phase,
14
+ Status,
15
+ AISessionData,
16
+ } from "./types"
17
+ import { isValidAISessionData } from "./types"
18
+ import { logger } from "../utils/logger"
19
+
20
+ // ============================================================================
21
+ // Database Row Type
22
+ // ============================================================================
23
+
24
+ interface SessionRow {
25
+ id: string
26
+ name: string
27
+ issue_number: number | null
28
+ issue_title: string | null
29
+ issue_body: string | null
30
+ issue_url: string | null
31
+ worktree_path: string
32
+ branch_name: string
33
+ phase: string
34
+ status: string
35
+ attention_reason: string | null
36
+ retry_count: number
37
+ tokens_used: number
38
+ pid: number | null
39
+ ai_session_data: string | null
40
+ created_at: string
41
+ updated_at: string
42
+ }
43
+
44
+ // ============================================================================
45
+ // Row Mapping
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Convert database row to Session
50
+ */
51
+ function rowToSession(row: SessionRow): Session {
52
+ return {
53
+ id: row.id,
54
+ name: row.name,
55
+ issueNumber: row.issue_number,
56
+ issueTitle: row.issue_title,
57
+ issueBody: row.issue_body,
58
+ issueUrl: row.issue_url,
59
+ worktreePath: row.worktree_path,
60
+ branchName: row.branch_name,
61
+ phase: row.phase as Phase,
62
+ status: row.status as Status,
63
+ attentionReason: row.attention_reason,
64
+ retryCount: row.retry_count,
65
+ tokensUsed: row.tokens_used,
66
+ pid: row.pid,
67
+ aiSessionData: parseAISessionData(row.ai_session_data),
68
+ createdAt: row.created_at,
69
+ updatedAt: row.updated_at,
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Parse AI session data JSON
75
+ */
76
+ function parseAISessionData(data: string | null): AISessionData | null {
77
+ if (!data) return null
78
+ try {
79
+ const parsed = JSON.parse(data)
80
+ if (!isValidAISessionData(parsed)) return null
81
+ return parsed
82
+ } catch {
83
+ return null
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Serialize AI session data to JSON
89
+ */
90
+ function serializeAISessionData(data: AISessionData | null | undefined): string | null {
91
+ if (!data) return null
92
+ return JSON.stringify(data)
93
+ }
94
+
95
+ // ============================================================================
96
+ // Query Operations
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Get all sessions
101
+ */
102
+ export function getAllSessions(): Session[] {
103
+ const db = getDatabase()
104
+ const rows = db.query<SessionRow, []>("SELECT * FROM sessions ORDER BY created_at DESC").all()
105
+ return rows.map(rowToSession)
106
+ }
107
+
108
+ /**
109
+ * Get a session by ID
110
+ *
111
+ * @param id - Session UUID
112
+ * @returns Session or null if not found
113
+ */
114
+ export function getSession(id: string): Session | null {
115
+ const db = getDatabase()
116
+ const row = db
117
+ .query<SessionRow, [string]>("SELECT * FROM sessions WHERE id = ?")
118
+ .get(id)
119
+
120
+ return row ? rowToSession(row) : null
121
+ }
122
+
123
+ /**
124
+ * Get sessions by status
125
+ *
126
+ * @param status - Status to filter by
127
+ */
128
+ export function getSessionsByStatus(status: Status): Session[] {
129
+ const db = getDatabase()
130
+ const rows = db
131
+ .query<SessionRow, [string]>(
132
+ "SELECT * FROM sessions WHERE status = ? ORDER BY created_at DESC"
133
+ )
134
+ .all(status)
135
+
136
+ return rows.map(rowToSession)
137
+ }
138
+
139
+ /**
140
+ * Get sessions by phase
141
+ *
142
+ * @param phase - Phase to filter by
143
+ */
144
+ export function getSessionsByPhase(phase: Phase): Session[] {
145
+ const db = getDatabase()
146
+ const rows = db
147
+ .query<SessionRow, [string]>(
148
+ "SELECT * FROM sessions WHERE phase = ? ORDER BY created_at DESC"
149
+ )
150
+ .all(phase)
151
+
152
+ return rows.map(rowToSession)
153
+ }
154
+
155
+ /**
156
+ * Get count of active sessions
157
+ */
158
+ export function getActiveSessionCount(): number {
159
+ const db = getDatabase()
160
+ const result = db
161
+ .query<{ count: number }, []>(
162
+ "SELECT COUNT(*) as count FROM sessions WHERE status = 'active'"
163
+ )
164
+ .get()
165
+
166
+ return result?.count ?? 0
167
+ }
168
+
169
+ /**
170
+ * Get count of all sessions
171
+ */
172
+ export function getSessionCount(): number {
173
+ const db = getDatabase()
174
+ const result = db
175
+ .query<{ count: number }, []>("SELECT COUNT(*) as count FROM sessions")
176
+ .get()
177
+
178
+ return result?.count ?? 0
179
+ }
180
+
181
+ // ============================================================================
182
+ // Create Operations
183
+ // ============================================================================
184
+
185
+ /**
186
+ * Create a new session
187
+ *
188
+ * @param data - Session creation data
189
+ * @returns The created Session
190
+ */
191
+ export function createSession(data: CreateSessionInput): Session {
192
+ const db = getDatabase()
193
+ const id = generateId()
194
+ const now = nowISO()
195
+ const aiSessionData = serializeAISessionData(data.aiSessionData ?? null)
196
+
197
+ logger.debug("Creating session", {
198
+ id,
199
+ name: data.name,
200
+ issueNumber: data.issueNumber ?? null,
201
+ worktreePath: data.worktreePath,
202
+ })
203
+
204
+ db.query(
205
+ `INSERT INTO sessions (
206
+ id, name, issue_number, issue_title, issue_body, issue_url,
207
+ worktree_path, branch_name, phase, status,
208
+ attention_reason, retry_count, tokens_used, pid, ai_session_data,
209
+ created_at, updated_at
210
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 'queued', NULL, 0, 0, NULL, ?, ?, ?)`
211
+ ).run(
212
+ id,
213
+ data.name,
214
+ data.issueNumber ?? null,
215
+ data.issueTitle ?? null,
216
+ data.issueBody ?? null,
217
+ data.issueUrl ?? null,
218
+ data.worktreePath,
219
+ data.branchName,
220
+ aiSessionData,
221
+ now,
222
+ now
223
+ )
224
+
225
+ logger.debug("Session created", { id })
226
+
227
+ return {
228
+ id,
229
+ name: data.name,
230
+ issueNumber: data.issueNumber ?? null,
231
+ issueTitle: data.issueTitle ?? null,
232
+ issueBody: data.issueBody ?? null,
233
+ issueUrl: data.issueUrl ?? null,
234
+ worktreePath: data.worktreePath,
235
+ branchName: data.branchName,
236
+ phase: "pending",
237
+ status: "queued",
238
+ attentionReason: null,
239
+ retryCount: 0,
240
+ tokensUsed: 0,
241
+ pid: null,
242
+ aiSessionData: data.aiSessionData ?? null,
243
+ createdAt: now,
244
+ updatedAt: now,
245
+ }
246
+ }
247
+
248
+ // ============================================================================
249
+ // Update Operations
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Update a session
254
+ *
255
+ * @param id - Session ID
256
+ * @param data - Fields to update
257
+ * @returns Updated Session
258
+ * @throws Error if session not found
259
+ */
260
+ export function updateSession(id: string, data: UpdateSessionInput): Session {
261
+ const db = getDatabase()
262
+ const now = nowISO()
263
+
264
+ // Build dynamic update query
265
+ const updates: string[] = ["updated_at = ?"]
266
+ const values: (string | number | null)[] = [now]
267
+
268
+ if (data.name !== undefined) {
269
+ updates.push("name = ?")
270
+ values.push(data.name)
271
+ }
272
+ if (data.phase !== undefined) {
273
+ updates.push("phase = ?")
274
+ values.push(data.phase)
275
+ }
276
+ if (data.status !== undefined) {
277
+ updates.push("status = ?")
278
+ values.push(data.status)
279
+ }
280
+
281
+ const shouldClearAttention =
282
+ data.status !== undefined &&
283
+ data.status !== "needs_attention" &&
284
+ data.attentionReason === undefined
285
+
286
+ if (data.attentionReason !== undefined) {
287
+ updates.push("attention_reason = ?")
288
+ values.push(data.attentionReason)
289
+ } else if (shouldClearAttention) {
290
+ updates.push("attention_reason = NULL")
291
+ }
292
+ if (data.retryCount !== undefined) {
293
+ updates.push("retry_count = ?")
294
+ values.push(data.retryCount)
295
+ }
296
+ if (data.tokensUsed !== undefined) {
297
+ updates.push("tokens_used = ?")
298
+ values.push(data.tokensUsed)
299
+ }
300
+ if (data.pid !== undefined) {
301
+ updates.push("pid = ?")
302
+ values.push(data.pid)
303
+ }
304
+ if (data.aiSessionData !== undefined) {
305
+ updates.push("ai_session_data = ?")
306
+ values.push(serializeAISessionData(data.aiSessionData))
307
+ }
308
+
309
+ values.push(id)
310
+
311
+ db.query(`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`).run(...values)
312
+
313
+ const session = getSession(id)
314
+ if (!session) {
315
+ throw new Error(`Session not found: ${id}`)
316
+ }
317
+
318
+ return session
319
+ }
320
+
321
+ /**
322
+ * Update session phase
323
+ *
324
+ * @param id - Session ID
325
+ * @param phase - New phase
326
+ */
327
+ export function updateSessionPhase(id: string, phase: Phase): void {
328
+ updateSession(id, { phase })
329
+ }
330
+
331
+ /**
332
+ * Update session status
333
+ *
334
+ * @param id - Session ID
335
+ * @param status - New status
336
+ * @param reason - Optional attention reason (for needs_attention status)
337
+ */
338
+ export function updateSessionStatus(
339
+ id: string,
340
+ status: Status,
341
+ reason?: string
342
+ ): void {
343
+ updateSession(id, {
344
+ status,
345
+ attentionReason: status === "needs_attention" ? reason ?? null : null,
346
+ })
347
+ }
348
+
349
+ /**
350
+ * Increment retry count and return new value
351
+ *
352
+ * @param id - Session ID
353
+ * @returns New retry count
354
+ */
355
+ export function incrementRetryCount(id: string): number {
356
+ const db = getDatabase()
357
+ const now = nowISO()
358
+
359
+ db.query(
360
+ "UPDATE sessions SET retry_count = retry_count + 1, updated_at = ? WHERE id = ?"
361
+ ).run(now, id)
362
+
363
+ const session = getSession(id)
364
+ return session?.retryCount ?? 0
365
+ }
366
+
367
+ /**
368
+ * Update tokens used
369
+ *
370
+ * @param id - Session ID
371
+ * @param tokens - New total tokens used
372
+ */
373
+ export function updateTokensUsed(id: string, tokens: number): void {
374
+ const db = getDatabase()
375
+ const now = nowISO()
376
+
377
+ db.query("UPDATE sessions SET tokens_used = ?, updated_at = ? WHERE id = ?").run(
378
+ tokens,
379
+ now,
380
+ id
381
+ )
382
+ }
383
+
384
+ /**
385
+ * Set the process ID for a session
386
+ *
387
+ * @param id - Session ID
388
+ * @param pid - Process ID or null to clear
389
+ */
390
+ export function setPid(id: string, pid: number | null): void {
391
+ const db = getDatabase()
392
+ const now = nowISO()
393
+
394
+ db.query("UPDATE sessions SET pid = ?, updated_at = ? WHERE id = ?").run(
395
+ pid,
396
+ now,
397
+ id
398
+ )
399
+ }
400
+
401
+ /**
402
+ * Set the AI session data for a session
403
+ *
404
+ * @param id - Session ID
405
+ * @param data - AI session data or null to clear
406
+ */
407
+ export function setAISessionData(id: string, data: AISessionData | null): void {
408
+ const db = getDatabase()
409
+ const now = nowISO()
410
+ const serialized = serializeAISessionData(data)
411
+
412
+ db.query("UPDATE sessions SET ai_session_data = ?, updated_at = ? WHERE id = ?").run(
413
+ serialized,
414
+ now,
415
+ id
416
+ )
417
+ }
418
+
419
+ /**
420
+ * Get the AI session data for a session
421
+ *
422
+ * @param id - Session ID
423
+ * @returns AISessionData or null
424
+ */
425
+ export function getAISessionData(id: string): AISessionData | null {
426
+ const session = getSession(id)
427
+ return session?.aiSessionData ?? null
428
+ }
429
+
430
+ // ============================================================================
431
+ // Delete Operations
432
+ // ============================================================================
433
+
434
+ /**
435
+ * Delete a session
436
+ *
437
+ * Note: This cascades to delete associated tasks and output buffers.
438
+ *
439
+ * @param id - Session ID
440
+ */
441
+ export function deleteSession(id: string): void {
442
+ const db = getDatabase()
443
+ db.query("DELETE FROM sessions WHERE id = ?").run(id)
444
+ }
445
+
446
+ /**
447
+ * Delete all sessions
448
+ *
449
+ * Warning: This cascades to delete all tasks and buffers.
450
+ */
451
+ export function deleteAllSessions(): void {
452
+ const db = getDatabase()
453
+ db.query("DELETE FROM sessions").run()
454
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Database type definitions for OpenSWE
3
+ *
4
+ * Defines types for sessions, human tasks, project state, and output buffers
5
+ * stored in the SQLite database.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Enums / Type Literals
10
+ // ============================================================================
11
+
12
+ /** Session phases representing the workflow stages */
13
+ export type Phase =
14
+ | "pending"
15
+ | "planning"
16
+ | "working"
17
+ | "completed"
18
+ | "failed"
19
+
20
+ /** Session status for tracking current state */
21
+ export type Status =
22
+ | "queued"
23
+ | "active"
24
+ | "paused"
25
+ | "needs_attention"
26
+ | "completed"
27
+ | "failed"
28
+
29
+ /** Backend session reference for AI integrations */
30
+ export interface AISessionData {
31
+ backend: "opencode" | "claude"
32
+ sessionId?: string
33
+ [key: string]: unknown
34
+ }
35
+
36
+ // ============================================================================
37
+ // Session Types
38
+ // ============================================================================
39
+
40
+ /** A session represents work on a specific issue or task */
41
+ export interface Session {
42
+ /** Unique identifier (UUID) */
43
+ id: string
44
+ /** Human-readable session name */
45
+ name: string
46
+ /** GitHub issue number (if linked to an issue) */
47
+ issueNumber: number | null
48
+ /** GitHub issue title (cached) */
49
+ issueTitle: string | null
50
+ /** GitHub issue body/description (cached) */
51
+ issueBody: string | null
52
+ /** GitHub issue URL (for quick access) */
53
+ issueUrl: string | null
54
+ /** Absolute path to the git worktree for this session */
55
+ worktreePath: string
56
+ /** Git branch name for this session */
57
+ branchName: string
58
+ /** Current workflow phase */
59
+ phase: Phase
60
+ /** Current session status */
61
+ status: Status
62
+ /** Reason for needs_attention status */
63
+ attentionReason: string | null
64
+ /** Number of retry attempts */
65
+ retryCount: number
66
+ /** Total tokens used by AI */
67
+ tokensUsed: number
68
+ /** Process ID if session is running */
69
+ pid: number | null
70
+ /** AI backend session reference data */
71
+ aiSessionData: AISessionData | null
72
+ /** ISO timestamp when session was created */
73
+ createdAt: string
74
+ /** ISO timestamp when session was last updated */
75
+ updatedAt: string
76
+ }
77
+
78
+ /** Input for creating a new session */
79
+ export interface CreateSessionInput {
80
+ name: string
81
+ issueNumber?: number
82
+ issueTitle?: string
83
+ issueBody?: string
84
+ issueUrl?: string
85
+ worktreePath: string
86
+ branchName: string
87
+ aiSessionData?: AISessionData | null
88
+ }
89
+
90
+ /** Input for updating an existing session */
91
+ export interface UpdateSessionInput {
92
+ name?: string
93
+ phase?: Phase
94
+ status?: Status
95
+ attentionReason?: string | null
96
+ retryCount?: number
97
+ tokensUsed?: number
98
+ pid?: number | null
99
+ aiSessionData?: AISessionData | null
100
+ }
101
+
102
+ // ============================================================================
103
+ // Human Task Types - REMOVED
104
+ // ============================================================================
105
+
106
+ // ============================================================================
107
+ // Project State Types
108
+ // ============================================================================
109
+
110
+ /**
111
+ * Project state stored in the database
112
+ *
113
+ * Note: This is distinct from ProjectConfig in workspace/project.ts.
114
+ * ProjectConfig is stored in .openswe/project.json and is the source of truth.
115
+ * ProjectState in the database mirrors essential fields for querying.
116
+ */
117
+ export interface ProjectState {
118
+ /** Always 1 (singleton) */
119
+ id: 1
120
+ /** Full repository name in owner/repo format */
121
+ repoFullName: string
122
+ /** Git remote URL */
123
+ repoUrl: string
124
+ /** ISO timestamp when project was created */
125
+ createdAt: string
126
+ /** ISO timestamp when project was last opened */
127
+ lastOpenedAt: string
128
+ }
129
+
130
+ /** Input for creating project state */
131
+ export interface CreateProjectInput {
132
+ repoFullName: string
133
+ repoUrl: string
134
+ }
135
+
136
+ // ============================================================================
137
+ // Output Buffer Types
138
+ // ============================================================================
139
+
140
+ /** Circular buffer for session output */
141
+ export interface OutputBuffer {
142
+ /** Session ID this buffer belongs to */
143
+ sessionId: string
144
+ /** Array of output lines (stored as JSON) */
145
+ lines: string[]
146
+ /** ISO timestamp when buffer was last updated */
147
+ lastUpdated: string
148
+ }
149
+
150
+ // ============================================================================
151
+ // Type Guards
152
+ // ============================================================================
153
+
154
+ const VALID_PHASES: readonly Phase[] = [
155
+ "pending",
156
+ "planning",
157
+ "working",
158
+ "completed",
159
+ "failed",
160
+ ]
161
+
162
+ const VALID_STATUSES: readonly Status[] = [
163
+ "queued",
164
+ "active",
165
+ "paused",
166
+ "needs_attention",
167
+ "completed",
168
+ "failed",
169
+ ]
170
+
171
+ const VALID_AI_BACKENDS = ["opencode", "claude"] as const
172
+
173
+ /** Check if a value is a valid Phase */
174
+ export function isValidPhase(val: unknown): val is Phase {
175
+ return typeof val === "string" && VALID_PHASES.includes(val as Phase)
176
+ }
177
+
178
+ /** Check if a value is a valid Status */
179
+ export function isValidStatus(val: unknown): val is Status {
180
+ return typeof val === "string" && VALID_STATUSES.includes(val as Status)
181
+ }
182
+
183
+ /** Check if a value is valid AISessionData */
184
+ export function isValidAISessionData(val: unknown): val is AISessionData {
185
+ if (typeof val !== "object" || val === null) return false
186
+
187
+ const backend = (val as { backend?: unknown }).backend
188
+ if (backend !== "opencode" && backend !== "claude") return false
189
+
190
+ const sessionId = (val as { sessionId?: unknown }).sessionId
191
+ if (sessionId !== undefined && typeof sessionId !== "string") return false
192
+
193
+ return true
194
+ }