@proletariat/cli 0.3.34 → 0.3.36

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 (198) hide show
  1. package/dist/commands/agent/auth.d.ts +15 -3
  2. package/dist/commands/agent/auth.js +136 -15
  3. package/dist/commands/agent/index.js +11 -2
  4. package/dist/commands/agent/list.js +16 -7
  5. package/dist/commands/agent/staff/add.d.ts +1 -0
  6. package/dist/commands/agent/staff/add.js +1 -0
  7. package/dist/commands/agent/staff/index.d.ts +15 -0
  8. package/dist/commands/agent/staff/index.js +83 -0
  9. package/dist/commands/agent/staff/list.d.ts +1 -0
  10. package/dist/commands/agent/staff/list.js +1 -0
  11. package/dist/commands/agent/staff/remove.d.ts +1 -0
  12. package/dist/commands/agent/staff/remove.js +1 -0
  13. package/dist/commands/agent/status.js +32 -4
  14. package/dist/commands/agent/themes/add-names.d.ts +1 -0
  15. package/dist/commands/agent/themes/add-names.js +1 -0
  16. package/dist/commands/agent/themes/create.d.ts +1 -0
  17. package/dist/commands/agent/themes/create.js +1 -0
  18. package/dist/commands/agent/themes/index.d.ts +10 -0
  19. package/dist/commands/agent/themes/index.js +144 -0
  20. package/dist/commands/agent/themes/list.d.ts +1 -0
  21. package/dist/commands/agent/themes/list.js +1 -0
  22. package/dist/commands/agent/themes/set.d.ts +1 -0
  23. package/dist/commands/agent/themes/set.js +1 -0
  24. package/dist/commands/agents/themes/add-names.d.ts +1 -0
  25. package/dist/commands/agents/themes/add-names.js +1 -0
  26. package/dist/commands/agents/themes/create.d.ts +1 -0
  27. package/dist/commands/agents/themes/create.js +1 -0
  28. package/dist/commands/agents/themes/list.d.ts +1 -0
  29. package/dist/commands/agents/themes/list.js +1 -0
  30. package/dist/commands/board/watch.js +6 -0
  31. package/dist/commands/branch/list.d.ts +1 -0
  32. package/dist/commands/branch/list.js +43 -12
  33. package/dist/commands/branch/where.js +3 -2
  34. package/dist/commands/category/list.d.ts +2 -1
  35. package/dist/commands/category/list.js +38 -13
  36. package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
  37. package/dist/commands/{claude.js → claude/index.js} +12 -12
  38. package/dist/commands/claude/open.d.ts +13 -0
  39. package/dist/commands/claude/open.js +175 -0
  40. package/dist/commands/diet.js +18 -2
  41. package/dist/commands/docker/logs.js +7 -3
  42. package/dist/commands/docker/shell.js +6 -0
  43. package/dist/commands/docker/start.js +20 -4
  44. package/dist/commands/docker/sync.d.ts +4 -0
  45. package/dist/commands/docker/sync.js +30 -2
  46. package/dist/commands/epic/show.d.ts +13 -0
  47. package/dist/commands/epic/show.js +16 -0
  48. package/dist/commands/epic/view.js +27 -0
  49. package/dist/commands/execution/config.d.ts +0 -4
  50. package/dist/commands/execution/config.js +10 -32
  51. package/dist/commands/execution/index.js +2 -1
  52. package/dist/commands/execution/logs.js +1 -1
  53. package/dist/commands/execution/stop.js +2 -1
  54. package/dist/commands/execution/view.js +22 -26
  55. package/dist/commands/init.js +2 -19
  56. package/dist/commands/label/create.d.ts +20 -0
  57. package/dist/commands/label/create.js +57 -0
  58. package/dist/commands/label/delete.d.ts +17 -0
  59. package/dist/commands/label/delete.js +32 -0
  60. package/dist/commands/label/group/create.d.ts +20 -0
  61. package/dist/commands/label/group/create.js +55 -0
  62. package/dist/commands/label/group/list.d.ts +14 -0
  63. package/dist/commands/label/group/list.js +52 -0
  64. package/dist/commands/label/index.d.ts +15 -0
  65. package/dist/commands/label/index.js +58 -0
  66. package/dist/commands/label/list.d.ts +16 -0
  67. package/dist/commands/label/list.js +83 -0
  68. package/dist/commands/link/list.js +3 -2
  69. package/dist/commands/mcp-server.js +27 -1
  70. package/dist/commands/phase/template/apply.d.ts +26 -0
  71. package/dist/commands/phase/template/apply.js +14 -0
  72. package/dist/commands/phase/template/create.d.ts +23 -0
  73. package/dist/commands/phase/template/create.js +14 -0
  74. package/dist/commands/phase/template/delete.d.ts +18 -0
  75. package/dist/commands/phase/template/delete.js +61 -0
  76. package/dist/commands/phase/template/list.d.ts +17 -0
  77. package/dist/commands/phase/template/list.js +89 -0
  78. package/dist/commands/phase/template/update.d.ts +1 -0
  79. package/dist/commands/phase/template/update.js +1 -0
  80. package/dist/commands/priority/add.js +1 -1
  81. package/dist/commands/project/create.js +3 -4
  82. package/dist/commands/project/update.js +5 -8
  83. package/dist/commands/pull.js +24 -0
  84. package/dist/commands/roadmap/generate.js +1 -2
  85. package/dist/commands/session/create.d.ts +19 -0
  86. package/dist/commands/session/create.js +102 -0
  87. package/dist/commands/session/health.js +2 -21
  88. package/dist/commands/session/index.js +14 -1
  89. package/dist/commands/session/list.js +26 -7
  90. package/dist/commands/session/peek.d.ts +38 -0
  91. package/dist/commands/session/peek.js +316 -0
  92. package/dist/commands/session/poke.d.ts +27 -0
  93. package/dist/commands/session/poke.js +219 -0
  94. package/dist/commands/spec/link/depends.d.ts +18 -0
  95. package/dist/commands/spec/link/depends.js +86 -0
  96. package/dist/commands/spec/link/index.d.ts +17 -0
  97. package/dist/commands/spec/link/index.js +92 -0
  98. package/dist/commands/spec/link/remove.d.ts +18 -0
  99. package/dist/commands/spec/link/remove.js +90 -0
  100. package/dist/commands/spec/view.js +29 -0
  101. package/dist/commands/support/logs.js +2 -2
  102. package/dist/commands/template/apply.js +5 -4
  103. package/dist/commands/template/create.js +1 -1
  104. package/dist/commands/template/list.js +2 -1
  105. package/dist/commands/theme/add-names.d.ts +4 -0
  106. package/dist/commands/theme/add-names.js +11 -1
  107. package/dist/commands/theme/create.d.ts +2 -0
  108. package/dist/commands/theme/create.js +8 -0
  109. package/dist/commands/ticket/bulk.js +2 -2
  110. package/dist/commands/ticket/complete.js +2 -2
  111. package/dist/commands/ticket/create.js +21 -0
  112. package/dist/commands/ticket/delete.js +8 -0
  113. package/dist/commands/ticket/edit.js +25 -0
  114. package/dist/commands/ticket/index.js +2 -2
  115. package/dist/commands/ticket/link/block.d.ts +15 -0
  116. package/dist/commands/ticket/link/block.js +95 -0
  117. package/dist/commands/ticket/link/index.d.ts +14 -0
  118. package/dist/commands/ticket/link/index.js +96 -0
  119. package/dist/commands/ticket/list.d.ts +1 -0
  120. package/dist/commands/ticket/list.js +6 -0
  121. package/dist/commands/ticket/move.js +25 -2
  122. package/dist/commands/ticket/resolve.js +4 -5
  123. package/dist/commands/ticket/show.d.ts +13 -0
  124. package/dist/commands/ticket/show.js +16 -0
  125. package/dist/commands/ticket/template/apply.d.ts +26 -0
  126. package/dist/commands/ticket/template/apply.js +14 -0
  127. package/dist/commands/ticket/template/delete.d.ts +18 -0
  128. package/dist/commands/ticket/template/delete.js +61 -0
  129. package/dist/commands/ticket/template/list.d.ts +17 -0
  130. package/dist/commands/ticket/template/list.js +78 -0
  131. package/dist/commands/ticket/template/save.d.ts +17 -0
  132. package/dist/commands/ticket/template/save.js +97 -0
  133. package/dist/commands/ticket/view.js +30 -0
  134. package/dist/commands/work/index.js +4 -0
  135. package/dist/commands/work/ready.js +17 -0
  136. package/dist/commands/work/resolve.js +1 -1
  137. package/dist/commands/work/spawn.js +4 -4
  138. package/dist/commands/work/start.d.ts +1 -0
  139. package/dist/commands/work/start.js +203 -93
  140. package/dist/commands/work/status.d.ts +14 -0
  141. package/dist/commands/work/status.js +60 -0
  142. package/dist/commands/workflow/index.js +2 -1
  143. package/dist/commands/workflow/show.d.ts +13 -0
  144. package/dist/commands/workflow/show.js +16 -0
  145. package/dist/commands/workspace/add.js +15 -0
  146. package/dist/commands/workspace/list.js +2 -1
  147. package/dist/commands/workspace/prune.js +5 -5
  148. package/dist/lib/branch/index.d.ts +1 -0
  149. package/dist/lib/database/index.d.ts +1 -1
  150. package/dist/lib/database/index.js +20 -0
  151. package/dist/lib/execution/config.d.ts +15 -1
  152. package/dist/lib/execution/config.js +28 -0
  153. package/dist/lib/execution/devcontainer.js +3 -1
  154. package/dist/lib/execution/runners.d.ts +18 -2
  155. package/dist/lib/execution/runners.js +71 -29
  156. package/dist/lib/execution/session-utils.d.ts +11 -1
  157. package/dist/lib/execution/session-utils.js +26 -1
  158. package/dist/lib/execution/storage.d.ts +5 -0
  159. package/dist/lib/execution/storage.js +18 -3
  160. package/dist/lib/execution/types.d.ts +3 -0
  161. package/dist/lib/flags/resolver.js +1 -0
  162. package/dist/lib/mcp/helpers.d.ts +1 -2
  163. package/dist/lib/mcp/tools/board.js +4 -6
  164. package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
  165. package/dist/lib/mcp/tools/diet.js +1 -0
  166. package/dist/lib/mcp/tools/epic.js +8 -3
  167. package/dist/lib/mcp/tools/index.d.ts +1 -0
  168. package/dist/lib/mcp/tools/index.js +1 -0
  169. package/dist/lib/mcp/tools/label.d.ts +6 -0
  170. package/dist/lib/mcp/tools/label.js +338 -0
  171. package/dist/lib/mcp/tools/spec.js +1 -1
  172. package/dist/lib/mcp/tools/ticket.js +57 -19
  173. package/dist/lib/mcp/tools/work.js +96 -6
  174. package/dist/lib/mcp/types.d.ts +10 -0
  175. package/dist/lib/multiline-input.js +8 -19
  176. package/dist/lib/pmo/base-command.d.ts +0 -1
  177. package/dist/lib/pmo/base-command.js +4 -5
  178. package/dist/lib/pmo/schema.d.ts +6 -0
  179. package/dist/lib/pmo/schema.js +44 -0
  180. package/dist/lib/pmo/storage/actions.js +1 -1
  181. package/dist/lib/pmo/storage/base.d.ts +6 -0
  182. package/dist/lib/pmo/storage/base.js +311 -52
  183. package/dist/lib/pmo/storage/index.d.ts +23 -1
  184. package/dist/lib/pmo/storage/index.js +59 -1
  185. package/dist/lib/pmo/storage/labels.d.ts +55 -0
  186. package/dist/lib/pmo/storage/labels.js +346 -0
  187. package/dist/lib/pmo/storage/tickets.js +17 -0
  188. package/dist/lib/pmo/storage/types.d.ts +25 -0
  189. package/dist/lib/pmo/types.d.ts +44 -0
  190. package/dist/lib/pmo/utils.js +1 -1
  191. package/dist/lib/prompt-command.d.ts +20 -0
  192. package/dist/lib/prompt-command.js +38 -2
  193. package/dist/lib/prompt-json.d.ts +36 -4
  194. package/dist/lib/prompt-json.js +129 -7
  195. package/dist/lib/styles.d.ts +37 -0
  196. package/dist/lib/styles.js +73 -0
  197. package/oclif.manifest.json +6399 -3799
  198. package/package.json +1 -1
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Label storage operations for PMO.
3
+ * Handles CRUD for labels, label groups, and ticket-label associations.
4
+ * Enforces group exclusivity constraints.
5
+ */
6
+ import { PMO_TABLES } from '../schema.js';
7
+ import { PMOError } from '../types.js';
8
+ import { slugify } from '../utils.js';
9
+ const T = PMO_TABLES;
10
+ export class LabelStorage {
11
+ ctx;
12
+ constructor(ctx) {
13
+ this.ctx = ctx;
14
+ }
15
+ // ===========================================================================
16
+ // Label Group Operations
17
+ // ===========================================================================
18
+ async listLabelGroups(filter) {
19
+ let query = `SELECT * FROM ${T.label_groups} WHERE 1=1`;
20
+ const params = [];
21
+ if (filter?.search) {
22
+ query += ' AND (name LIKE ? OR description LIKE ?)';
23
+ params.push(`%${filter.search}%`, `%${filter.search}%`);
24
+ }
25
+ query += ' ORDER BY position ASC, name ASC';
26
+ const rows = this.ctx.db.prepare(query).all(...params);
27
+ return rows.map(rowToLabelGroup);
28
+ }
29
+ async getLabelGroup(id) {
30
+ const row = this.ctx.db.prepare(`SELECT * FROM ${T.label_groups} WHERE id = ?`).get(id);
31
+ return row ? rowToLabelGroup(row) : null;
32
+ }
33
+ async getLabelGroupByName(name) {
34
+ const row = this.ctx.db.prepare(`SELECT * FROM ${T.label_groups} WHERE LOWER(name) = LOWER(?)`).get(name);
35
+ return row ? rowToLabelGroup(row) : null;
36
+ }
37
+ async createLabelGroup(group) {
38
+ const id = group.id || slugify(group.name);
39
+ // Check for duplicate name
40
+ const existing = await this.getLabelGroupByName(group.name);
41
+ if (existing) {
42
+ throw new PMOError('CONFLICT', `Label group "${group.name}" already exists`);
43
+ }
44
+ // Get next position
45
+ const maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.label_groups}`).get();
46
+ const now = new Date().toISOString();
47
+ this.ctx.db.prepare(`
48
+ INSERT INTO ${T.label_groups} (id, name, description, is_exclusive, is_required, position, created_at)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)
50
+ `).run(id, group.name, group.description || null, group.isExclusive !== undefined ? (group.isExclusive ? 1 : 0) : 1, group.isRequired !== undefined ? (group.isRequired ? 1 : 0) : 0, group.position ?? maxPos.max_pos + 1, now);
51
+ return (await this.getLabelGroup(id));
52
+ }
53
+ async updateLabelGroup(id, changes) {
54
+ const existing = await this.getLabelGroup(id);
55
+ if (!existing) {
56
+ throw new PMOError('NOT_FOUND', `Label group not found: ${id}`);
57
+ }
58
+ const updates = [];
59
+ const params = [];
60
+ if (changes.name !== undefined) {
61
+ updates.push('name = ?');
62
+ params.push(changes.name);
63
+ }
64
+ if (changes.description !== undefined) {
65
+ updates.push('description = ?');
66
+ params.push(changes.description);
67
+ }
68
+ if (changes.isExclusive !== undefined) {
69
+ updates.push('is_exclusive = ?');
70
+ params.push(changes.isExclusive ? 1 : 0);
71
+ }
72
+ if (changes.isRequired !== undefined) {
73
+ updates.push('is_required = ?');
74
+ params.push(changes.isRequired ? 1 : 0);
75
+ }
76
+ if (changes.position !== undefined) {
77
+ updates.push('position = ?');
78
+ params.push(changes.position);
79
+ }
80
+ if (updates.length > 0) {
81
+ params.push(id);
82
+ this.ctx.db.prepare(`UPDATE ${T.label_groups} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
83
+ }
84
+ return (await this.getLabelGroup(id));
85
+ }
86
+ async deleteLabelGroup(id) {
87
+ const existing = await this.getLabelGroup(id);
88
+ if (!existing) {
89
+ throw new PMOError('NOT_FOUND', `Label group not found: ${id}`);
90
+ }
91
+ // Labels in this group will have group_id set to NULL (ON DELETE SET NULL)
92
+ this.ctx.db.prepare(`DELETE FROM ${T.label_groups} WHERE id = ?`).run(id);
93
+ }
94
+ // ===========================================================================
95
+ // Label Operations
96
+ // ===========================================================================
97
+ async listLabels(filter) {
98
+ let query = `
99
+ SELECT l.*, lg.name as group_name
100
+ FROM ${T.labels} l
101
+ LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
102
+ WHERE 1=1
103
+ `;
104
+ const params = [];
105
+ if (filter?.groupId) {
106
+ query += ' AND l.group_id = ?';
107
+ params.push(filter.groupId);
108
+ }
109
+ if (filter?.search) {
110
+ query += ' AND (l.name LIKE ? OR l.description LIKE ?)';
111
+ params.push(`%${filter.search}%`, `%${filter.search}%`);
112
+ }
113
+ if (filter?.isBuiltin !== undefined) {
114
+ query += ' AND l.is_builtin = ?';
115
+ params.push(filter.isBuiltin ? 1 : 0);
116
+ }
117
+ query += ' ORDER BY lg.position ASC, l.position ASC, l.name ASC';
118
+ const rows = this.ctx.db.prepare(query).all(...params);
119
+ return rows.map(rowToLabel);
120
+ }
121
+ async getLabel(id) {
122
+ const row = this.ctx.db.prepare(`
123
+ SELECT l.*, lg.name as group_name
124
+ FROM ${T.labels} l
125
+ LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
126
+ WHERE l.id = ?
127
+ `).get(id);
128
+ return row ? rowToLabel(row) : null;
129
+ }
130
+ async getLabelByName(name, groupId) {
131
+ let query = `
132
+ SELECT l.*, lg.name as group_name
133
+ FROM ${T.labels} l
134
+ LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
135
+ WHERE LOWER(l.name) = LOWER(?)
136
+ `;
137
+ const params = [name];
138
+ if (groupId) {
139
+ query += ' AND l.group_id = ?';
140
+ params.push(groupId);
141
+ }
142
+ const row = this.ctx.db.prepare(query).get(...params);
143
+ return row ? rowToLabel(row) : null;
144
+ }
145
+ async createLabel(label) {
146
+ const id = label.id || slugify(label.name);
147
+ // Validate group if provided
148
+ if (label.groupId) {
149
+ const group = await this.getLabelGroup(label.groupId);
150
+ if (!group) {
151
+ throw new PMOError('NOT_FOUND', `Label group not found: ${label.groupId}`);
152
+ }
153
+ }
154
+ // Get next position within group
155
+ let maxPos;
156
+ if (label.groupId) {
157
+ maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.labels} WHERE group_id = ?`).get(label.groupId);
158
+ }
159
+ else {
160
+ maxPos = this.ctx.db.prepare(`SELECT COALESCE(MAX(position), -1) as max_pos FROM ${T.labels} WHERE group_id IS NULL`).get();
161
+ }
162
+ const now = new Date().toISOString();
163
+ try {
164
+ this.ctx.db.prepare(`
165
+ INSERT INTO ${T.labels} (id, name, color, description, group_id, position, is_builtin, created_at)
166
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
167
+ `).run(id, label.name, label.color || null, label.description || null, label.groupId || null, label.position ?? maxPos.max_pos + 1, label.isBuiltin ? 1 : 0, now);
168
+ }
169
+ catch (err) {
170
+ if (err instanceof Error && err.message.includes('UNIQUE constraint')) {
171
+ throw new PMOError('CONFLICT', `Label "${label.name}" already exists in this group`);
172
+ }
173
+ throw err;
174
+ }
175
+ return (await this.getLabel(id));
176
+ }
177
+ async updateLabel(id, changes) {
178
+ const existing = await this.getLabel(id);
179
+ if (!existing) {
180
+ throw new PMOError('NOT_FOUND', `Label not found: ${id}`);
181
+ }
182
+ const updates = [];
183
+ const params = [];
184
+ if (changes.name !== undefined) {
185
+ updates.push('name = ?');
186
+ params.push(changes.name);
187
+ }
188
+ if (changes.color !== undefined) {
189
+ updates.push('color = ?');
190
+ params.push(changes.color);
191
+ }
192
+ if (changes.description !== undefined) {
193
+ updates.push('description = ?');
194
+ params.push(changes.description);
195
+ }
196
+ if (changes.groupId !== undefined) {
197
+ updates.push('group_id = ?');
198
+ params.push(changes.groupId);
199
+ }
200
+ if (changes.position !== undefined) {
201
+ updates.push('position = ?');
202
+ params.push(changes.position);
203
+ }
204
+ if (updates.length > 0) {
205
+ params.push(id);
206
+ this.ctx.db.prepare(`UPDATE ${T.labels} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
207
+ }
208
+ return (await this.getLabel(id));
209
+ }
210
+ async deleteLabel(id) {
211
+ const existing = await this.getLabel(id);
212
+ if (!existing) {
213
+ throw new PMOError('NOT_FOUND', `Label not found: ${id}`);
214
+ }
215
+ // ticket_labels rows cascade on delete
216
+ this.ctx.db.prepare(`DELETE FROM ${T.labels} WHERE id = ?`).run(id);
217
+ }
218
+ // ===========================================================================
219
+ // Ticket-Label Association Operations
220
+ // ===========================================================================
221
+ /**
222
+ * Add a label to a ticket.
223
+ * Enforces group exclusivity: if the label belongs to an exclusive group,
224
+ * removes any existing label from the same group first.
225
+ */
226
+ async addLabelToTicket(ticketId, labelId) {
227
+ // Validate ticket exists
228
+ const ticket = this.ctx.db.prepare(`SELECT id FROM ${T.tickets} WHERE id = ?`).get(ticketId);
229
+ if (!ticket) {
230
+ throw new PMOError('NOT_FOUND', `Ticket not found: ${ticketId}`);
231
+ }
232
+ // Get the label and its group info
233
+ const label = await this.getLabel(labelId);
234
+ if (!label) {
235
+ throw new PMOError('NOT_FOUND', `Label not found: ${labelId}`);
236
+ }
237
+ // If label belongs to an exclusive group, enforce exclusivity
238
+ if (label.groupId) {
239
+ const group = await this.getLabelGroup(label.groupId);
240
+ if (group?.isExclusive) {
241
+ // Remove any existing labels from this group on this ticket
242
+ this.ctx.db.prepare(`
243
+ DELETE FROM ${T.ticket_labels}
244
+ WHERE ticket_id = ? AND label_id IN (
245
+ SELECT id FROM ${T.labels} WHERE group_id = ?
246
+ )
247
+ `).run(ticketId, label.groupId);
248
+ }
249
+ }
250
+ // Add the label (ignore if already exists)
251
+ this.ctx.db.prepare(`
252
+ INSERT OR IGNORE INTO ${T.ticket_labels} (ticket_id, label_id)
253
+ VALUES (?, ?)
254
+ `).run(ticketId, labelId);
255
+ }
256
+ /**
257
+ * Remove a label from a ticket.
258
+ */
259
+ async removeLabelFromTicket(ticketId, labelId) {
260
+ const result = this.ctx.db.prepare(`
261
+ DELETE FROM ${T.ticket_labels}
262
+ WHERE ticket_id = ? AND label_id = ?
263
+ `).run(ticketId, labelId);
264
+ if (result.changes === 0) {
265
+ throw new PMOError('NOT_FOUND', `Label ${labelId} is not applied to ticket ${ticketId}`);
266
+ }
267
+ }
268
+ /**
269
+ * Get all labels for a ticket.
270
+ */
271
+ async getLabelsForTicket(ticketId) {
272
+ const rows = this.ctx.db.prepare(`
273
+ SELECT l.*, lg.name as group_name
274
+ FROM ${T.ticket_labels} tl
275
+ JOIN ${T.labels} l ON tl.label_id = l.id
276
+ LEFT JOIN ${T.label_groups} lg ON l.group_id = lg.id
277
+ WHERE tl.ticket_id = ?
278
+ ORDER BY lg.position ASC, l.position ASC
279
+ `).all(ticketId);
280
+ return rows.map(rowToLabel);
281
+ }
282
+ /**
283
+ * Add a label to a ticket by name.
284
+ * Resolves the label name to an ID first.
285
+ * If the label name matches a group:label pattern, resolves within that group.
286
+ */
287
+ async addLabelToTicketByName(ticketId, labelName) {
288
+ const label = await this.resolveLabelByName(labelName);
289
+ if (!label) {
290
+ throw new PMOError('NOT_FOUND', `Label not found: "${labelName}"`);
291
+ }
292
+ return this.addLabelToTicket(ticketId, label.id);
293
+ }
294
+ /**
295
+ * Remove a label from a ticket by name.
296
+ */
297
+ async removeLabelFromTicketByName(ticketId, labelName) {
298
+ const label = await this.resolveLabelByName(labelName);
299
+ if (!label) {
300
+ throw new PMOError('NOT_FOUND', `Label not found: "${labelName}"`);
301
+ }
302
+ return this.removeLabelFromTicket(ticketId, label.id);
303
+ }
304
+ /**
305
+ * Resolve a label by name or group:name pattern.
306
+ */
307
+ async resolveLabelByName(nameOrPattern) {
308
+ // Check for group:label pattern
309
+ if (nameOrPattern.includes(':')) {
310
+ const [groupName, labelName] = nameOrPattern.split(':', 2);
311
+ const group = await this.getLabelGroupByName(groupName);
312
+ if (group) {
313
+ return this.getLabelByName(labelName, group.id);
314
+ }
315
+ }
316
+ // Try direct name lookup
317
+ return this.getLabelByName(nameOrPattern);
318
+ }
319
+ }
320
+ // ===========================================================================
321
+ // Row Converters
322
+ // ===========================================================================
323
+ function rowToLabelGroup(row) {
324
+ return {
325
+ id: row.id,
326
+ name: row.name,
327
+ description: row.description || undefined,
328
+ isExclusive: row.is_exclusive === 1,
329
+ isRequired: row.is_required === 1,
330
+ position: row.position,
331
+ createdAt: new Date(row.created_at),
332
+ };
333
+ }
334
+ function rowToLabel(row) {
335
+ return {
336
+ id: row.id,
337
+ name: row.name,
338
+ color: row.color || undefined,
339
+ description: row.description || undefined,
340
+ groupId: row.group_id || undefined,
341
+ groupName: row.group_name || undefined,
342
+ position: row.position,
343
+ isBuiltin: row.is_builtin === 1,
344
+ createdAt: new Date(row.created_at),
345
+ };
346
+ }
@@ -576,6 +576,23 @@ export class TicketStorage {
576
576
  query += ' AND ws.name = ?';
577
577
  params.push(filter.column);
578
578
  }
