@proletariat/cli 0.3.36 → 0.3.40

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 (64) hide show
  1. package/README.md +37 -2
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/branch/where.js +6 -17
  4. package/dist/commands/epic/ticket.js +7 -24
  5. package/dist/commands/execution/config.js +4 -14
  6. package/dist/commands/execution/logs.js +6 -0
  7. package/dist/commands/execution/view.js +8 -0
  8. package/dist/commands/mcp-server.js +2 -1
  9. package/dist/commands/pmo/init.js +12 -40
  10. package/dist/commands/qa/index.d.ts +54 -0
  11. package/dist/commands/qa/index.js +762 -0
  12. package/dist/commands/repo/view.js +2 -8
  13. package/dist/commands/session/attach.js +4 -4
  14. package/dist/commands/session/health.js +4 -4
  15. package/dist/commands/session/list.js +1 -19
  16. package/dist/commands/session/peek.js +6 -6
  17. package/dist/commands/session/poke.js +2 -2
  18. package/dist/commands/ticket/epic.js +17 -43
  19. package/dist/commands/work/spawn-all.js +1 -1
  20. package/dist/commands/work/spawn.js +15 -4
  21. package/dist/commands/work/start.js +17 -9
  22. package/dist/commands/work/watch.js +1 -1
  23. package/dist/commands/workspace/prune.js +3 -3
  24. package/dist/hooks/init.js +10 -2
  25. package/dist/lib/agents/commands.d.ts +5 -0
  26. package/dist/lib/agents/commands.js +143 -97
  27. package/dist/lib/database/drizzle-schema.d.ts +465 -0
  28. package/dist/lib/database/drizzle-schema.js +53 -0
  29. package/dist/lib/database/index.d.ts +47 -1
  30. package/dist/lib/database/index.js +138 -20
  31. package/dist/lib/execution/runners.d.ts +34 -0
  32. package/dist/lib/execution/runners.js +134 -7
  33. package/dist/lib/execution/session-utils.d.ts +5 -0
  34. package/dist/lib/execution/session-utils.js +45 -3
  35. package/dist/lib/execution/spawner.js +15 -2
  36. package/dist/lib/execution/storage.d.ts +1 -1
  37. package/dist/lib/execution/storage.js +17 -2
  38. package/dist/lib/execution/types.d.ts +1 -0
  39. package/dist/lib/mcp/tools/index.d.ts +1 -0
  40. package/dist/lib/mcp/tools/index.js +1 -0
  41. package/dist/lib/mcp/tools/tmux.d.ts +16 -0
  42. package/dist/lib/mcp/tools/tmux.js +182 -0
  43. package/dist/lib/mcp/tools/work.js +52 -0
  44. package/dist/lib/pmo/schema.d.ts +1 -1
  45. package/dist/lib/pmo/schema.js +1 -0
  46. package/dist/lib/pmo/storage/base.js +207 -0
  47. package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
  48. package/dist/lib/pmo/storage/dependencies.js +11 -3
  49. package/dist/lib/pmo/storage/epics.js +1 -1
  50. package/dist/lib/pmo/storage/helpers.d.ts +4 -4
  51. package/dist/lib/pmo/storage/helpers.js +36 -26
  52. package/dist/lib/pmo/storage/projects.d.ts +2 -0
  53. package/dist/lib/pmo/storage/projects.js +207 -119
  54. package/dist/lib/pmo/storage/specs.d.ts +2 -0
  55. package/dist/lib/pmo/storage/specs.js +274 -188
  56. package/dist/lib/pmo/storage/tickets.d.ts +2 -0
  57. package/dist/lib/pmo/storage/tickets.js +350 -290
  58. package/dist/lib/pmo/storage/views.d.ts +2 -0
  59. package/dist/lib/pmo/storage/views.js +183 -130
  60. package/dist/lib/prompt-json.d.ts +5 -0
  61. package/dist/lib/prompt-json.js +9 -0
  62. package/oclif.manifest.json +3922 -3819
  63. package/package.json +11 -6
  64. package/LICENSE +0 -190
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Board view operations.
3
+ *
4
+ * This module uses Drizzle ORM for type-safe database queries.
3
5
  */
