@proletariat/cli 0.3.35 → 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 (148) hide show
  1. package/README.md +37 -2
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/agent/auth.d.ts +12 -2
  4. package/dist/commands/agent/auth.js +128 -4
  5. package/dist/commands/agent/list.js +16 -7
  6. package/dist/commands/agent/status.js +32 -4
  7. package/dist/commands/board/watch.js +6 -0
  8. package/dist/commands/branch/list.d.ts +1 -0
  9. package/dist/commands/branch/list.js +43 -12
  10. package/dist/commands/branch/where.js +9 -19
  11. package/dist/commands/category/list.d.ts +2 -1
  12. package/dist/commands/category/list.js +38 -13
  13. package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
  14. package/dist/commands/{claude.js → claude/index.js} +12 -12
  15. package/dist/commands/claude/open.d.ts +13 -0
  16. package/dist/commands/claude/open.js +175 -0
  17. package/dist/commands/diet.js +18 -2
  18. package/dist/commands/docker/logs.js +7 -3
  19. package/dist/commands/docker/shell.js +6 -0
  20. package/dist/commands/docker/start.js +20 -4
  21. package/dist/commands/docker/sync.d.ts +4 -0
  22. package/dist/commands/docker/sync.js +30 -2
  23. package/dist/commands/epic/show.d.ts +13 -0
  24. package/dist/commands/epic/show.js +16 -0
  25. package/dist/commands/epic/ticket.js +7 -24
  26. package/dist/commands/epic/view.js +27 -0
  27. package/dist/commands/execution/config.d.ts +0 -4
  28. package/dist/commands/execution/config.js +14 -46
  29. package/dist/commands/execution/index.js +2 -1
  30. package/dist/commands/execution/logs.js +7 -1
  31. package/dist/commands/execution/stop.js +2 -1
  32. package/dist/commands/execution/view.js +30 -26
  33. package/dist/commands/init.js +2 -19
  34. package/dist/commands/label/create.js +2 -1
  35. package/dist/commands/label/delete.js +2 -1
  36. package/dist/commands/label/group/create.js +2 -1
  37. package/dist/commands/label/group/list.js +2 -1
  38. package/dist/commands/label/list.js +2 -1
  39. package/dist/commands/mcp-server.js +27 -1
  40. package/dist/commands/phase/template/list.js +2 -1
  41. package/dist/commands/pmo/init.js +12 -40
  42. package/dist/commands/project/create.js +3 -4
  43. package/dist/commands/project/update.js +5 -6
  44. package/dist/commands/pull.js +24 -0
  45. package/dist/commands/qa/index.d.ts +54 -0
  46. package/dist/commands/qa/index.js +762 -0
  47. package/dist/commands/repo/view.js +2 -8
  48. package/dist/commands/session/attach.js +4 -4
  49. package/dist/commands/session/create.d.ts +19 -0
  50. package/dist/commands/session/create.js +102 -0
  51. package/dist/commands/session/health.js +4 -23
  52. package/dist/commands/session/index.js +14 -1
  53. package/dist/commands/session/list.js +9 -8
  54. package/dist/commands/session/peek.d.ts +38 -0
  55. package/dist/commands/session/peek.js +316 -0
  56. package/dist/commands/session/poke.d.ts +27 -0
  57. package/dist/commands/session/poke.js +219 -0
  58. package/dist/commands/spec/view.js +29 -0
  59. package/dist/commands/template/list.js +2 -1
  60. package/dist/commands/theme/add-names.d.ts +4 -0
  61. package/dist/commands/theme/add-names.js +11 -1
  62. package/dist/commands/theme/create.d.ts +2 -0
  63. package/dist/commands/theme/create.js +8 -0
  64. package/dist/commands/ticket/bulk.js +2 -2
  65. package/dist/commands/ticket/complete.js +2 -2
  66. package/dist/commands/ticket/create.js +21 -0
  67. package/dist/commands/ticket/delete.js +8 -0
  68. package/dist/commands/ticket/edit.js +25 -0
  69. package/dist/commands/ticket/epic.js +17 -43
  70. package/dist/commands/ticket/index.js +2 -2
  71. package/dist/commands/ticket/move.js +25 -2
  72. package/dist/commands/ticket/resolve.js +3 -4
  73. package/dist/commands/ticket/show.d.ts +13 -0
  74. package/dist/commands/ticket/show.js +16 -0
  75. package/dist/commands/ticket/template/list.js +2 -1
  76. package/dist/commands/ticket/view.d.ts +0 -1
  77. package/dist/commands/ticket/view.js +30 -1
  78. package/dist/commands/work/index.js +4 -0
  79. package/dist/commands/work/spawn-all.js +1 -1
  80. package/dist/commands/work/spawn.js +15 -4
  81. package/dist/commands/work/start.js +186 -103
  82. package/dist/commands/work/status.d.ts +14 -0
  83. package/dist/commands/work/status.js +60 -0
  84. package/dist/commands/work/watch.js +1 -1
  85. package/dist/commands/workflow/index.js +2 -1
  86. package/dist/commands/workflow/show.d.ts +13 -0
  87. package/dist/commands/workflow/show.js +16 -0
  88. package/dist/commands/workspace/add.js +15 -0
  89. package/dist/commands/workspace/list.js +2 -1
  90. package/dist/commands/workspace/prune.js +7 -7
  91. package/dist/hooks/init.js +10 -2
  92. package/dist/lib/agents/commands.d.ts +5 -0
  93. package/dist/lib/agents/commands.js +143 -97
  94. package/dist/lib/branch/index.d.ts +1 -0
  95. package/dist/lib/database/drizzle-schema.d.ts +465 -0
  96. package/dist/lib/database/drizzle-schema.js +53 -0
  97. package/dist/lib/database/index.d.ts +47 -1
  98. package/dist/lib/database/index.js +138 -20
  99. package/dist/lib/execution/config.d.ts +15 -1
  100. package/dist/lib/execution/config.js +28 -0
  101. package/dist/lib/execution/runners.d.ts +45 -0
  102. package/dist/lib/execution/runners.js +187 -26
  103. package/dist/lib/execution/session-utils.d.ts +16 -1
  104. package/dist/lib/execution/session-utils.js +71 -4
  105. package/dist/lib/execution/spawner.js +15 -2
  106. package/dist/lib/execution/storage.d.ts +6 -1
  107. package/dist/lib/execution/storage.js +35 -5
  108. package/dist/lib/execution/types.d.ts +3 -0
  109. package/dist/lib/mcp/tools/board.js +4 -6
  110. package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
  111. package/dist/lib/mcp/tools/epic.js +8 -3
  112. package/dist/lib/mcp/tools/index.d.ts +1 -0
  113. package/dist/lib/mcp/tools/index.js +1 -0
  114. package/dist/lib/mcp/tools/spec.js +1 -1
  115. package/dist/lib/mcp/tools/ticket.js +11 -9
  116. package/dist/lib/mcp/tools/tmux.d.ts +16 -0
  117. package/dist/lib/mcp/tools/tmux.js +182 -0
  118. package/dist/lib/mcp/tools/work.js +148 -6
  119. package/dist/lib/mcp/types.d.ts +10 -0
  120. package/dist/lib/multiline-input.js +2 -1
  121. package/dist/lib/pmo/base-command.js +4 -4
  122. package/dist/lib/pmo/schema.d.ts +1 -1
  123. package/dist/lib/pmo/schema.js +1 -0
  124. package/dist/lib/pmo/storage/actions.js +1 -1
  125. package/dist/lib/pmo/storage/base.js +402 -50
  126. package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
  127. package/dist/lib/pmo/storage/dependencies.js +11 -3
  128. package/dist/lib/pmo/storage/epics.js +1 -1
  129. package/dist/lib/pmo/storage/helpers.d.ts +4 -4
  130. package/dist/lib/pmo/storage/helpers.js +36 -26
  131. package/dist/lib/pmo/storage/projects.d.ts +2 -0
  132. package/dist/lib/pmo/storage/projects.js +207 -119
  133. package/dist/lib/pmo/storage/specs.d.ts +2 -0
  134. package/dist/lib/pmo/storage/specs.js +274 -188
  135. package/dist/lib/pmo/storage/tickets.d.ts +2 -0
  136. package/dist/lib/pmo/storage/tickets.js +350 -290
  137. package/dist/lib/pmo/storage/types.d.ts +1 -0
  138. package/dist/lib/pmo/storage/views.d.ts +2 -0
  139. package/dist/lib/pmo/storage/views.js +183 -130
  140. package/dist/lib/prompt-command.d.ts +20 -0
  141. package/dist/lib/prompt-command.js +38 -2
  142. package/dist/lib/prompt-json.d.ts +41 -4
  143. package/dist/lib/prompt-json.js +138 -7
  144. package/dist/lib/styles.d.ts +37 -0
  145. package/dist/lib/styles.js +73 -0
  146. package/oclif.manifest.json +4046 -3385
  147. package/package.json +11 -6
  148. package/LICENSE +0 -190