579
+ if (filter?.label) {
580
+ query += ` AND t.id IN (
581
+ SELECT tl.ticket_id FROM ${T.ticket_labels} tl
582
+ JOIN ${T.labels} l ON tl.label_id = l.id
583
+ WHERE LOWER(l.name) = LOWER(?)
584
+ )`;
585
+ params.push(filter.label);
586
+ }
587
+ if (filter?.labelGroup) {
588
+ query += ` AND t.id IN (
589
+ SELECT tl.ticket_id FROM ${T.ticket_labels} tl
590
+ JOIN ${T.labels} l ON tl.label_id = l.id
591
+ JOIN ${T.label_groups} lg ON l.group_id = lg.id
592
+ WHERE LOWER(lg.name) = LOWER(?)
593
+ )`;
594
+ params.push(filter.labelGroup);
595
+ }
579
596
  // Order by status column position, then ticket position within status
580
597
  if (projectIdOrName === undefined) {
581
598
  query += ` ORDER BY p.name, ws.position, t.position ASC, t.created_at ASC`;
@@ -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;
@@ -226,3 +227,27 @@ export interface CategoryRow {
226
227
  is_builtin: number;
227
228
  created_at: string;
228
229
  }
230
+ export interface LabelGroupRow {
231
+ id: string;
232
+ name: string;
233
+ description: string | null;
234
+ is_exclusive: number;
235
+ is_required: number;
236
+ position: number;
237
+ created_at: string;
238
+ }
239
+ export interface LabelRow {
240
+ id: string;
241
+ name: string;
242
+ color: string | null;
243
+ description: string | null;
244
+ group_id: string | null;
245
+ group_name?: string | null;
246
+ position: number;
247
+ is_builtin: number;
248
+ created_at: string;
249
+ }
250
+ export interface TicketLabelRow {
251
+ ticket_id: string;
252
+ label_id: string;
253
+ }
@@ -248,6 +248,48 @@ export interface PhaseTemplatePhase {
248
248
  description?: string;
249
249
  isDefault?: boolean;
250
250
  }
251
+ /**
252
+ * Label group - groups related labels and optionally enforces mutual exclusivity.
253
+ * Example: "Function" group (ship/grow/support/bizops/strategy) is exclusive.
254
+ */
255
+ export interface LabelGroup {
256
+ id: string;
257
+ name: string;
258
+ description?: string;
259
+ isExclusive: boolean;
260
+ isRequired: boolean;
261
+ position: number;
262
+ createdAt: Date;
263
+ }
264
+ /**
265
+ * Label - a tag that can be applied to tickets.
266
+ * Labels can optionally belong to a group.
267
+ */
268
+ export interface Label {
269
+ id: string;
270
+ name: string;
271
+ color?: string;
272
+ description?: string;
273
+ groupId?: string;
274
+ groupName?: string;
275
+ position: number;
276
+ isBuiltin: boolean;
277
+ createdAt: Date;
278
+ }
279
+ /**
280
+ * Filter options for listing labels.
281
+ */
282
+ export interface LabelFilter {
283
+ groupId?: string;
284
+ search?: string;
285
+ isBuiltin?: boolean;
286
+ }
287
+ /**
288
+ * Filter options for listing label groups.
289
+ */
290
+ export interface LabelGroupFilter {
291
+ search?: string;
292
+ }
251
293
  /**
252
294
  * Roadmap - a curated collection of projects for documentation/visualization.
253
295
  * Roadmaps group projects in a specific order for generating roadmap documents.
@@ -582,6 +624,8 @@ export interface TicketFilter {
582
624
  column?: string;
583
625
  projectId?: string;
584
626
  allProjects?: boolean;
627
+ label?: string;
628
+ labelGroup?: string;
585
629
  }
586
630
  export interface SpecFilter {
587
631
  status?: SpecStatus;
@@ -269,5 +269,5 @@ export function getPrioritySortIndex(db, value) {
269
269
  return Infinity;
270
270
  const priorities = getWorkspacePriorities(db);
271
271
  const index = priorities.indexOf(value);
272
- return index >= 0 ? index : Infinity;
272
+ return index !== -1 ? index : Infinity;
273
273
  }
@@ -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
@@ -193,6 +193,27 @@ export interface ExecutionResultJsonOutput {
193
193
  * Union type for all JSON output types
194
194
  */
195
195
  export type JsonOutput = PromptJsonOutput | SuccessJsonOutput | ErrorJsonOutput | DryRunJsonOutput | ConfirmationNeededJsonOutput | ExecutionResultJsonOutput;
196
+ /**
197
+ * All valid JSON envelope type discriminators.
198
+ * Used for contract tests and schema validation.
199
+ */
200
+ export declare const JSON_ENVELOPE_TYPES: readonly ["prompt", "success", "error", "dry-run", "confirmation_needed", "execution_result"];
201
+ export type JsonEnvelopeType = typeof JSON_ENVELOPE_TYPES[number];
202
+ /**
203
+ * Required fields per envelope type for contract validation.
204
+ * Tests use this to verify no fields are accidentally removed.
205
+ */
206
+ export declare const JSON_ENVELOPE_REQUIRED_FIELDS: Record<JsonEnvelopeType, string[]>;
207
+ /**
208
+ * Validate that a parsed JSON object conforms to the machine-mode envelope schema.
209
+ *
210
+ * Returns an array of validation errors (empty = valid).
211
+ * Useful for contract tests and runtime validation of JSON output.
212
+ *
213
+ * @param obj - Parsed JSON object to validate
214
+ * @returns Array of validation error strings (empty if valid)
215
+ */
216
+ export declare function validateJsonEnvelope(obj: unknown): string[];
196
217
  /**
197
218
  * Flags interface for JSON mode detection
198
219
  */
@@ -211,9 +232,18 @@ export interface MachineOutputFlags {
211
232
  machine?: boolean;
212
233
  }
213
234
  /**
214
- * Check if the current environment is non-TTY (piped output)
235
+ * Check if the current environment is non-TTY (piped input or output)
236
+ *
237
+ * Uses the "either" strategy: returns true if EITHER stdin OR stdout is non-TTY.
238
+ * This covers the primary use case of scripts/agents calling prlt as a subprocess,
239
+ * where both stdin and stdout are typically non-TTY.
240
+ *
241
+ * Returns true if:
242
+ * - stdin is not a TTY (e.g., piped input)
243
+ * - stdout is not a TTY (e.g., piped output)
244
+ * - PRLT_JSON=1 environment variable is set (overrides TTY detection)
215
245
  *
216
- * @returns true if stdout is not a TTY (e.g., piped to another process)
246
+ * @returns true if either stdin or stdout is not a TTY, or PRLT_JSON=1 is set
217
247
  */
218
248
  export declare function isNonTTY(): boolean;
219
249
  /**
@@ -221,7 +251,8 @@ export declare function isNonTTY(): boolean;
221
251
  *
222
252
  * Returns true if:
223
253
  * - The --json flag is set (or -m/--machine aliases)
224
- * - The environment is non-TTY (piped output)
254
+ * - The PRLT_JSON=1 environment variable is set
255
+ * - Either stdin or stdout is non-TTY (piped input/output)
225
256
  *
226
257
  * @param flags - Command flags object
227
258
  * @returns true if JSON mode should be used
@@ -236,7 +267,8 @@ export declare const isAgentMode: typeof shouldOutputJson;
236
267
  *
237
268
  * Returns true if:
238
269
  * - The --json flag is set (or -m/--machine aliases)
239
- * - The environment is non-TTY (piped output)
270
+ * - The PRLT_JSON=1 environment variable is set
271
+ * - Either stdin or stdout is non-TTY (piped input/output)
240
272
  *
241
273
  * @param flags - Command flags object
242
274
  * @returns true if machine-readable output mode should be used