4
6
  import { Board, BoardView, BoardViewFilter, BoardViewFilters } from '../types.js';
5
7
  import { StorageContext } from './types.js';
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Board view operations.
3
+ *
4
+ * This module uses Drizzle ORM for type-safe database queries.
3
5
  */
4
- import { PMO_TABLES } from '../schema.js';
6
+ import { eq, and, like, or, asc, desc, sql } from 'drizzle-orm';
7
+ import { pmoBoardViews, pmoProjects, pmoTickets, pmoWorkflowStatuses, pmoSubtasks, pmoTicketMetadata, } from '../../database/drizzle-schema.js';
5
8
  import { PMOError, } from '../types.js';
6
9
  import { slugify } from '../utils.js';
7
10
  import { getAcceptanceCriteriaSync } from './helpers.js';
8
- const T = PMO_TABLES;
9
11
  export class ViewStorage {
10
12
  ctx;
11
13
  constructor(ctx) {
@@ -24,33 +26,42 @@ export class ViewStorage {
24
26
  if (!identifier)
25
27
  return null;
26
28
  // 1. Exact ID match
27
- const exactMatch = this.ctx.db.prepare(`
28
- SELECT id FROM ${T.projects} WHERE id = ?
29
- `).get(identifier);
29
+ const exactMatch = this.ctx.drizzle
30
+ .select({ id: pmoProjects.id })
31
+ .from(pmoProjects)
32
+ .where(eq(pmoProjects.id, identifier))
33
+ .get();
30
34
  if (exactMatch)
31
35
  return exactMatch.id;
32
36
  // 2. Case-insensitive ID match
33
- const caseInsensitiveId = this.ctx.db.prepare(`
34
- SELECT id FROM ${T.projects} WHERE LOWER(id) = LOWER(?)
35
- `).get(identifier);
37
+ const caseInsensitiveId = this.ctx.drizzle
38
+ .select({ id: pmoProjects.id })
39
+ .from(pmoProjects)
40
+ .where(sql `LOWER(${pmoProjects.id}) = LOWER(${identifier})`)
41
+ .get();
36
42
  if (caseInsensitiveId)
37
43
  return caseInsensitiveId.id;
38
44
  // 3. Exact name match
39
- const nameMatch = this.ctx.db.prepare(`
40
- SELECT id FROM ${T.projects} WHERE name = ?
41
- `).get(identifier);
45
+ const nameMatch = this.ctx.drizzle
46
+ .select({ id: pmoProjects.id })
47
+ .from(pmoProjects)
48
+ .where(eq(pmoProjects.name, identifier))
49
+ .get();
42
50
  if (nameMatch)
43
51
  return nameMatch.id;
44
52
  // 4. Case-insensitive name match
45
- const caseInsensitiveName = this.ctx.db.prepare(`
46
- SELECT id FROM ${T.projects} WHERE LOWER(name) = LOWER(?)
47
- `).get(identifier);
53
+ const caseInsensitiveName = this.ctx.drizzle
54
+ .select({ id: pmoProjects.id })
55
+ .from(pmoProjects)
56
+ .where(sql `LOWER(${pmoProjects.name}) = LOWER(${identifier})`)
57
+ .get();
48
58
  if (caseInsensitiveName)
49
59
  return caseInsensitiveName.id;
50
60
  // 5. Slugified name match
51
- const allProjects = this.ctx.db.prepare(`
52
- SELECT id, name FROM ${T.projects}
53
- `).all();
61
+ const allProjects = this.ctx.drizzle
62
+ .select({ id: pmoProjects.id, name: pmoProjects.name })
63
+ .from(pmoProjects)
64
+ .all();
54
65
  const identifierLower = identifier.toLowerCase();
55
66
  for (const project of allProjects) {
56
67
  const projectSlug = slugify(project.name);
@@ -64,34 +75,37 @@ export class ViewStorage {
64
75
  * List board views.
65
76
  */
66
77
  async listBoardViews(filter) {
67
- let sql = `SELECT * FROM ${T.board_views}`;
78
+ let query = this.ctx.drizzle
79
+ .select()
80
+ .from(pmoBoardViews)
81
+ .$dynamic();
68
82
  const conditions = [];
69
- const params = [];
70
83
  if (filter?.projectId) {
71
- conditions.push('project_id = ?');
72
- params.push(filter.projectId);
84
+ conditions.push(eq(pmoBoardViews.projectId, filter.projectId));
73
85
  }
74
86
  if (filter?.isDefault !== undefined) {
75
- conditions.push('is_default = ?');
76
- params.push(filter.isDefault ? 1 : 0);
87
+ conditions.push(eq(pmoBoardViews.isDefault, filter.isDefault));
77
88
  }
78
89
  if (filter?.search) {
79
- conditions.push('(name LIKE ? OR description LIKE ?)');
80
- const searchTerm = `%${filter.search}%`;
81
- params.push(searchTerm, searchTerm);
90
+ conditions.push(or(like(pmoBoardViews.name, `%${filter.search}%`), like(pmoBoardViews.description, `%${filter.search}%`)));
82
91
  }
83
92
  if (conditions.length > 0) {
84
- sql += ' WHERE ' + conditions.join(' AND ');
93
+ query = query.where(and(...conditions));
85
94
  }
86
- sql += ' ORDER BY is_default DESC, name ASC';
87
- const rows = this.ctx.db.prepare(sql).all(...params);
95
+ const rows = query
96
+ .orderBy(desc(pmoBoardViews.isDefault), asc(pmoBoardViews.name))
97
+ .all();
88
98
  return rows.map((row) => this.rowToBoardView(row));
89
99
  }
90
100
  /**
91
101
  * Get a board view by ID.
92
102
  */
93
103
  async getBoardView(id) {
94
- const row = this.ctx.db.prepare(`SELECT * FROM ${T.board_views} WHERE id = ?`).get(id);
104
+ const row = this.ctx.drizzle
105
+ .select()
106
+ .from(pmoBoardViews)
107
+ .where(eq(pmoBoardViews.id, id))
108
+ .get();
95
109
  if (!row)
96
110
  return null;
97
111
  return this.rowToBoardView(row);
@@ -111,14 +125,24 @@ export class ViewStorage {
111
125
  const filters = JSON.stringify(view.filters || {});
112
126
  // If this is set as default, unset other defaults for this project
113
127
  if (view.isDefault) {
114
- this.ctx.db.prepare(`
115
- UPDATE ${T.board_views} SET is_default = 0 WHERE project_id = ?
116
- `).run(view.projectId);
128
+ this.ctx.drizzle
129
+ .update(pmoBoardViews)
130
+ .set({ isDefault: false })
131
+ .where(eq(pmoBoardViews.projectId, view.projectId))
132
+ .run();
117
133
  }
118
- this.ctx.db.prepare(`
119
- INSERT INTO ${T.board_views} (id, project_id, name, description, is_default, filters, group_by, sort_by, created_at, updated_at)
120
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
121
- `).run(id, view.projectId, view.name, view.description || null, view.isDefault ? 1 : 0, filters, view.groupBy || null, view.sortBy || null, now, now);
134
+ this.ctx.drizzle.insert(pmoBoardViews).values({
135
+ id,
136
+ projectId: view.projectId,
137
+ name: view.name,
138
+ description: view.description || null,
139
+ isDefault: view.isDefault || false,
140
+ filters,
141
+ groupBy: view.groupBy || null,
142
+ sortBy: view.sortBy || null,
143
+ createdAt: String(now),
144
+ updatedAt: String(now),
145
+ }).run();
122
146
  return (await this.getBoardView(id));
123
147
  }
124
148
  /**
@@ -129,47 +153,45 @@ export class ViewStorage {
129
153
  if (!existing) {
130
154
  throw new PMOError('NOT_FOUND', `Board view not found: ${id}`);
131
155
  }
132
- const updates = ['updated_at = ?'];
133
- const params = [Date.now()];
134
- if (changes.name !== undefined) {
135
- updates.push('name = ?');
136
- params.push(changes.name);
137
- }
138
- if (changes.description !== undefined) {
139
- updates.push('description = ?');
140
- params.push(changes.description || null);
141
- }
156
+ const updates = {
157
+ updatedAt: String(Date.now()),
158
+ };
159
+ if (changes.name !== undefined)
160
+ updates.name = changes.name;
161
+ if (changes.description !== undefined)
162
+ updates.description = changes.description || null;
142
163
  if (changes.isDefault !== undefined) {
143
164
  // If setting as default, unset other defaults for this project
144
165
  if (changes.isDefault) {
145
- this.ctx.db.prepare(`
146
- UPDATE ${T.board_views} SET is_default = 0 WHERE project_id = ?
147
- `).run(existing.projectId);
166
+ this.ctx.drizzle
167
+ .update(pmoBoardViews)
168
+ .set({ isDefault: false })
169
+ .where(eq(pmoBoardViews.projectId, existing.projectId))
170
+ .run();
148
171
  }
149
- updates.push('is_default = ?');
150
- params.push(changes.isDefault ? 1 : 0);
151
- }
152
- if (changes.filters !== undefined) {
153
- updates.push('filters = ?');
154
- params.push(JSON.stringify(changes.filters));
155
- }
156
- if (changes.groupBy !== undefined) {
157
- updates.push('group_by = ?');
158
- params.push(changes.groupBy || null);
159
- }
160
- if (changes.sortBy !== undefined) {
161
- updates.push('sort_by = ?');
162
- params.push(changes.sortBy || null);
172
+ updates.isDefault = changes.isDefault;
163
173
  }
164
- params.push(id);
165
- this.ctx.db.prepare(`UPDATE ${T.board_views} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
174
+ if (changes.filters !== undefined)
175
+ updates.filters = JSON.stringify(changes.filters);
176
+ if (changes.groupBy !== undefined)
177
+ updates.groupBy = changes.groupBy || null;
178
+ if (changes.sortBy !== undefined)
179
+ updates.sortBy = changes.sortBy || null;
180
+ this.ctx.drizzle
181
+ .update(pmoBoardViews)
182
+ .set(updates)
183
+ .where(eq(pmoBoardViews.id, id))
184
+ .run();
166
185
  return (await this.getBoardView(id));
167
186
  }
168
187
  /**
169
188
  * Delete a board view.
170
189
  */
171
190
  async deleteBoardView(id) {
172
- const result = this.ctx.db.prepare(`DELETE FROM ${T.board_views} WHERE id = ?`).run(id);
191
+ const result = this.ctx.drizzle
192
+ .delete(pmoBoardViews)
193
+ .where(eq(pmoBoardViews.id, id))
194
+ .run();
173
195
  if (result.changes === 0) {
174
196
  throw new PMOError('NOT_FOUND', `Board view not found: ${id}`);
175
197
  }
@@ -178,9 +200,11 @@ export class ViewStorage {
178
200
  * Get the default board view for a project.
179
201
  */
180
202
  async getDefaultBoardView(projectId) {
181
- const row = this.ctx.db.prepare(`
182
- SELECT * FROM ${T.board_views} WHERE project_id = ? AND is_default = 1
183
- `).get(projectId);
203
+ const row = this.ctx.drizzle
204
+ .select()
205
+ .from(pmoBoardViews)
206
+ .where(and(eq(pmoBoardViews.projectId, projectId), eq(pmoBoardViews.isDefault, true)))
207
+ .get();
184
208
  if (!row)
185
209
  return null;
186
210
  return this.rowToBoardView(row);
@@ -208,22 +232,32 @@ export class ViewStorage {
208
232
  // Override with explicit filters if provided
209
233
  const effectiveFilters = { ...viewFilters, ...filters };
210
234
  // Get project metadata using resolved ID
211
- const projectRow = this.ctx.db.prepare(`SELECT * FROM ${T.projects} WHERE id = ?`).get(resolvedId);
235
+ const projectRow = this.ctx.drizzle
236
+ .select({
237
+ id: pmoProjects.id,
238
+ name: pmoProjects.name,
239
+ workflowId: pmoProjects.workflowId,
240
+ updatedAt: pmoProjects.updatedAt,
241
+ })
242
+ .from(pmoProjects)
243
+ .where(eq(pmoProjects.id, resolvedId))
244
+ .get();
212
245
  if (!projectRow) {
213
246
  throw new PMOError('NOT_FOUND', `Project not found: ${projectIdOrName}. Run init() first.`);
214
247
  }
215
- // Get the project's workflow
216
- const projectMeta = this.ctx.db.prepare(`
217
- SELECT workflow_id FROM ${T.projects} WHERE id = ?
218
- `).get(resolvedId);
219
- const workflowId = projectMeta?.workflow_id || 'default';
248
+ const workflowId = projectRow.workflowId || 'default';
220
249
  // Get workflow statuses as columns
221
- const columnRows = this.ctx.db.prepare(`
222
- SELECT id, workflow_id, name, position
223
- FROM ${T.workflow_statuses}
224
- WHERE workflow_id = ?
225
- ORDER BY position
226
- `).all(workflowId);
250
+ const columnRows = this.ctx.drizzle
251
+ .select({
252
+ id: pmoWorkflowStatuses.id,
253
+ workflowId: pmoWorkflowStatuses.workflowId,
254
+ name: pmoWorkflowStatuses.name,
255
+ position: pmoWorkflowStatuses.position,
256
+ })
257
+ .from(pmoWorkflowStatuses)
258
+ .where(eq(pmoWorkflowStatuses.workflowId, workflowId))
259
+ .orderBy(asc(pmoWorkflowStatuses.position))
260
+ .all();
227
261
  // Filter columns if columnIds filter is set
228
262
  const filteredColumnRows = effectiveFilters.columnIds?.length
229
263
  ? columnRows.filter((col) => effectiveFilters.columnIds.includes(col.id))
@@ -244,82 +278,101 @@ export class ViewStorage {
244
278
  id: projectRow.id,
245
279
  name: projectRow.name,
246
280
  columns,
247
- updatedAt: new Date(projectRow.updated_at),
281
+ updatedAt: new Date(projectRow.updatedAt),
248
282
  };
249
283
  }
250
284
  /**
251
285
  * Get tickets for a column (workflow status) with filters applied.
252
286
  */
253
287
  async getTicketsForColumnWithFilters(columnId, projectId, filters) {
254
- // columnId is now a workflow_status id
255
- let sql = `
256
- SELECT t.*,
257
- ws.position as board_position,
258
- ws.name as column_name,
259
- ws.name as status_name,
260
- ws.category as status_category
261
- FROM ${T.tickets} t
262
- LEFT JOIN ${T.workflow_statuses} ws ON t.status_id = ws.id
263
- WHERE t.status_id = ? AND t.project_id = ?
264
- `;
265
- const params = [columnId, projectId];
288
+ // Build conditions array for dynamic WHERE clause
289
+ const conditions = [
290
+ eq(pmoTickets.statusId, columnId),
291
+ eq(pmoTickets.projectId, projectId),
292
+ ];
266
293
  // Apply filters
267
294
  if (filters.assignee !== undefined) {
268
295
  if (filters.assignee === 'unassigned') {
269
- sql += ' AND (t.assignee IS NULL OR t.assignee = "")';
296
+ conditions.push(or(sql `${pmoTickets.assignee} IS NULL`, eq(pmoTickets.assignee, '')));
270
297
  }
271
298
  else {
272
- sql += ' AND t.assignee = ?';
273
- params.push(filters.assignee);
299
+ conditions.push(eq(pmoTickets.assignee, filters.assignee));
274
300
  }
275
301
  }
276
302
  if (filters.owner !== undefined) {
277
- sql += ' AND t.owner = ?';
278
- params.push(filters.owner);
303
+ conditions.push(eq(pmoTickets.owner, filters.owner));
279
304
  }
280
305
  if (filters.priority !== undefined) {
281
- sql += ' AND UPPER(t.priority) = UPPER(?)';
282
- params.push(filters.priority);
306
+ conditions.push(sql `UPPER(${pmoTickets.priority}) = UPPER(${filters.priority})`);
283
307
  }
284
308
  if (filters.statusCategory !== undefined) {
285
- sql += ' AND ws.category = ?';
286
- params.push(filters.statusCategory);
309
+ conditions.push(eq(pmoWorkflowStatuses.category, filters.statusCategory));
287
310
  }
288
311
  if (filters.statusId !== undefined) {
289
- sql += ' AND t.status_id = ?';
290
- params.push(filters.statusId);
312
+ conditions.push(eq(pmoTickets.statusId, filters.statusId));
291
313
  }
292
314
  if (filters.epicId !== undefined) {
293
- sql += ' AND t.epic_id = ?';
294
- params.push(filters.epicId);
315
+ conditions.push(eq(pmoTickets.epicId, filters.epicId));
295
316
  }
296
317
  if (filters.search !== undefined) {
297
- sql += ' AND (t.title LIKE ? OR t.description LIKE ?)';
298
318
  const searchTerm = `%${filters.search}%`;
299
- params.push(searchTerm, searchTerm);
319
+ conditions.push(or(like(pmoTickets.title, searchTerm), like(pmoTickets.description, searchTerm)));
300
320
  }
301
- // Order by ticket position, then created_at as tiebreaker
302
- sql += ` ORDER BY t.position ASC, t.created_at ASC`;
303
- const rows = this.ctx.db.prepare(sql).all(...params);
321
+ const rows = this.ctx.drizzle
322
+ .select({
323
+ id: pmoTickets.id,
324
+ project_id: pmoTickets.projectId,
325
+ title: pmoTickets.title,
326
+ description: pmoTickets.description,
327
+ priority: pmoTickets.priority,
328
+ category: pmoTickets.category,
329
+ status: pmoTickets.status,
330
+ status_id: pmoTickets.statusId,
331
+ owner: pmoTickets.owner,
332
+ assignee: pmoTickets.assignee,
333
+ branch: pmoTickets.branch,
334
+ spec_id: pmoTickets.specId,
335
+ epic_id: pmoTickets.epicId,
336
+ labels: pmoTickets.labels,
337
+ position: pmoTickets.position,
338
+ created_at: pmoTickets.createdAt,
339
+ updated_at: pmoTickets.updatedAt,
340
+ last_synced_from_spec: pmoTickets.lastSyncedFromSpec,
341
+ last_synced_from_board: pmoTickets.lastSyncedFromBoard,
342
+ board_position: pmoWorkflowStatuses.position,
343
+ column_name: pmoWorkflowStatuses.name,
344
+ status_name: pmoWorkflowStatuses.name,
345
+ status_category: pmoWorkflowStatuses.category,
346
+ })
347
+ .from(pmoTickets)
348
+ .leftJoin(pmoWorkflowStatuses, eq(pmoTickets.statusId, pmoWorkflowStatuses.id))
349
+ .where(and(...conditions))
350
+ .orderBy(asc(pmoTickets.position), asc(pmoTickets.createdAt))
351
+ .all();
304
352
  return Promise.all(rows.map((row) => this.rowToTicketWithColumn(row)));
305
353
  }
306
354
  async rowToTicketWithColumn(row) {
307
355
  // Get subtasks
308
- const subtaskRows = this.ctx.db.prepare(`
309
- SELECT * FROM ${T.subtasks} WHERE ticket_id = ? ORDER BY position
310
- `).all(row.id);
356
+ const subtaskRows = this.ctx.drizzle
357
+ .select()
358
+ .from(pmoSubtasks)
359
+ .where(eq(pmoSubtasks.ticketId, row.id))
360
+ .orderBy(asc(pmoSubtasks.position))
361
+ .all();
311
362
  const subtasks = subtaskRows.map((s) => ({
312
363
  id: s.id,
313
364
  title: s.title,
314
- done: s.done === 1,
365
+ done: s.done ?? false,
315
366
  }));
316
367
  // Get metadata
317
- const metadataRows = this.ctx.db.prepare(`
318
- SELECT key, value FROM ${T.ticket_metadata} WHERE ticket_id = ?
319
- `).all(row.id);
368
+ const metadataRows = this.ctx.drizzle
369
+ .select({ key: pmoTicketMetadata.key, value: pmoTicketMetadata.value })
370
+ .from(pmoTicketMetadata)
371
+ .where(eq(pmoTicketMetadata.ticketId, row.id))
372
+ .all();
320
373
  const metadata = {};
321
374
  for (const m of metadataRows) {
322
- metadata[m.key] = m.value;
375
+ metadata[m.key] = m.value || '';
323
376
  }
324
377
  // Parse labels from JSON
325
378
  let labels = [];
@@ -347,9 +400,9 @@ export class ViewStorage {
347
400
  subtasks,
348
401
  labels,
349
402
  metadata,
350
- acceptanceCriteria: getAcceptanceCriteriaSync(this.ctx.db, row.id),
351
- createdAt: new Date(row.created_at),
352
- updatedAt: new Date(row.updated_at),
403
+ acceptanceCriteria: getAcceptanceCriteriaSync(this.ctx.drizzle, row.id),
404
+ createdAt: new Date(row.created_at || Date.now()),
405
+ updatedAt: new Date(row.updated_at || Date.now()),
353
406
  lastSyncedFromSpec: row.last_synced_from_spec
354
407
  ? new Date(row.last_synced_from_spec)
355
408
  : undefined,
@@ -388,15 +441,15 @@ export class ViewStorage {
388
441
  rowToBoardView(row) {
389
442
  return {
390
443
  id: row.id,
391
- projectId: row.project_id,
444
+ projectId: row.projectId,
392
445
  name: row.name,
393
446
  description: row.description || undefined,
394
- isDefault: row.is_default === 1,
447
+ isDefault: row.isDefault ?? false,
395
448
  filters: row.filters ? JSON.parse(row.filters) : {},
396
- groupBy: row.group_by,
397
- sortBy: row.sort_by,
398
- createdAt: new Date(row.created_at),
399
- updatedAt: new Date(row.updated_at),
449
+ groupBy: row.groupBy,
450
+ sortBy: row.sortBy,
451
+ createdAt: new Date(row.createdAt || Date.now()),
452
+ updatedAt: new Date(row.updatedAt || Date.now()),
400
453
  };
401
454
  }
402
455
  }
@@ -86,6 +86,8 @@ export interface OutputMetadata {
86
86
  flags: Record<string, unknown>;
87
87
  /** Timestamp of the output */
88
88
  timestamp?: string;
89
+ /** Resolved PR mode after flag precedence ('create-pr' | 'no-pr') */
90
+ resolvedPRMode?: string;
89
91
  }
90
92
  /**
91
93
  * JSON output when a prompt would be shown
@@ -243,6 +245,9 @@ export interface MachineOutputFlags {
243
245
  * - stdout is not a TTY (e.g., piped output)
244
246
  * - PRLT_JSON=1 environment variable is set (overrides TTY detection)
245
247
  *
248
+ * Returns false if:
249
+ * - PRLT_FORCE_TEXT=1 is set (forces text output in non-TTY environments, useful for testing)
250
+ *
246
251
  * @returns true if either stdin or stdout is not a TTY, or PRLT_JSON=1 is set
247
252
  */
248
253
  export declare function isNonTTY(): boolean;
@@ -149,9 +149,18 @@ export function validateJsonEnvelope(obj) {
149
149
  * - stdout is not a TTY (e.g., piped output)
150
150
  * - PRLT_JSON=1 environment variable is set (overrides TTY detection)
151
151
  *
152
+ * Returns false if:
153
+ * - PRLT_FORCE_TEXT=1 is set (forces text output in non-TTY environments, useful for testing)
154
+ *
152
155
  * @returns true if either stdin or stdout is not a TTY, or PRLT_JSON=1 is set
153
156
  */
154
157
  export function isNonTTY() {
158
+ // PRLT_FORCE_TEXT overrides non-TTY detection, forcing human-readable text output.
159
+ // Used in E2E tests where execSync creates a non-TTY child process but tests
160
+ // assert on styled text output.
161
+ if (process.env.PRLT_FORCE_TEXT === '1' || process.env.PRLT_FORCE_TEXT === 'true') {
162
+ return false;
163
+ }
155
164
  if (process.env.PRLT_JSON === '1' || process.env.PRLT_JSON === 'true') {
156
165
  return true;
157
166
  }