@@ -169,6 +169,7 @@ export interface WorkActionRow {
169
169
  prompt: string;
170
170
  end_prompt: string | null;
171
171
  default_category: string | null;
172
+ modifies_code: number;
172
173
  is_builtin: number;
173
174
  position: number;
174
175
  created_at: string;
@@ -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
  }
@@ -30,6 +30,26 @@ import { type JsonFlags } from './prompt-json.js';
30
30
  * ```
31
31
  */
32
32
  export declare abstract class PromptCommand extends Command {
33
+ /**
34
+ * TTY-aware log method - strips ANSI codes and emoji in non-TTY mode.
35
+ *
36
+ * Use this instead of this.log() when outputting styled text (chalk colors, emoji prefixes).
37
+ * In TTY mode, outputs styled text as-is. In non-TTY mode, strips ANSI and emoji.
38
+ *
39
+ * @param message - The styled message (may contain ANSI codes and emoji)
40
+ * @param args - Additional arguments passed to this.log()
41
+ */
42
+ protected logPlain(message?: string, ...args: string[]): void;
43
+ /**
44
+ * Check if plain output mode is active (non-TTY, PRLT_PLAIN, NO_COLOR).
45
+ * Convenience wrapper for use in commands.
46
+ */
47
+ protected get isPlain(): boolean;
48
+ /**
49
+ * Check if running in non-TTY environment.
50
+ * Convenience wrapper for use in commands.
51
+ */
52
+ protected get isNonTTY(): boolean;
33
53
  /**
34
54
  * Prompt wrapper - drop-in replacement for inquirer.prompt
35
55
  *
@@ -1,6 +1,7 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
- import { isAgentMode, outputPromptAsJson, createMetadata, normalizeChoices, } from './prompt-json.js';
3
+ import { isAgentMode, isNonTTY, outputPromptAsJson, createMetadata, normalizeChoices, } from './prompt-json.js';
4
+ import { isPlainOutput, plainText } from './styles.js';
4
5
  /**
5
6
  * Lightweight base command with prompt() method for JSON mode support.
6
7
  *
@@ -31,6 +32,41 @@ import { isAgentMode, outputPromptAsJson, createMetadata, normalizeChoices, } fr
31
32
  * ```
