@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.
- package/dist/commands/diet.d.ts +20 -0
- package/dist/commands/diet.js +181 -0
- package/dist/commands/mcp-server.js +2 -1
- package/dist/commands/priority/add.d.ts +15 -0
- package/dist/commands/priority/add.js +70 -0
- package/dist/commands/priority/list.d.ts +10 -0
- package/dist/commands/priority/list.js +34 -0
- package/dist/commands/priority/remove.d.ts +13 -0
- package/dist/commands/priority/remove.js +54 -0
- package/dist/commands/priority/set.d.ts +14 -0
- package/dist/commands/priority/set.js +60 -0
- package/dist/commands/pull.d.ts +23 -0
- package/dist/commands/pull.js +219 -0
- package/dist/commands/roadmap/generate.js +10 -5
- package/dist/commands/template/apply.js +5 -4
- package/dist/commands/template/create.js +9 -5
- package/dist/commands/ticket/create.js +6 -5
- package/dist/commands/ticket/edit.js +9 -9
- package/dist/commands/ticket/list.d.ts +2 -0
- package/dist/commands/ticket/list.js +20 -13
- package/dist/commands/ticket/update.js +8 -5
- package/dist/commands/work/spawn.d.ts +13 -0
- package/dist/commands/work/spawn.js +388 -1
- package/dist/lib/mcp/tools/diet.d.ts +6 -0
- package/dist/lib/mcp/tools/diet.js +261 -0
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/template.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +48 -3
- package/dist/lib/pmo/diet.d.ts +102 -0
- package/dist/lib/pmo/diet.js +127 -0
- package/dist/lib/pmo/storage/base.d.ts +5 -0
- package/dist/lib/pmo/storage/base.js +47 -0
- package/dist/lib/pmo/types.d.ts +12 -6
- package/dist/lib/pmo/types.js +6 -2
- package/dist/lib/pmo/utils.d.ts +40 -0
- package/dist/lib/pmo/utils.js +76 -0
- package/oclif.manifest.json +2872 -2534
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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: '' }, ...
|
|
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 {
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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 {
|
|
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
|
-
...
|
|
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 {
|
|
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:
|
|
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
|
-
...
|
|
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 {
|
|
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
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
}
|