@proletariat/cli 0.3.31 → 0.3.33
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 +397 -6
- package/dist/commands/work/start.d.ts +1 -0
- package/dist/commands/work/start.js +5 -0
- package/dist/lib/execution/runners.js +4 -0
- package/dist/lib/execution/types.d.ts +1 -0
- 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 +16 -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 +2783 -2438
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { getWorkspaceInfo, getTicketTmuxSession, killTmuxSession } from '../../l
|
|
|
7
7
|
import { isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from '../../lib/execution/runners.js';
|
|
8
8
|
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, outputConfirmationNeededAsJson, outputExecutionResultAsJson, } from '../../lib/prompt-json.js';
|
|
9
9
|
import { FlagResolver } from '../../lib/flags/index.js';
|
|
10
|
+
import { loadDietConfig, formatDietConfig, } from '../../lib/pmo/diet.js';
|
|
10
11
|
export default class WorkSpawn extends PMOCommand {
|
|
11
12
|
static description = 'Spawn work for multiple tickets by column (batch mode)';
|
|
12
13
|
static strict = false; // Allow multiple ticket ID args without defining them
|
|
@@ -19,6 +20,11 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
19
20
|
'<%= config.bin %> <%= command.id %> --dry-run # Preview without executing',
|
|
20
21
|
'<%= config.bin %> <%= command.id %> --many --json # Output ticket choices as JSON (for agents)',
|
|
21
22
|
'<%= config.bin %> <%= command.id %> TKT-001 --action custom --message "Add unit tests" # Custom prompt',
|
|
23
|
+
'<%= config.bin %> <%= command.id %> --count 5 --action implement # Top 5 by rank',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> --count 10 --diet --action groom # Diet-balanced',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> --count 5 --category ship --action implement # Filtered by category',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> --count 5 --priority P0 --action implement # Filtered by priority',
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --count 10 --diet --category ship,grow --action groom # Combined',
|
|
22
28
|
];
|
|
23
29
|
static flags = {
|
|
24
30
|
...pmoBaseFlags,
|
|
@@ -104,7 +110,7 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
104
110
|
description: 'Action to perform (e.g., groom, implement, review, custom). Prompts if not provided.',
|
|
105
111
|
}),
|
|
106
112
|
message: Flags.string({
|
|
107
|
-
description: '
|
|
113
|
+
description: 'Additional instructions for the agent (appended to any action prompt)',
|
|
108
114
|
}),
|
|
109
115
|
session: Flags.string({
|
|
110
116
|
description: 'Session manager inside container (tmux runs agent in tmux inside container)',
|
|
@@ -119,6 +125,27 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
119
125
|
description: 'Use independent git clone instead of worktree (more isolation, no real-time sync)',
|
|
120
126
|
default: false,
|
|
121
127
|
}),
|
|
128
|
+
count: Flags.integer({
|
|
129
|
+
char: 'n',
|
|
130
|
+
description: 'Number of tickets to spawn (selects top N by rank)',
|
|
131
|
+
min: 1,
|
|
132
|
+
}),
|
|
133
|
+
diet: Flags.boolean({
|
|
134
|
+
description: 'Apply diet-balanced category weighting when selecting tickets',
|
|
135
|
+
default: false,
|
|
136
|
+
}),
|
|
137
|
+
category: Flags.string({
|
|
138
|
+
description: 'Filter tickets by category (comma-separated, e.g., ship,grow)',
|
|
139
|
+
}),
|
|
140
|
+
priority: Flags.string({
|
|
141
|
+
description: 'Filter tickets by priority (comma-separated, e.g., P0,P1)',
|
|
142
|
+
}),
|
|
143
|
+
epic: Flags.string({
|
|
144
|
+
description: 'Filter tickets by epic ID',
|
|
145
|
+
}),
|
|
146
|
+
status: Flags.string({
|
|
147
|
+
description: 'Filter tickets by status name (e.g., Backlog, Ready)',
|
|
148
|
+
}),
|
|
122
149
|
};
|
|
123
150
|
async execute() {
|
|
124
151
|
const { flags, argv } = await this.parse(WorkSpawn);
|
|
@@ -200,12 +227,16 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
200
227
|
}
|
|
201
228
|
// Note: With ephemeral agents, we no longer need to check for available pre-registered agents
|
|
202
229
|
// Agents are created on-demand when spawning
|
|
203
|
-
// Determine spawn mode: All, Many,
|
|
230
|
+
// Determine spawn mode: All, Many, Args, or Count
|
|
204
231
|
let spawnMode = 'all';
|
|
205
232
|
if (ticketIdArgs.length > 0) {
|
|
206
233
|
// Ticket IDs provided as positional args
|
|
207
234
|
spawnMode = 'args';
|
|
208
235
|
}
|
|
236
|
+
else if (flags.count) {
|
|
237
|
+
// Count-based selection with optional filters
|
|
238
|
+
spawnMode = 'count';
|
|
239
|
+
}
|
|
209
240
|
else if (flags.all) {
|
|
210
241
|
spawnMode = 'all';
|
|
211
242
|
}
|
|
@@ -226,6 +257,7 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
226
257
|
type: 'list',
|
|
227
258
|
message: 'How would you like to spawn work?',
|
|
228
259
|
choices: () => [
|
|
260
|
+
{ name: jsonMode ? 'Count - Spawn next N tickets (with optional filters)' : '🔢 Count - Spawn next N tickets (with optional filters)', value: 'count' },
|
|
229
261
|
{ name: jsonMode ? 'All - Spawn all tickets in a column' : '📦 All - Spawn all tickets tickets in a column', value: 'all' },
|
|
230
262
|
{ name: jsonMode ? 'Many - Select specific tickets to spawn' : '✅ Many - Select specific tickets to spawn', value: 'many' },
|
|
231
263
|
],
|
|
@@ -331,6 +363,289 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
331
363
|
this.log(styles.header(`🚀 Spawn: ${ticketsToSpawn.length} ticket(s)`));
|
|
332
364
|
this.log(styles.muted(`Tickets: ${ticketsToSpawn.map(t => t.id).join(', ')}`));
|
|
333
365
|
}
|
|
366
|
+
else if (spawnMode === 'count') {
|
|
367
|
+
// COUNT MODE: Select top N tickets by rank with optional filters and diet balancing
|
|
368
|
+
let targetCount = flags.count;
|
|
369
|
+
// If count not provided via flag (interactive mode selected 'count'), prompt for it
|
|
370
|
+
if (!targetCount) {
|
|
371
|
+
const countResolver = new FlagResolver({
|
|
372
|
+
commandName: 'work spawn',
|
|
373
|
+
baseCommand: 'prlt work spawn',
|
|
374
|
+
jsonMode,
|
|
375
|
+
flags: {},
|
|
376
|
+
context: { projectId },
|
|
377
|
+
});
|
|
378
|
+
countResolver.addPrompt({
|
|
379
|
+
flagName: 'spawnCount',
|
|
380
|
+
type: 'input',
|
|
381
|
+
message: 'How many agents to spawn?',
|
|
382
|
+
default: '5',
|
|
383
|
+
validate: (value) => {
|
|
384
|
+
const num = Number.parseInt(String(value), 10);
|
|
385
|
+
if (Number.isNaN(num) || num < 1)
|
|
386
|
+
return 'Please enter a number >= 1';
|
|
387
|
+
return true;
|
|
388
|
+
},
|
|
389
|
+
transform: (value) => Number.parseInt(String(value), 10),
|
|
390
|
+
});
|
|
391
|
+
const countResult = await countResolver.resolve();
|
|
392
|
+
targetCount = countResult.spawnCount;
|
|
393
|
+
}
|
|
394
|
+
// Determine selection strategy
|
|
395
|
+
let useDiet = flags.diet;
|
|
396
|
+
// In interactive mode, prompt for selection strategy if not specified via flags
|
|
397
|
+
const hasFilterFlags = flags.category || flags.priority || flags.epic || flags.status;
|
|
398
|
+
if (!useDiet && !hasFilterFlags && !flags.count) {
|
|
399
|
+
// Interactive mode - let user choose strategy
|
|
400
|
+
const strategyResolver = new FlagResolver({
|
|
401
|
+
commandName: 'work spawn',
|
|
402
|
+
baseCommand: 'prlt work spawn',
|
|
403
|
+
jsonMode,
|
|
404
|
+
flags: {},
|
|
405
|
+
});
|
|
406
|
+
strategyResolver.addPrompt({
|
|
407
|
+
flagName: 'strategy',
|
|
408
|
+
type: 'list',
|
|
409
|
+
message: 'Selection strategy:',
|
|
410
|
+
choices: () => [
|
|
411
|
+
{ name: jsonMode ? 'Top ranked - Select by position order' : '📊 Top ranked - Select by position order', value: 'top' },
|
|
412
|
+
{ name: jsonMode ? 'Diet-balanced - Balance across category weights' : '⚖️ Diet-balanced - Balance across category weights', value: 'diet' },
|
|
413
|
+
{ name: jsonMode ? 'Filtered - Pick filters to narrow selection' : '🔍 Filtered - Pick filters to narrow selection', value: 'filtered' },
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
const strategyResult = await strategyResolver.resolve();
|
|
417
|
+
if (strategyResult.strategy === 'diet') {
|
|
418
|
+
useDiet = true;
|
|
419
|
+
}
|
|
420
|
+
else if (strategyResult.strategy === 'filtered') {
|
|
421
|
+
// Prompt for filters interactively
|
|
422
|
+
const filterResolver = new FlagResolver({
|
|
423
|
+
commandName: 'work spawn',
|
|
424
|
+
baseCommand: 'prlt work spawn',
|
|
425
|
+
jsonMode,
|
|
426
|
+
flags: {},
|
|
427
|
+
});
|
|
428
|
+
// Get unique categories from tickets for choices
|
|
429
|
+
const categories = [...new Set(allTickets.map(t => t.category).filter(Boolean))];
|
|
430
|
+
if (categories.length > 0) {
|
|
431
|
+
filterResolver.addPrompt({
|
|
432
|
+
flagName: 'filterCategory',
|
|
433
|
+
type: 'checkbox',
|
|
434
|
+
message: 'Filter by category (space to toggle, enter to continue):',
|
|
435
|
+
choices: () => categories.map(c => ({ name: c, value: c })),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// Get unique priorities
|
|
439
|
+
const priorities = [...new Set(allTickets.map(t => t.priority).filter(Boolean))];
|
|
440
|
+
if (priorities.length > 0) {
|
|
441
|
+
const priorityOrder = ['P0', 'P1', 'P2', 'P3'];
|
|
442
|
+
priorities.sort((a, b) => {
|
|
443
|
+
const ai = priorityOrder.indexOf(a);
|
|
444
|
+
const bi = priorityOrder.indexOf(b);
|
|
445
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
446
|
+
});
|
|
447
|
+
filterResolver.addPrompt({
|
|
448
|
+
flagName: 'filterPriority',
|
|
449
|
+
type: 'checkbox',
|
|
450
|
+
message: 'Filter by priority (space to toggle, enter to continue):',
|
|
451
|
+
choices: () => priorities.map(p => ({ name: p, value: p })),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// Get epics for choices
|
|
455
|
+
const epics = await this.storage.listEpics(projectId);
|
|
456
|
+
if (epics.length > 0) {
|
|
457
|
+
filterResolver.addPrompt({
|
|
458
|
+
flagName: 'filterEpic',
|
|
459
|
+
type: 'list',
|
|
460
|
+
message: 'Filter by epic:',
|
|
461
|
+
choices: () => [
|
|
462
|
+
{ name: jsonMode ? 'Any epic' : '(any)', value: '' },
|
|
463
|
+
...epics.map(e => ({ name: `${e.id} - ${e.title}`, value: e.id })),
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// Status filter
|
|
468
|
+
filterResolver.addPrompt({
|
|
469
|
+
flagName: 'filterStatus',
|
|
470
|
+
type: 'list',
|
|
471
|
+
message: 'Filter by status:',
|
|
472
|
+
choices: () => [
|
|
473
|
+
{ name: jsonMode ? 'Any status' : '(any)', value: '' },
|
|
474
|
+
...columnNames.map(name => {
|
|
475
|
+
const count = allTickets.filter(t => t.statusName === name).length;
|
|
476
|
+
return { name: `${name} (${count})`, value: name };
|
|
477
|
+
}),
|
|
478
|
+
],
|
|
479
|
+
});
|
|
480
|
+
const filterResult = await filterResolver.resolve();
|
|
481
|
+
// Apply interactive filter selections to flags for use below
|
|
482
|
+
if (filterResult.filterCategory && filterResult.filterCategory.length > 0) {
|
|
483
|
+
flags.category = filterResult.filterCategory.join(',');
|
|
484
|
+
}
|
|
485
|
+
if (filterResult.filterPriority && filterResult.filterPriority.length > 0) {
|
|
486
|
+
flags.priority = filterResult.filterPriority.join(',');
|
|
487
|
+
}
|
|
488
|
+
if (filterResult.filterEpic) {
|
|
489
|
+
flags.epic = filterResult.filterEpic;
|
|
490
|
+
}
|
|
491
|
+
if (filterResult.filterStatus) {
|
|
492
|
+
flags.status = filterResult.filterStatus;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Build candidate pool: start with all tickets, apply filters
|
|
497
|
+
let candidates = [...allTickets];
|
|
498
|
+
// Apply category filter
|
|
499
|
+
if (flags.category) {
|
|
500
|
+
const categoryList = flags.category.split(',').map(c => c.trim().toLowerCase());
|
|
501
|
+
candidates = candidates.filter(t => {
|
|
502
|
+
const ticketCat = (t.category || '').toLowerCase();
|
|
503
|
+
return categoryList.includes(ticketCat);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
// Apply priority filter
|
|
507
|
+
if (flags.priority) {
|
|
508
|
+
const priorityList = flags.priority.split(',').map(p => p.trim().toUpperCase());
|
|
509
|
+
candidates = candidates.filter(t => {
|
|
510
|
+
const ticketPriority = (t.priority || '').toUpperCase();
|
|
511
|
+
return priorityList.includes(ticketPriority);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// Apply epic filter
|
|
515
|
+
if (flags.epic) {
|
|
516
|
+
candidates = candidates.filter(t => t.epicId === flags.epic);
|
|
517
|
+
}
|
|
518
|
+
// Apply status filter
|
|
519
|
+
if (flags.status) {
|
|
520
|
+
const statusName = flags.status.toLowerCase();
|
|
521
|
+
candidates = candidates.filter(t => (t.statusName || '').toLowerCase() === statusName);
|
|
522
|
+
}
|
|
523
|
+
// Sort by position (rank order)
|
|
524
|
+
candidates.sort((a, b) => (a.position || 0) - (b.position || 0));
|
|
525
|
+
// Filter out blocked tickets
|
|
526
|
+
const unblockedCandidates = [];
|
|
527
|
+
let skippedBlocked = 0;
|
|
528
|
+
for (const ticket of candidates) {
|
|
529
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential dependency check
|
|
530
|
+
const blocked = await this.storage.isTicketBlocked(ticket.id);
|
|
531
|
+
if (blocked) {
|
|
532
|
+
skippedBlocked++;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
unblockedCandidates.push(ticket);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (unblockedCandidates.length === 0) {
|
|
539
|
+
db.close();
|
|
540
|
+
const msg = `No eligible tickets found${skippedBlocked > 0 ? ` (${skippedBlocked} blocked)` : ''}.`;
|
|
541
|
+
return handleError('NO_ELIGIBLE_TICKETS', msg);
|
|
542
|
+
}
|
|
543
|
+
// Select tickets using chosen strategy
|
|
544
|
+
if (useDiet) {
|
|
545
|
+
// Diet-balanced selection
|
|
546
|
+
const dietConfig = loadDietConfig(db);
|
|
547
|
+
ticketsToSpawn = this.selectDietBalanced(unblockedCandidates, targetCount, dietConfig);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
// Top ranked (position order) - just take the first N
|
|
551
|
+
ticketsToSpawn = unblockedCandidates.slice(0, targetCount);
|
|
552
|
+
}
|
|
553
|
+
if (ticketsToSpawn.length === 0) {
|
|
554
|
+
db.close();
|
|
555
|
+
return handleError('NO_TICKETS_SELECTED', 'No tickets could be selected with the given criteria.');
|
|
556
|
+
}
|
|
557
|
+
// Show preview
|
|
558
|
+
this.log('');
|
|
559
|
+
this.log(styles.header(`🚀 Spawn: ${ticketsToSpawn.length} ticket(s)${useDiet ? ' (diet-balanced)' : ''}`));
|
|
560
|
+
if (useDiet) {
|
|
561
|
+
const dietConfig = loadDietConfig(db);
|
|
562
|
+
// Show category breakdown
|
|
563
|
+
const catCounts = new Map();
|
|
564
|
+
for (const t of ticketsToSpawn) {
|
|
565
|
+
const cat = t.category || 'uncategorized';
|
|
566
|
+
catCounts.set(cat, (catCounts.get(cat) || 0) + 1);
|
|
567
|
+
}
|
|
568
|
+
const breakdown = [...catCounts.entries()].map(([cat, count]) => `${count}x ${cat}`).join(', ');
|
|
569
|
+
this.log(styles.muted(` Diet: ${formatDietConfig(dietConfig)}`));
|
|
570
|
+
this.log(styles.muted(` Breakdown: ${breakdown}`));
|
|
571
|
+
}
|
|
572
|
+
if (skippedBlocked > 0) {
|
|
573
|
+
this.log(styles.muted(` Skipped: ${skippedBlocked} blocked ticket(s)`));
|
|
574
|
+
}
|
|
575
|
+
this.log(styles.muted(` Tickets: ${ticketsToSpawn.map(t => t.id).join(', ')}`));
|
|
576
|
+
// Confirmation (unless --yes)
|
|
577
|
+
if (!flags.yes && !jsonMode) {
|
|
578
|
+
const confirmResolver = new FlagResolver({
|
|
579
|
+
commandName: 'work spawn',
|
|
580
|
+
baseCommand: 'prlt work spawn',
|
|
581
|
+
jsonMode,
|
|
582
|
+
flags: {},
|
|
583
|
+
});
|
|
584
|
+
confirmResolver.addPrompt({
|
|
585
|
+
flagName: 'confirmed',
|
|
586
|
+
type: 'list',
|
|
587
|
+
message: `Spawn ${ticketsToSpawn.length} ticket(s)?`,
|
|
588
|
+
choices: () => [
|
|
589
|
+
{ name: 'Yes', value: 'yes' },
|
|
590
|
+
{ name: 'No', value: 'no' },
|
|
591
|
+
],
|
|
592
|
+
});
|
|
593
|
+
const confirmResult = await confirmResolver.resolve();
|
|
594
|
+
if (confirmResult.confirmed === 'no') {
|
|
595
|
+
db.close();
|
|
596
|
+
this.log(styles.muted('Cancelled.'));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// In JSON mode without --yes, return confirmation_needed
|
|
601
|
+
if (jsonMode && !flags.yes) {
|
|
602
|
+
const metadata = createMetadata('work spawn', flags);
|
|
603
|
+
const ticketIds = ticketsToSpawn.map(t => t.id).join(' ');
|
|
604
|
+
let confirmCmd = `prlt work spawn ${ticketIds}`;
|
|
605
|
+
if (flags.action)
|
|
606
|
+
confirmCmd += ` --action ${flags.action}`;
|
|
607
|
+
if (flags.display)
|
|
608
|
+
confirmCmd += ` --display ${flags.display}`;
|
|
609
|
+
if (flags['run-on-host'])
|
|
610
|
+
confirmCmd += ' --run-on-host';
|
|
611
|
+
if (flags['skip-permissions'])
|
|
612
|
+
confirmCmd += ' --skip-permissions';
|
|
613
|
+
if (flags.executor)
|
|
614
|
+
confirmCmd += ` --executor ${flags.executor}`;
|
|
615
|
+
if (flags.session)
|
|
616
|
+
confirmCmd += ` --session ${flags.session}`;
|
|
617
|
+
if (flags['create-pr'])
|
|
618
|
+
confirmCmd += ' --create-pr';
|
|
619
|
+
if (flags['no-pr'])
|
|
620
|
+
confirmCmd += ' --no-pr';
|
|
621
|
+
if (flags.clone)
|
|
622
|
+
confirmCmd += ' --clone';
|
|
623
|
+
if (flags.focus)
|
|
624
|
+
confirmCmd += ' --focus';
|
|
625
|
+
confirmCmd += ' --yes';
|
|
626
|
+
const plan = {
|
|
627
|
+
tickets: ticketsToSpawn.map(t => ({
|
|
628
|
+
id: t.id,
|
|
629
|
+
title: t.title,
|
|
630
|
+
status: t.statusName,
|
|
631
|
+
category: t.category,
|
|
632
|
+
priority: t.priority,
|
|
633
|
+
})),
|
|
634
|
+
action: flags.action,
|
|
635
|
+
count: ticketsToSpawn.length,
|
|
636
|
+
diet: useDiet,
|
|
637
|
+
filters: {
|
|
638
|
+
category: flags.category || null,
|
|
639
|
+
priority: flags.priority || null,
|
|
640
|
+
epic: flags.epic || null,
|
|
641
|
+
status: flags.status || null,
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
db.close();
|
|
645
|
+
outputConfirmationNeededAsJson(plan, confirmCmd, `Ready to spawn ${ticketsToSpawn.length} agent(s). Run with --yes to execute.`, metadata);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
334
649
|
else if (spawnMode === 'all') {
|
|
335
650
|
// ALL MODE: Column picker, then spawn all tickets in that column
|
|
336
651
|
let targetColumn = flags.column;
|
|
@@ -655,10 +970,6 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
655
970
|
batchAction = 'custom';
|
|
656
971
|
batchCustomMessage = flags.message;
|
|
657
972
|
}
|
|
658
|
-
else if (flags.message && flags.action !== 'custom') {
|
|
659
|
-
// --message provided without --action custom - warn user
|
|
660
|
-
this.warn('--message flag is only used with --action custom, ignoring');
|
|
661
|
-
}
|
|
662
973
|
// Now fetch action details after selection is made
|
|
663
974
|
if (batchAction === 'custom') {
|
|
664
975
|
// Custom action - user provides their own prompt
|
|
@@ -1013,6 +1324,10 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
1013
1324
|
else if (batchAction) {
|
|
1014
1325
|
startArgs.push('--action', batchAction);
|
|
1015
1326
|
}
|
|
1327
|
+
// Pass --message for additional instructions (non-custom actions)
|
|
1328
|
+
if (flags.message && batchAction !== 'custom') {
|
|
1329
|
+
startArgs.push('--message', flags.message);
|
|
1330
|
+
}
|
|
1016
1331
|
}
|
|
1017
1332
|
else {
|
|
1018
1333
|
// Batch mode: pass all settings to skip prompts
|
|
@@ -1041,6 +1356,10 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
1041
1356
|
else {
|
|
1042
1357
|
startArgs.push('--action', batchAction || 'implement');
|
|
1043
1358
|
}
|
|
1359
|
+
// Pass --message for additional instructions (non-custom actions)
|
|
1360
|
+
if (flags.message && batchAction !== 'custom') {
|
|
1361
|
+
startArgs.push('--message', flags.message);
|
|
1362
|
+
}
|
|
1044
1363
|
// Pass session manager (tmux inside container by default)
|
|
1045
1364
|
if (flags.session)
|
|
1046
1365
|
startArgs.push('--session', flags.session);
|
|
@@ -1094,4 +1413,76 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
1094
1413
|
throw error;
|
|
1095
1414
|
}
|
|
1096
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Select tickets using diet-balanced category weighting.
|
|
1418
|
+
* Uses a two-pass algorithm:
|
|
1419
|
+
* Pass 1: Walk candidates in position order, pull if category not over ceiling.
|
|
1420
|
+
* Pass 2: Force-pull from underrepresented categories.
|
|
1421
|
+
*/
|
|
1422
|
+
selectDietBalanced(candidates, targetCount, dietConfig) {
|
|
1423
|
+
const selected = [];
|
|
1424
|
+
const selectedIds = new Set();
|
|
1425
|
+
// Build category count map (start from zero since we're selecting fresh)
|
|
1426
|
+
const categoryCounts = new Map();
|
|
1427
|
+
for (const ratio of dietConfig.ratios) {
|
|
1428
|
+
categoryCounts.set(ratio.category, 0);
|
|
1429
|
+
}
|
|
1430
|
+
// Calculate ceiling per category
|
|
1431
|
+
const getCeiling = (category) => {
|
|
1432
|
+
const ratio = dietConfig.ratios.find(r => r.category === category);
|
|
1433
|
+
if (!ratio)
|
|
1434
|
+
return targetCount; // No ceiling for uncategorized
|
|
1435
|
+
return Math.ceil(targetCount * ratio.target);
|
|
1436
|
+
};
|
|
1437
|
+
// Pass 1: Walk candidates in order, pull if under ceiling
|
|
1438
|
+
const skipped = [];
|
|
1439
|
+
for (const ticket of candidates) {
|
|
1440
|
+
if (selected.length >= targetCount)
|
|
1441
|
+
break;
|
|
1442
|
+
const cat = (ticket.category || '').toLowerCase();
|
|
1443
|
+
const currentCount = categoryCounts.get(cat) || 0;
|
|
1444
|
+
const ceiling = getCeiling(cat);
|
|
1445
|
+
if (currentCount < ceiling) {
|
|
1446
|
+
selected.push(ticket);
|
|
1447
|
+
selectedIds.add(ticket.id);
|
|
1448
|
+
categoryCounts.set(cat, currentCount + 1);
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
skipped.push(ticket);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
// Pass 2: Force-pull from underrepresented categories
|
|
1455
|
+
if (selected.length < targetCount) {
|
|
1456
|
+
for (const ratio of dietConfig.ratios) {
|
|
1457
|
+
if (selected.length >= targetCount)
|
|
1458
|
+
break;
|
|
1459
|
+
const currentCount = categoryCounts.get(ratio.category) || 0;
|
|
1460
|
+
const targetForCat = Math.ceil(targetCount * ratio.target);
|
|
1461
|
+
if (currentCount < targetForCat) {
|
|
1462
|
+
const catTickets = skipped.filter(t => (t.category || '').toLowerCase() === ratio.category && !selectedIds.has(t.id));
|
|
1463
|
+
for (const ticket of catTickets) {
|
|
1464
|
+
if (selected.length >= targetCount)
|
|
1465
|
+
break;
|
|
1466
|
+
if ((categoryCounts.get(ratio.category) || 0) >= targetForCat)
|
|
1467
|
+
break;
|
|
1468
|
+
selected.push(ticket);
|
|
1469
|
+
selectedIds.add(ticket.id);
|
|
1470
|
+
categoryCounts.set(ratio.category, (categoryCounts.get(ratio.category) || 0) + 1);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
// If still under target, fill from any remaining candidates
|
|
1476
|
+
if (selected.length < targetCount) {
|
|
1477
|
+
for (const ticket of candidates) {
|
|
1478
|
+
if (selected.length >= targetCount)
|
|
1479
|
+
break;
|
|
1480
|
+
if (!selectedIds.has(ticket.id)) {
|
|
1481
|
+
selected.push(ticket);
|
|
1482
|
+
selectedIds.add(ticket.id);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return selected;
|
|
1487
|
+
}
|
|
1097
1488
|
}
|
|
@@ -11,6 +11,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
11
11
|
executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
prompt: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
watch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
'vm-host': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -109,6 +109,9 @@ export default class WorkStart extends PMOCommand {
|
|
|
109
109
|
char: 'p',
|
|
110
110
|
description: 'Custom prompt (overrides action)',
|
|
111
111
|
}),
|
|
112
|
+
message: Flags.string({
|
|
113
|
+
description: 'Additional instructions appended to any action prompt',
|
|
114
|
+
}),
|
|
112
115
|
watch: Flags.boolean({
|
|
113
116
|
char: 'w',
|
|
114
117
|
description: 'Stream output in real-time',
|
|
@@ -779,6 +782,8 @@ export default class WorkStart extends PMOCommand {
|
|
|
779
782
|
actionPrompt: customPrompt || selectedAction?.prompt,
|
|
780
783
|
actionEndPrompt: customPrompt ? undefined : selectedAction?.endPrompt,
|
|
781
784
|
modifiesCode: customPrompt ? true : selectedAction?.modifiesCode ?? true,
|
|
785
|
+
// Additional instructions from --message flag
|
|
786
|
+
customMessage: flags.message,
|
|
782
787
|
};
|
|
783
788
|
// Check if agent has devcontainer config
|
|
784
789
|
const hasDevcontainer = hasDevcontainerConfig(agentDir);
|
|
@@ -214,6 +214,10 @@ function buildPrompt(context) {
|
|
|
214
214
|
}
|
|
215
215
|
// Note: Branch setup (fetch + checkout/create) is now handled programmatically
|
|
216
216
|
// in work/start.ts before the agent spawns, so no prompt instructions needed
|
|
217
|
+
// Additional instructions from --message flag (appended to any action)
|
|
218
|
+
if (context.customMessage) {
|
|
219
|
+
prompt += `\n## Additional Instructions\n\n${context.customMessage}\n`;
|
|
220
|
+
}
|
|
217
221
|
// END HOOK - Action-specific completion instructions
|
|
218
222
|
prompt += `\n---\n\n## When Complete\n\n`;
|
|
219
223
|
// For revisions, use the revision-specific end prompt
|