@proletariat/cli 0.3.30 → 0.3.32

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 (39) hide show
  1. package/dist/commands/diet.d.ts +20 -0
  2. package/dist/commands/diet.js +181 -0
  3. package/dist/commands/mcp-server.js +2 -1
  4. package/dist/commands/priority/add.d.ts +15 -0
  5. package/dist/commands/priority/add.js +70 -0
  6. package/dist/commands/priority/list.d.ts +10 -0
  7. package/dist/commands/priority/list.js +34 -0
  8. package/dist/commands/priority/remove.d.ts +13 -0
  9. package/dist/commands/priority/remove.js +54 -0
  10. package/dist/commands/priority/set.d.ts +14 -0
  11. package/dist/commands/priority/set.js +60 -0
  12. package/dist/commands/pull.d.ts +23 -0
  13. package/dist/commands/pull.js +219 -0
  14. package/dist/commands/roadmap/generate.js +10 -5
  15. package/dist/commands/template/apply.js +5 -4
  16. package/dist/commands/template/create.js +9 -5
  17. package/dist/commands/ticket/create.js +6 -5
  18. package/dist/commands/ticket/edit.js +9 -9
  19. package/dist/commands/ticket/list.d.ts +2 -0
  20. package/dist/commands/ticket/list.js +20 -13
  21. package/dist/commands/ticket/update.js +8 -5
  22. package/dist/commands/work/spawn.d.ts +13 -0
  23. package/dist/commands/work/spawn.js +388 -1
  24. package/dist/lib/mcp/tools/diet.d.ts +6 -0
  25. package/dist/lib/mcp/tools/diet.js +261 -0
  26. package/dist/lib/mcp/tools/index.d.ts +1 -0
  27. package/dist/lib/mcp/tools/index.js +1 -0
  28. package/dist/lib/mcp/tools/template.js +1 -1
  29. package/dist/lib/mcp/tools/ticket.js +48 -3
  30. package/dist/lib/pmo/diet.d.ts +102 -0
  31. package/dist/lib/pmo/diet.js +127 -0
  32. package/dist/lib/pmo/storage/base.d.ts +5 -0
  33. package/dist/lib/pmo/storage/base.js +47 -0
  34. package/dist/lib/pmo/types.d.ts +12 -6
  35. package/dist/lib/pmo/types.js +6 -2
  36. package/dist/lib/pmo/utils.d.ts +40 -0
  37. package/dist/lib/pmo/utils.js +76 -0
  38. package/oclif.manifest.json +2872 -2534
  39. package/package.json +1 -1