32
33
  */
33
34
  export class PromptCommand extends Command {
35
+ /**
36
+ * TTY-aware log method - strips ANSI codes and emoji in non-TTY mode.
37
+ *
38
+ * Use this instead of this.log() when outputting styled text (chalk colors, emoji prefixes).
39
+ * In TTY mode, outputs styled text as-is. In non-TTY mode, strips ANSI and emoji.
40
+ *
41
+ * @param message - The styled message (may contain ANSI codes and emoji)
42
+ * @param args - Additional arguments passed to this.log()
43
+ */
44
+ logPlain(message, ...args) {
45
+ if (message === undefined) {
46
+ this.log();
47
+ return;
48
+ }
49
+ if (isPlainOutput()) {
50
+ this.log(plainText(message), ...args);
51
+ }
52
+ else {
53
+ this.log(message, ...args);
54
+ }
55
+ }
56
+ /**
57
+ * Check if plain output mode is active (non-TTY, PRLT_PLAIN, NO_COLOR).
58
+ * Convenience wrapper for use in commands.
59
+ */
60
+ get isPlain() {
61
+ return isPlainOutput();
62
+ }
63
+ /**
64
+ * Check if running in non-TTY environment.
65
+ * Convenience wrapper for use in commands.
66
+ */
67
+ get isNonTTY() {
68
+ return isNonTTY();
69
+ }
34
70
  /**
35
71
  * Prompt wrapper - drop-in replacement for inquirer.prompt
36
72
  *
@@ -73,7 +109,7 @@ export class PromptCommand extends Command {
73
109
  */
74
110
  async prompt(questions, jsonModeConfig) {
75
111
  // Auto-detect non-TTY: switch to JSON mode when no TTY present
76
- if (!jsonModeConfig && !process.stdin.isTTY) {
112
+ if (!jsonModeConfig && isNonTTY()) {
77
113
  jsonModeConfig = { flags: { json: true }, commandName: this.id ?? 'unknown' };
78
114
  }
79
115
  // Check for JSON/agent mode
@@ -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
@@ -193,6 +195,27 @@ export interface ExecutionResultJsonOutput {
193
195
  * Union type for all JSON output types
194
196
  */
195
197
  export type JsonOutput = PromptJsonOutput | SuccessJsonOutput | ErrorJsonOutput | DryRunJsonOutput | ConfirmationNeededJsonOutput | ExecutionResultJsonOutput;
198
+ /**
199
+ * All valid JSON envelope type discriminators.
200
+ * Used for contract tests and schema validation.
201
+ */
202
+ export declare const JSON_ENVELOPE_TYPES: readonly ["prompt", "success", "error", "dry-run", "confirmation_needed", "execution_result"];
203
+ export type JsonEnvelopeType = typeof JSON_ENVELOPE_TYPES[number];
204
+ /**
205
+ * Required fields per envelope type for contract validation.
206
+ * Tests use this to verify no fields are accidentally removed.
207
+ */
208
+ export declare const JSON_ENVELOPE_REQUIRED_FIELDS: Record<JsonEnvelopeType, string[]>;
209
+ /**
210
+ * Validate that a parsed JSON object conforms to the machine-mode envelope schema.
211
+ *
212
+ * Returns an array of validation errors (empty = valid).
213
+ * Useful for contract tests and runtime validation of JSON output.
214
+ *
215
+ * @param obj - Parsed JSON object to validate
216
+ * @returns Array of validation error strings (empty if valid)
217
+ */
218
+ export declare function validateJsonEnvelope(obj: unknown): string[];
196
219
  /**
197
220
  * Flags interface for JSON mode detection
198
221
  */
@@ -211,9 +234,21 @@ export interface MachineOutputFlags {
211
234
  machine?: boolean;
212
235
  }
213
236
  /**
214
- * Check if the current environment is non-TTY (piped output)
237
+ * Check if the current environment is non-TTY (piped input or output)
238
+ *
239
+ * Uses the "either" strategy: returns true if EITHER stdin OR stdout is non-TTY.
240
+ * This covers the primary use case of scripts/agents calling prlt as a subprocess,
241
+ * where both stdin and stdout are typically non-TTY.
242
+ *
243
+ * Returns true if:
244
+ * - stdin is not a TTY (e.g., piped input)
245
+ * - stdout is not a TTY (e.g., piped output)
246
+ * - PRLT_JSON=1 environment variable is set (overrides TTY detection)
247
+ *
248
+ * Returns false if:
249
+ * - PRLT_FORCE_TEXT=1 is set (forces text output in non-TTY environments, useful for testing)
215
250
  *
216
- * @returns true if stdout is not a TTY (e.g., piped to another process)
251
+ * @returns true if either stdin or stdout is not a TTY, or PRLT_JSON=1 is set
217
252
  */
218
253
  export declare function isNonTTY(): boolean;
219
254
  /**
@@ -221,7 +256,8 @@ export declare function isNonTTY(): boolean;
221
256
  *
222
257
  * Returns true if:
223
258
  * - The --json flag is set (or -m/--machine aliases)
224
- * - The environment is non-TTY (piped output)
259
+ * - The PRLT_JSON=1 environment variable is set
260
+ * - Either stdin or stdout is non-TTY (piped input/output)
225
261
  *
226
262
  * @param flags - Command flags object
227
263
  * @returns true if JSON mode should be used
@@ -236,7 +272,8 @@ export declare const isAgentMode: typeof shouldOutputJson;
236
272
  *
237
273
  * Returns true if:
238
274
  * - The --json flag is set (or -m/--machine aliases)
239
- * - The environment is non-TTY (piped output)
275
+ * - The PRLT_JSON=1 environment variable is set
276
+ * - Either stdin or stdout is non-TTY (piped input/output)
240
277
  *
241
278
  * @param flags - Command flags object
242
279
  * @returns true if machine-readable output mode should be used