@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.
Files changed (43) 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 +397 -6
  24. package/dist/commands/work/start.d.ts +1 -0
  25. package/dist/commands/work/start.js +5 -0
  26. package/dist/lib/execution/runners.js +4 -0
  27. package/dist/lib/execution/types.d.ts +1 -0
  28. package/dist/lib/mcp/tools/diet.d.ts +6 -0
  29. package/dist/lib/mcp/tools/diet.js +261 -0
  30. package/dist/lib/mcp/tools/index.d.ts +1 -0
  31. package/dist/lib/mcp/tools/index.js +1 -0
  32. package/dist/lib/mcp/tools/template.js +1 -1
  33. package/dist/lib/mcp/tools/ticket.js +48 -3
  34. package/dist/lib/pmo/diet.d.ts +102 -0
  35. package/dist/lib/pmo/diet.js +127 -0
  36. package/dist/lib/pmo/storage/base.d.ts +5 -0
  37. package/dist/lib/pmo/storage/base.js +16 -0
  38. package/dist/lib/pmo/types.d.ts +12 -6
  39. package/dist/lib/pmo/types.js +6 -2
  40. package/dist/lib/pmo/utils.d.ts +40 -0
  41. package/dist/lib/pmo/utils.js +76 -0
  42. package/oclif.manifest.json +2783 -2438
  43. 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: 'Custom prompt/message for the agent (use with --action custom)',
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, or Args (positional ticket IDs)
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
@@ -86,6 +86,7 @@ export interface ExecutionContext {
86
86
  actionPrompt?: string;
87
87
  actionEndPrompt?: string;
88
88
  modifiesCode?: boolean;
89
+ customMessage?: string;
89
90
  prFeedback?: string;
90
91
  isRevision?: boolean;
91
92
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MCP Diet & Pull Tools
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import type { McpToolContext } from '../types.js';
6
+ export declare function registerDietTools(server: McpServer, ctx: McpToolContext): void;