@@ -0,0 +1,219 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../lib/pmo/base-command.js';
3
+ import { styles, divider } from '../lib/styles.js';
4
+ import { loadDietConfig, formatDietConfig, } from '../lib/pmo/diet.js';
5
+ export default class Pull extends PMOCommand {
6
+ static description = 'Pull tickets from Backlog to Ready using diet ratio enforcement';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %>',
9
+ '<%= config.bin %> <%= command.id %> --count 20',
10
+ '<%= config.bin %> <%= command.id %> --dry-run',
11
+ '<%= config.bin %> <%= command.id %> --count 50 --dry-run',
12
+ ];
13
+ static flags = {
14
+ ...pmoBaseFlags,
15
+ count: Flags.integer({
16
+ char: 'n',
17
+ description: 'Number of tickets to pull to Ready',
18
+ default: 50,
19
+ min: 1,
20
+ }),
21
+ 'dry-run': Flags.boolean({
22
+ char: 'd',
23
+ description: 'Show what would be pulled without moving tickets',
24
+ default: false,
25
+ }),
26
+ };
27
+ async execute() {
28
+ const { flags } = await this.parse(Pull);
29
+ const projectId = await this.requireProject();
30
+ const count = flags.count;
31
+ const dryRun = flags['dry-run'];
32
+ // Load diet config
33
+ const db = this.storage.getDatabase();
34
+ const dietConfig = loadDietConfig(db);
35
+ // Get project workflow statuses to find the target "Ready" status
36
+ const project = await this.storage.getProject(projectId);
37
+ if (!project) {
38
+ this.error(`Project not found: ${projectId}`);
39
+ }
40
+ const workflowId = project.workflowId || 'default';
41
+ const statuses = await this.storage.listStatuses(workflowId);
42
+ const hasBacklog = statuses.some(s => s.category === 'backlog');
43
+ const readyStatuses = statuses.filter(s => s.category === 'unstarted');
44
+ if (!hasBacklog) {
45
+ this.error('No backlog statuses found in workflow. Cannot pull tickets.');
46
+ }
47
+ if (readyStatuses.length === 0) {
48
+ this.error('No ready/unstarted statuses found in workflow. Cannot pull tickets.');
49
+ }
50
+ // Target status is the first unstarted status (Ready/To Do)
51
+ const targetStatus = readyStatuses.sort((a, b) => a.position - b.position)[0];
52
+ // Get all backlog tickets ordered by position (using statusCategory filter)
53
+ const allBacklogTickets = await this.storage.listTickets(projectId, { statusCategory: 'backlog' });
54
+ allBacklogTickets.sort((a, b) => (a.position || 0) - (b.position || 0));
55
+ if (allBacklogTickets.length === 0) {
56
+ this.log(styles.warning('No tickets in backlog to pull.'));
57
+ return;
58
+ }
59
+ // Get existing ready tickets for diet calculation
60
+ const existingReadyTickets = await this.storage.listTickets(projectId, { statusCategory: 'unstarted' });
61
+ // Run the pull algorithm
62
+ const result = await this.runPullAlgorithm(allBacklogTickets, existingReadyTickets, dietConfig, count);
63
+ // Display results
64
+ this.displayPullResults(result, dietConfig, targetStatus, dryRun);
65
+ // Move tickets if not dry-run
66
+ if (!dryRun && result.pulled.length > 0) {
67
+ for (const ticket of result.pulled) {
68
+ // eslint-disable-next-line no-await-in-loop -- Sequential moves to maintain ordering
69
+ await this.storage.moveTicket(projectId, ticket.id, targetStatus.name);
70
+ }
71
+ this.log(styles.success(`\nMoved ${result.pulled.length} ticket${result.pulled.length === 1 ? '' : 's'} to ${targetStatus.name}.`));
72
+ }
73
+ else if (dryRun && result.pulled.length > 0) {
74
+ this.log(styles.warning(`\nDry run: ${result.pulled.length} ticket${result.pulled.length === 1 ? '' : 's'} would be moved to ${targetStatus.name}.`));
75
+ }
76
+ }
77
+ /**
78
+ * Core pull algorithm with diet enforcement.
79
+ *
80
+ * Pass 1: Walk backlog top-down, pull if category not over ceiling
81
+ * Pass 2: Force-pull from underrepresented categories
82
+ */
83
+ async runPullAlgorithm(backlogTickets, existingReadyTickets, dietConfig, targetCount) {
84
+ const pulled = [];
85
+ let skippedBlocked = 0;
86
+ let skippedCeiling = 0;
87
+ // Build category count map from existing ready tickets
88
+ const categoryCounts = new Map();
89
+ for (const ratio of dietConfig.ratios) {
90
+ categoryCounts.set(ratio.category, 0);
91
+ }
92
+ for (const ticket of existingReadyTickets) {
93
+ const cat = (ticket.category || '').toLowerCase();
94
+ if (cat) {
95
+ categoryCounts.set(cat, (categoryCounts.get(cat) || 0) + 1);
96
+ }
97
+ }
98
+ // Track which tickets are pulled (to avoid duplicates in second pass)
99
+ const pulledIds = new Set();
100
+ // Calculate ceiling per category (total = existing ready + new pulls)
101
+ const totalTarget = existingReadyTickets.length + targetCount;
102
+ const getCeiling = (category) => {
103
+ const ratio = dietConfig.ratios.find(r => r.category === category);
104
+ if (!ratio)
105
+ return targetCount; // No ceiling for uncategorized
106
+ return Math.ceil(totalTarget * ratio.target);
107
+ };
108
+ // Pass 1: Walk backlog top-down, pull if under ceiling
109
+ const remainingBacklog = [];
110
+ for (const ticket of backlogTickets) {
111
+ if (pulled.length >= targetCount)
112
+ break;
113
+ // Check if blocked (all blocking dependencies must be completed/canceled)
114
+ // eslint-disable-next-line no-await-in-loop -- Need sequential dependency check
115
+ const blocked = await this.storage.isTicketBlocked(ticket.id);
116
+ if (blocked) {
117
+ skippedBlocked++;
118
+ continue;
119
+ }
120
+ const cat = (ticket.category || '').toLowerCase();
121
+ const currentCount = categoryCounts.get(cat) || 0;
122
+ const ceiling = getCeiling(cat);
123
+ if (currentCount < ceiling) {
124
+ pulled.push({
125
+ id: ticket.id,
126
+ title: ticket.title,
127
+ category: ticket.category || undefined,
128
+ position: ticket.position || 0,
129
+ pass: 'first',
130
+ });
131
+ pulledIds.add(ticket.id);
132
+ categoryCounts.set(cat, currentCount + 1);
133
+ }
134
+ else {
135
+ skippedCeiling++;
136
+ remainingBacklog.push(ticket);
137
+ }
138
+ }
139
+ // Pass 2: Force-pull from underrepresented categories
140
+ if (pulled.length < targetCount) {
141
+ for (const ratio of dietConfig.ratios) {
142
+ if (pulled.length >= targetCount)
143
+ break;
144
+ const currentCount = categoryCounts.get(ratio.category) || 0;
145
+ const targetForCat = Math.ceil(totalTarget * ratio.target);
146
+ if (currentCount < targetForCat) {
147
+ const catTickets = remainingBacklog.filter(t => (t.category || '').toLowerCase() === ratio.category && !pulledIds.has(t.id));
148
+ for (const ticket of catTickets) {
149
+ if (pulled.length >= targetCount)
150
+ break;
151
+ if ((categoryCounts.get(ratio.category) || 0) >= targetForCat)
152
+ break;
153
+ // eslint-disable-next-line no-await-in-loop -- Sequential dependency check
154
+ const blocked = await this.storage.isTicketBlocked(ticket.id);
155
+ if (blocked)
156
+ continue;
157
+ pulled.push({
158
+ id: ticket.id,
159
+ title: ticket.title,
160
+ category: ticket.category || undefined,
161
+ position: ticket.position || 0,
162
+ pass: 'second',
163
+ });
164
+ pulledIds.add(ticket.id);
165
+ categoryCounts.set(ratio.category, (categoryCounts.get(ratio.category) || 0) + 1);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ return {
171
+ pulled,
172
+ skippedBlocked,
173
+ skippedCeiling,
174
+ totalCandidates: backlogTickets.length,
175
+ };
176
+ }
177
+ /**
178
+ * Display pull results.
179
+ */
180
+ displayPullResults(result, dietConfig, targetStatus, dryRun) {
181
+ const prefix = dryRun ? '[DRY RUN] ' : '';
182
+ this.log(styles.title(`\n${prefix}Pull Results`));
183
+ this.log(divider(60));
184
+ // Summary stats
185
+ this.log(` Backlog candidates: ${result.totalCandidates}`);
186
+ this.log(` Skipped (blocked): ${result.skippedBlocked}`);
187
+ this.log(` Skipped (ceiling): ${result.skippedCeiling}`);
188
+ this.log(` ${dryRun ? 'Would pull' : 'Pulling'}: ${styles.emphasis(String(result.pulled.length))}`);
189
+ this.log(` Target status: ${targetStatus.name}`);
190
+ this.log(` Diet: ${formatDietConfig(dietConfig)}`);
191
+ this.log(divider(60));
192
+ if (result.pulled.length === 0) {
193
+ this.log(styles.warning('\nNo tickets to pull.'));
194
+ return;
195
+ }
196
+ // Group by category for display
197
+ const byCategory = new Map();
198
+ for (const ticket of result.pulled) {
199
+ const cat = ticket.category || 'uncategorized';
200
+ if (!byCategory.has(cat)) {
201
+ byCategory.set(cat, []);
202
+ }
203
+ byCategory.get(cat).push(ticket);
204
+ }
205
+ // Display by category
206
+ for (const [category, tickets] of byCategory) {
207
+ const ratio = dietConfig.ratios.find(r => r.category === category);
208
+ const targetPct = ratio ? `${Math.round(ratio.target * 100)}%` : 'n/a';
209
+ const actualPct = result.pulled.length > 0
210
+ ? `${Math.round((tickets.length / result.pulled.length) * 100)}%`
211
+ : '0%';
212
+ this.log(`\n ${styles.emphasis(category)} (${tickets.length} tickets, target: ${targetPct}, actual: ${actualPct})`);
213
+ for (const ticket of tickets) {
214
+ const passLabel = ticket.pass === 'second' ? styles.warning(' [force-pull]') : '';
215
+ this.log(` ${styles.code(ticket.id)} ${ticket.title}${passLabel}`);
216
+ }
217
+ }
218
+ }
219
+ }
@@ -4,7 +4,8 @@ import * as path from 'node:path';
4
4
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { slugify } from '../../lib/pmo/utils.js';
7
- import { normalizePriority, PRIORITIES } from '../../lib/pmo/types.js';
7
+ import { normalizePriority } from '../../lib/pmo/types.js';
8
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
8
9
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
9
10
  export default class RoadmapGenerate extends PMOCommand {
10
11
  static description = 'Generate roadmap markdown file';
@@ -125,9 +126,11 @@ export default class RoadmapGenerate extends PMOCommand {
125
126
  const filteredTickets = excludeDone
126
127
  ? tickets.filter(t => t.statusCategory !== 'completed')
127
128
  : tickets;
128
- // Group tickets by priority
129
+ // Group tickets by priority (using workspace priority scale)
130
+ const db = this.storage.getDatabase();
131
+ const workspacePriorities = getWorkspacePriorities(db);
129
132
  const ticketsByPriority = new Map();
130
- for (const priority of PRIORITIES) {
133
+ for (const priority of workspacePriorities) {
131
134
  ticketsByPriority.set(priority, []);
132
135
  }
133
136
  ticketsByPriority.set('unset', []);
@@ -181,13 +184,15 @@ export default class RoadmapGenerate extends PMOCommand {
181
184
  this.log(styles.success(`Generated ${filePath}`));
182
185
  }
183
186
  getPriorityLabel(priority) {
184
- const labels = {
187
+ // For well-known P0-P3 priorities, provide descriptive labels
188
+ const defaultLabels = {
185
189
  'P0': 'Critical',
186
190
  'P1': 'High',
187
191
  'P2': 'Medium',
188
192
  'P3': 'Low',
189
193
  };
190
- return labels[priority] || priority;
194
+ // For user-defined priorities, the value IS the label
195
+ return defaultLabels[priority] || priority;
191
196
  }
192
197
  cleanForTable(text) {
193
198
  return (text || '').replace(/\|/g, '-').replace(/\n/g, ' ').replace(/\r/g, '').trim();
@@ -1,6 +1,6 @@
1
1
  import { Flags, Args } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
3
- import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
3
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
4
4
  import { styles } from '../../lib/styles.js';
5
5
  import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildFormPromptConfig, buildPromptConfig, } from '../../lib/prompt-json.js';
6
6
  export default class TemplateApply extends PMOCommand {
@@ -34,8 +34,7 @@ export default class TemplateApply extends PMOCommand {
34
34
  }),
35
35
  priority: Flags.string({
36
36
  char: 'p',
37
- description: 'Priority override (ticket only)',
38
- options: [...PRIORITIES],
37
+ description: 'Priority override (ticket only, uses workspace priority scale)',
39
38
  }),
40
39
  category: Flags.string({
41
40
  description: 'Category override (ticket only)',
@@ -149,10 +148,12 @@ export default class TemplateApply extends PMOCommand {
149
148
  const labels = template.defaultLabels;
150
149
  // Interactive mode
151
150
  if (flags.interactive || !title) {
151
+ const db = this.storage.getDatabase();
152
+ const workspacePriorities = getWorkspacePriorities(db);
152
153
  const fields = [
153
154
  { type: 'input', name: 'title', message: 'Title:', default: title || undefined },
154
155
  { type: 'list', name: 'column', message: 'Column:', choices: columns.map(c => ({ name: c, value: c })), default: column },
155
- { type: 'list', name: 'priority', message: 'Priority:', choices: [{ name: 'None', value: '' }, ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p }))], default: priority },
156
+ { type: 'list', name: 'priority', message: 'Priority:', choices: [{ name: 'None', value: '' }, ...workspacePriorities.map(p => ({ name: p, value: p }))], default: priority },
156
157
  ];
157
158
  if (jsonMode) {
158
159
  outputPromptAsJson(buildFormPromptConfig(fields), createMetadata('template apply', flags));
@@ -1,6 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
- import { PRIORITIES, PRIORITY_LABELS, TICKET_CATEGORIES } from '../../lib/pmo/types.js';
3
+ import { TICKET_CATEGORIES } from '../../lib/pmo/types.js';
4
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
4
5
  import { styles } from '../../lib/styles.js';
5
6
  import { shouldOutputJson, outputSuccessAsJson, outputPromptAsJson, outputErrorAsJson, buildFormPromptConfig, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
6
7
  export default class TemplateCreate extends PMOCommand {
@@ -37,8 +38,7 @@ export default class TemplateCreate extends PMOCommand {
37
38
  }),
38
39
  priority: Flags.string({
39
40
  char: 'p',
40
- description: 'Default priority (ticket only)',
41
- options: [...PRIORITIES],
41
+ description: 'Default priority (ticket only, uses workspace priority scale)',
42
42
  }),
43
43
  category: Flags.string({
44
44
  char: 'c',
@@ -103,6 +103,8 @@ export default class TemplateCreate extends PMOCommand {
103
103
  // Check if we have required data
104
104
  if (!name) {
105
105
  if (jsonMode) {
106
+ const db = this.storage.getDatabase();
107
+ const workspacePriorities = getWorkspacePriorities(db);
106
108
  const fields = [
107
109
  { type: 'input', name: 'name', message: 'Template name:' },
108
110
  { type: 'input', name: 'description', message: 'Description (optional):' },
@@ -112,7 +114,7 @@ export default class TemplateCreate extends PMOCommand {
112
114
  message: 'Default priority:',
113
115
  choices: [
114
116
  { name: 'None', value: '' },
115
- ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p })),
117
+ ...workspacePriorities.map(p => ({ name: p, value: p })),
116
118
  ],
117
119
  },
118
120
  {
@@ -154,13 +156,15 @@ export default class TemplateCreate extends PMOCommand {
154
156
  description = desc || undefined;
155
157
  }
156
158
  if (!jsonMode && priority === undefined) {
159
+ const db = this.storage.getDatabase();
160
+ const workspacePriorities = getWorkspacePriorities(db);
157
161
  const { p } = await this.prompt([{
158
162
  type: 'list',
159
163
  name: 'p',
160
164
  message: 'Default priority:',
161
165
  choices: [
162
166
  { name: 'None', value: '' },
163
- ...PRIORITIES.map(pr => ({ name: PRIORITY_LABELS[pr], value: pr })),
167
+ ...workspacePriorities.map(pr => ({ name: pr, value: pr })),
164
168
  ],
165
169
  }], null);
166
170
  priority = p || undefined;
@@ -5,7 +5,7 @@ import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index
5
5
  // Note: inquirer import kept for inquirer.Separator usage in interactive mode
6
6
  import { styles } from '../../lib/styles.js';
7
7
  import { updateEpicTicketsSection } from '../../lib/pmo/epic-files.js';
8
- import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
8
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
9
9
  import { shouldOutputJson, outputErrorAsJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
10
10
  import { FlagResolver } from '../../lib/flags/index.js';
11
11
  import { multiLineInput } from '../../lib/multiline-input.js';
@@ -40,8 +40,7 @@ export default class TicketCreate extends PMOCommand {
40
40
  }),
41
41
  priority: Flags.string({
42
42
  char: 'p',
43
- description: 'Ticket priority',
44
- options: [...PRIORITIES],
43
+ description: 'Ticket priority (uses workspace priority scale)',
45
44
  }),
46
45
  category: Flags.string({
47
46
  description: 'Ticket category (e.g., bug, feature, refactor)',
@@ -361,7 +360,9 @@ export default class TicketCreate extends PMOCommand {
361
360
  default: flags.column || columns[0],
362
361
  },
363
362
  ], null);
364
- // Prompt for priority
363
+ // Prompt for priority (using workspace priority scale)
364
+ const db = this.storage.getDatabase();
365
+ const workspacePriorities = getWorkspacePriorities(db);
365
366
  const { priority: answerPriority } = await this.prompt([
366
367
  {
367
368
  type: 'list',
@@ -369,7 +370,7 @@ export default class TicketCreate extends PMOCommand {
369
370
  message: 'Priority:',
370
371
  choices: [
371
372
  { name: 'None', value: undefined },
372
- ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p })),
373
+ ...workspacePriorities.map(p => ({ name: p, value: p })),
373
374
  ],
374
375
  default: flags.priority || template?.defaultPriority,
375
376
  },
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
3
  import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
- import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
4
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
7
7
  import { multiLineInput } from '../../lib/multiline-input.js';
@@ -33,8 +33,7 @@ export default class TicketEdit extends PMOCommand {
33
33
  }),
34
34
  priority: Flags.string({
35
35
  char: 'p',
36
- description: 'New ticket priority',
37
- options: [...PRIORITIES, 'none'],
36
+ description: 'New ticket priority (uses workspace priority scale, "none" to clear)',
38
37
  }),
39
38
  category: Flags.string({
40
39
  description: 'New ticket category',
@@ -132,15 +131,14 @@ export default class TicketEdit extends PMOCommand {
132
131
  // In JSON mode without flags, output a form prompt instead of interactive prompts
133
132
  if (jsonMode) {
134
133
  const { outputPromptAsJson, buildFormPromptConfig } = await import('../../lib/prompt-json.js');
134
+ const db = this.storage.getDatabase();
135
+ const workspacePriorities = getWorkspacePriorities(db);
135
136
  const formConfig = buildFormPromptConfig([
136
137
  { type: 'input', name: 'title', message: 'Title:', default: ticket.title },
137
138
  { type: 'multiline', name: 'description', message: 'Description:', default: ticket.description || '' },
138
139
  { type: 'list', name: 'priority', message: 'Priority:', choices: [
139
140
  { name: 'None', value: '' },
140
- { name: 'P0 - Critical', value: 'P0' },
141
- { name: 'P1 - High', value: 'P1' },
142
- { name: 'P2 - Medium', value: 'P2' },
143
- { name: 'P3 - Low', value: 'P3' },
141
+ ...workspacePriorities.map(p => ({ name: p, value: p })),
144
142
  ], default: ticket.priority || '' },
145
143
  { type: 'input', name: 'category', message: 'Category:', default: ticket.category || '' },
146
144
  ]);
@@ -287,7 +285,9 @@ export default class TicketEdit extends PMOCommand {
287
285
  if (descResult.cancelled) {
288
286
  throw new Error('Edit cancelled');
289
287
  }
290
- // Continue with remaining prompts - priority first
288
+ // Continue with remaining prompts - priority first (using workspace scale)
289
+ const db = this.storage.getDatabase();
290
+ const workspacePriorities = getWorkspacePriorities(db);
291
291
  const { priority } = await this.prompt([
292
292
  {
293
293
  type: 'list',
@@ -295,7 +295,7 @@ export default class TicketEdit extends PMOCommand {
295
295
  message: 'Priority:',
296
296
  choices: [
297
297
  { name: 'None', value: '' },
298
- ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p })),
298
+ ...workspacePriorities.map(p => ({ name: p, value: p })),
299
299
  ],
300
300
  default: ticket.priority || '',
301
301
  },
@@ -1,6 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class TicketList extends Command {
3
3
  static description: string;
4
+ /** Dynamic priority order (set from workspace settings in run()) */
5
+ private priorityOrder;
4
6
  static examples: string[];
5
7
  static flags: {
6
8
  column: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,13 +1,19 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { pmoBaseFlags } from '../../lib/pmo/index.js';
3
- import { PRIORITIES } from '../../lib/pmo/types.js';
3
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
4
4
  import { getPMOContext } from '../../lib/pmo/pmo-context.js';
5
5
  import { styles, formatPriority, formatCategory, getColumnStyle, getColumnEmoji, divider, getPriorityStyle, } from '../../lib/styles.js';
6
6
  import { isNonTTY } from '../../lib/prompt-json.js';
7
- // Priority order for grouping: P0, P1, P2, P3, None
8
- const PRIORITY_ORDER = ['P0', 'P1', 'P2', 'P3', 'None'];
7
+ // Priority order for grouping - dynamically resolved from workspace settings
8
+ // Computed at runtime and includes 'None' for unset priorities
9
+ function getPriorityOrder(db) {
10
+ const priorities = getWorkspacePriorities(db);
11
+ return [...priorities, 'None'];
12
+ }
9
13
  export default class TicketList extends Command {
10
14
  static description = 'List tickets from the PMO board';
15
+ /** Dynamic priority order (set from workspace settings in run()) */
16
+ priorityOrder = ['P0', 'P1', 'P2', 'P3', 'None'];
11
17
  static examples = [
12
18
  '<%= config.bin %> <%= command.id %>',
13
19
  '<%= config.bin %> <%= command.id %> --column Backlog',
@@ -29,8 +35,7 @@ export default class TicketList extends Command {
29
35
  }),
30
36
  priority: Flags.string({
31
37
  char: 'p',
32
- description: 'Filter by priority',
33
- options: [...PRIORITIES],
38
+ description: 'Filter by priority (uses workspace priority scale)',
34
39
  }),
35
40
  category: Flags.string({
36
41
  description: 'Filter by category',
@@ -79,6 +84,8 @@ export default class TicketList extends Command {
79
84
  logger: (msg) => this.log(styles.muted(msg)),
80
85
  });
81
86
  try {
87
+ // Set dynamic priority order from workspace settings
88
+ this.priorityOrder = getPriorityOrder(pmoContext.storage.getDatabase());
82
89
  // Build filter
83
90
  const filter = {};
84
91
  if (flags.all) {
@@ -239,7 +246,7 @@ export default class TicketList extends Command {
239
246
  outputCrossProjectTableByPriority(tickets) {
240
247
  // Group tickets by priority
241
248
  const byPriority = {};
242
- for (const priority of PRIORITY_ORDER) {
249
+ for (const priority of this.priorityOrder) {
243
250
  byPriority[priority] = [];
244
251
  }
245
252
  for (const ticket of tickets) {
@@ -249,7 +256,7 @@ export default class TicketList extends Command {
249
256
  }
250
257
  byPriority[priority].push(ticket);
251
258
  }
252
- for (const priority of PRIORITY_ORDER) {
259
+ for (const priority of this.priorityOrder) {
253
260
  const priorityTickets = byPriority[priority];
254
261
  // Priority header
255
262
  const headerColor = getPriorityStyle(priority);
@@ -305,7 +312,7 @@ export default class TicketList extends Command {
305
312
  outputCrossProjectCompactByPriority(tickets) {
306
313
  // Group tickets by priority
307
314
  const byPriority = {};
308
- for (const priority of PRIORITY_ORDER) {
315
+ for (const priority of this.priorityOrder) {
309
316
  byPriority[priority] = [];
310
317
  }
311
318
  for (const ticket of tickets) {
@@ -315,7 +322,7 @@ export default class TicketList extends Command {
315
322
  }
316
323
  byPriority[priority].push(ticket);
317
324
  }
318
- for (const priority of PRIORITY_ORDER) {
325
+ for (const priority of this.priorityOrder) {
319
326
  const priorityTickets = byPriority[priority];
320
327
  if (priorityTickets.length === 0)
321
328
  continue;
@@ -380,7 +387,7 @@ export default class TicketList extends Command {
380
387
  outputTableByPriority(tickets) {
381
388
  // Group tickets by priority
382
389
  const byPriority = {};
383
- for (const priority of PRIORITY_ORDER) {
390
+ for (const priority of this.priorityOrder) {
384
391
  byPriority[priority] = [];
385
392
  }
386
393
  for (const ticket of tickets) {
@@ -391,7 +398,7 @@ export default class TicketList extends Command {
391
398
  byPriority[priority].push(ticket);
392
399
  }
393
400
  // Display ALL priority groups
394
- for (const priority of PRIORITY_ORDER) {
401
+ for (const priority of this.priorityOrder) {
395
402
  const priorityTickets = byPriority[priority];
396
403
  // Priority header with color
397
404
  const headerColor = getPriorityStyle(priority);
@@ -457,7 +464,7 @@ export default class TicketList extends Command {
457
464
  outputCompactByPriority(tickets) {
458
465
  // Group by priority
459
466
  const byPriority = {};
460
- for (const priority of PRIORITY_ORDER) {
467
+ for (const priority of this.priorityOrder) {
461
468
  byPriority[priority] = [];
462
469
  }
463
470
  for (const ticket of tickets) {
@@ -467,7 +474,7 @@ export default class TicketList extends Command {
467
474
  }
468
475
  byPriority[priority].push(ticket);
469
476
  }
470
- for (const priority of PRIORITY_ORDER) {
477
+ for (const priority of this.priorityOrder) {
471
478
  const priorityTickets = byPriority[priority];
472
479
  // Show all priority groups
473
480
  const headerColor = getPriorityStyle(priority);
@@ -1,6 +1,6 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
3
- import { PRIORITIES, PRIORITY_LABELS } from '../../lib/pmo/types.js';
3
+ import { getWorkspacePriorities } from '../../lib/pmo/utils.js';
4
4
  import { styles } from '../../lib/styles.js';
5
5
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
6
6
  export default class TicketUpdate extends PMOCommand {
@@ -28,8 +28,7 @@ export default class TicketUpdate extends PMOCommand {
28
28
  }),
29
29
  priority: Flags.string({
30
30
  char: 'p',
31
- description: 'Set priority (P0, P1, P2, P3)',
32
- options: [...PRIORITIES],
31
+ description: 'Set priority (uses workspace priority scale)',
33
32
  }),
34
33
  category: Flags.string({
35
34
  char: 'c',
@@ -104,13 +103,15 @@ export default class TicketUpdate extends PMOCommand {
104
103
  ],
105
104
  }], jsonModeConfig);
106
105
  if (updateType === 'priority' || updateType === 'both') {
106
+ const db = this.storage.getDatabase();
107
+ const workspacePriorities = getWorkspacePriorities(db);
107
108
  const { priority } = await this.prompt([{
108
109
  type: 'list',
109
110
  name: 'priority',
110
111
  message: 'Set priority to:',
111
112
  choices: [
112
113
  { name: `(Keep existing: ${ticket.priority || 'none'})`, value: null, command: '' },
113
- ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p, command: `prlt ticket update ${ticketId} --priority ${p}${projectId ? ` -P ${projectId}` : ''} --json` })),
114
+ ...workspacePriorities.map(p => ({ name: p, value: p, command: `prlt ticket update ${ticketId} --priority ${p}${projectId ? ` -P ${projectId}` : ''} --json` })),
114
115
  { name: 'None (clear priority)', value: '', command: `prlt ticket update ${ticketId} --priority none${projectId ? ` -P ${projectId}` : ''} --json` },
115
116
  ],
116
117
  }], jsonModeConfig);
@@ -208,13 +209,15 @@ export default class TicketUpdate extends PMOCommand {
208
209
  ],
209
210
  }], jsonModeConfig);
210
211
  if (updateType === 'priority' || updateType === 'both') {
212
+ const db = this.storage.getDatabase();
213
+ const bulkWorkspacePriorities = getWorkspacePriorities(db);
211
214
  const { priority } = await this.prompt([{
212
215
  type: 'list',
213
216
  name: 'priority',
214
217
  message: 'Set priority to:',
215
218
  choices: [
216
219
  { name: '(Keep existing)', value: null, command: '' },
217
- ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p, command: `prlt ticket update --bulk --priority ${p} --json` })),
220
+ ...bulkWorkspacePriorities.map(p => ({ name: p, value: p, command: `prlt ticket update --bulk --priority ${p} --json` })),
218
221
  { name: 'None (clear priority)', value: '', command: 'prlt ticket update --bulk --priority none --json' },
219
222
  ],
220
223
  }], jsonModeConfig);
@@ -26,7 +26,20 @@ export default class WorkSpawn extends PMOCommand {
26
26
  session: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
27
  focus: import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
28
  clone: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
+ count: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ diet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ category: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
+ priority: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
33
+ epic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
34
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
35
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
36
  };
31
37
  execute(): Promise<void>;
38
+ /**
39
+ * Select tickets using diet-balanced category weighting.
40
+ * Uses a two-pass algorithm:
41
+ * Pass 1: Walk candidates in position order, pull if category not over ceiling.
42
+ * Pass 2: Force-pull from underrepresented categories.
43
+ */
44
+ private selectDietBalanced;
32
45
  }