@proletariat/cli 0.3.19 → 0.3.21

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 (81) hide show
  1. package/dist/commands/agent/login.js +2 -2
  2. package/dist/commands/agent/remove.d.ts +1 -0
  3. package/dist/commands/agent/remove.js +36 -28
  4. package/dist/commands/agent/shell.js +2 -2
  5. package/dist/commands/agent/staff/remove.d.ts +1 -0
  6. package/dist/commands/agent/staff/remove.js +36 -28
  7. package/dist/commands/agent/status.js +2 -2
  8. package/dist/commands/agent/temp/cleanup.js +10 -17
  9. package/dist/commands/agent/themes/add-names.d.ts +1 -0
  10. package/dist/commands/agent/themes/add-names.js +5 -1
  11. package/dist/commands/agent/visit.js +2 -2
  12. package/dist/commands/board/view.d.ts +15 -0
  13. package/dist/commands/board/view.js +136 -0
  14. package/dist/commands/config/index.js +6 -3
  15. package/dist/commands/epic/link/index.js +17 -0
  16. package/dist/commands/execution/config.d.ts +34 -0
  17. package/dist/commands/execution/config.js +433 -0
  18. package/dist/commands/execution/index.js +6 -1
  19. package/dist/commands/execution/kill.d.ts +12 -0
  20. package/dist/commands/execution/kill.js +17 -0
  21. package/dist/commands/execution/list.js +5 -4
  22. package/dist/commands/execution/logs.js +1 -0
  23. package/dist/commands/execution/view.d.ts +17 -0
  24. package/dist/commands/execution/view.js +288 -0
  25. package/dist/commands/phase/move.js +8 -0
  26. package/dist/commands/phase/template/apply.js +2 -2
  27. package/dist/commands/phase/template/create.js +67 -20
  28. package/dist/commands/phase/template/list.js +1 -1
  29. package/dist/commands/pr/index.js +6 -2
  30. package/dist/commands/pr/list.d.ts +17 -0
  31. package/dist/commands/pr/list.js +163 -0
  32. package/dist/commands/project/update.d.ts +19 -0
  33. package/dist/commands/project/update.js +163 -0
  34. package/dist/commands/roadmap/create.js +5 -0
  35. package/dist/commands/spec/delete.d.ts +18 -0
  36. package/dist/commands/spec/delete.js +111 -0
  37. package/dist/commands/spec/edit.d.ts +23 -0
  38. package/dist/commands/spec/edit.js +232 -0
  39. package/dist/commands/spec/index.js +5 -0
  40. package/dist/commands/status/create.js +38 -34
  41. package/dist/commands/status/list.js +5 -3
  42. package/dist/commands/template/phase/create.d.ts +1 -0
  43. package/dist/commands/template/phase/create.js +10 -1
  44. package/dist/commands/template/phase/index.js +4 -4
  45. package/dist/commands/template/ticket/create.d.ts +20 -0
  46. package/dist/commands/template/ticket/create.js +87 -0
  47. package/dist/commands/template/ticket/delete.d.ts +1 -1
  48. package/dist/commands/template/ticket/delete.js +4 -2
  49. package/dist/commands/template/ticket/save.d.ts +2 -0
  50. package/dist/commands/template/ticket/save.js +11 -0
  51. package/dist/commands/ticket/create.js +8 -1
  52. package/dist/commands/ticket/edit.js +1 -1
  53. package/dist/commands/ticket/list.d.ts +2 -0
  54. package/dist/commands/ticket/list.js +39 -2
  55. package/dist/commands/ticket/template/create.d.ts +9 -1
  56. package/dist/commands/ticket/template/create.js +224 -52
  57. package/dist/commands/ticket/template/save.d.ts +2 -1
  58. package/dist/commands/ticket/template/save.js +58 -7
  59. package/dist/commands/ticket/update.js +2 -2
  60. package/dist/commands/work/ready.js +8 -8
  61. package/dist/commands/work/spawn.js +32 -8
  62. package/dist/commands/work/watch.js +2 -0
  63. package/dist/lib/agents/commands.d.ts +7 -0
  64. package/dist/lib/agents/commands.js +11 -0
  65. package/dist/lib/agents/index.js +14 -4
  66. package/dist/lib/branch/index.js +24 -0
  67. package/dist/lib/execution/config.d.ts +2 -0
  68. package/dist/lib/execution/config.js +12 -0
  69. package/dist/lib/execution/runners.js +1 -2
  70. package/dist/lib/pmo/storage/epics.js +20 -10
  71. package/dist/lib/pmo/storage/helpers.d.ts +10 -0
  72. package/dist/lib/pmo/storage/helpers.js +59 -1
  73. package/dist/lib/pmo/storage/projects.js +20 -8
  74. package/dist/lib/pmo/storage/specs.js +23 -13
  75. package/dist/lib/pmo/storage/statuses.js +39 -18
  76. package/dist/lib/pmo/storage/subtasks.js +19 -8
  77. package/dist/lib/pmo/storage/tickets.js +27 -15
  78. package/dist/lib/pmo/utils.d.ts +4 -2
  79. package/dist/lib/pmo/utils.js +4 -2
  80. package/oclif.manifest.json +4037 -3234
  81. package/package.json +2 -4
@@ -3,12 +3,15 @@ import inquirer from 'inquirer';
3
3
  import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
4
4
  import { PRIORITIES, PRIORITY_LABELS, TICKET_CATEGORIES } from '../../../lib/pmo/types.js';
5
5
  import { styles } from '../../../lib/styles.js';
6
+ import { shouldOutputJson, outputSuccessAsJson, outputPromptAsJson, buildFormPromptConfig, createMetadata, } from '../../../lib/prompt-json.js';
6
7
  export default class TicketTemplateCreate extends PMOCommand {
7
8
  static description = 'Create a new ticket template from scratch';
8
9
  static examples = [
9
10
  '<%= config.bin %> <%= command.id %> "Bug Report"',
10
11
  '<%= config.bin %> <%= command.id %> "Feature Request" -d "Template for new features"',
11
12
  '<%= config.bin %> <%= command.id %> "Task" --title-pattern "[TASK] " --priority P2',
13
+ '<%= config.bin %> <%= command.id %> "Onboarding" --subtask "Setup environment" --subtask "Read docs"',
14
+ '<%= config.bin %> <%= command.id %> "Bug" --ac "Bug is fixed" --ac "Tests pass" --category bug',
12
15
  ];
13
16
  static args = {
14
17
  name: Args.string({
@@ -25,6 +28,9 @@ export default class TicketTemplateCreate extends PMOCommand {
25
28
  'title-pattern': Flags.string({
26
29
  description: 'Default title prefix/pattern (e.g., "[BUG] ")',
27
30
  }),
31
+ 'description-template': Flags.string({
32
+ description: 'Default description template (markdown)',
33
+ }),
28
34
  priority: Flags.string({
29
35
  char: 'p',
30
36
  description: 'Default priority',
@@ -35,12 +41,141 @@ export default class TicketTemplateCreate extends PMOCommand {
35
41
  description: 'Default category',
36
42
  options: [...TICKET_CATEGORIES],
37
43
  }),
44
+ subtask: Flags.string({
45
+ description: 'Add a suggested subtask (can be used multiple times)',
46
+ multiple: true,
47
+ }),
48
+ ac: Flags.string({
49
+ description: 'Add an acceptance criterion pattern (can be used multiple times)',
50
+ multiple: true,
51
+ }),
52
+ label: Flags.string({
53
+ char: 'l',
54
+ description: 'Add a default label (can be used multiple times)',
55
+ multiple: true,
56
+ }),
57
+ json: Flags.boolean({
58
+ description: 'Output prompt configuration as JSON (for AI agents/scripts)',
59
+ default: false,
60
+ }),
38
61
  };
39
62
  getPMOOptions() {
40
63
  return { promptIfMultiple: false };
41
64
  }
42
65
  async execute() {
43
66
  const { args, flags } = await this.parse(TicketTemplateCreate);
67
+ const jsonMode = shouldOutputJson(flags);
68
+ // Check if we have all required data via flags (non-interactive mode)
69
+ const hasName = Boolean(args.name);
70
+ const hasAllFlags = hasName; // Name is the only required field
71
+ // In JSON mode with missing required data, output form prompt
72
+ if (jsonMode && !hasAllFlags) {
73
+ const fields = [];
74
+ if (!hasName) {
75
+ fields.push({
76
+ type: 'input',
77
+ name: 'name',
78
+ message: 'Template name:',
79
+ });
80
+ }
81
+ // Add optional fields that weren't provided
82
+ if (flags.description === undefined) {
83
+ fields.push({
84
+ type: 'input',
85
+ name: 'description',
86
+ message: 'Template description (optional):',
87
+ });
88
+ }
89
+ if (flags['title-pattern'] === undefined) {
90
+ fields.push({
91
+ type: 'input',
92
+ name: 'titlePattern',
93
+ message: 'Title prefix/pattern (optional, e.g., "[BUG] "):',
94
+ });
95
+ }
96
+ if (flags.priority === undefined) {
97
+ fields.push({
98
+ type: 'list',
99
+ name: 'priority',
100
+ message: 'Default priority:',
101
+ choices: [
102
+ { name: 'None', value: '' },
103
+ ...PRIORITIES.map(p => ({ name: PRIORITY_LABELS[p], value: p })),
104
+ ],
105
+ });
106
+ }
107
+ if (flags.category === undefined) {
108
+ fields.push({
109
+ type: 'list',
110
+ name: 'category',
111
+ message: 'Default category:',
112
+ choices: [
113
+ { name: 'None', value: '' },
114
+ ...TICKET_CATEGORIES.map(c => ({ name: c, value: c })),
115
+ ],
116
+ });
117
+ }
118
+ outputPromptAsJson(buildFormPromptConfig(fields), createMetadata('ticket template create', flags));
119
+ return; // outputPromptAsJson exits, but TypeScript needs this
120
+ }
121
+ // Non-interactive mode: use flag values directly
122
+ if (hasAllFlags && (jsonMode || this.hasNonDefaultFlags(flags))) {
123
+ const name = args.name;
124
+ const description = flags.description;
125
+ const titlePattern = flags['title-pattern'];
126
+ const priority = flags.priority;
127
+ const category = flags.category;
128
+ const subtasks = flags.subtask || [];
129
+ const acs = flags.ac || [];
130
+ const labels = flags.label || [];
131
+ // Build description template with ACs if provided
132
+ let descriptionTemplate = flags['description-template'];
133
+ if (acs.length > 0 && !descriptionTemplate) {
134
+ // Create a description template with acceptance criteria section
135
+ descriptionTemplate = '## Description\n\n## Acceptance Criteria\n' +
136
+ acs.map(ac => `- [ ] ${ac}`).join('\n') + '\n';
137
+ }
138
+ else if (acs.length > 0 && descriptionTemplate) {
139
+ // Append ACs to existing template if it doesn't have an AC section
140
+ if (!descriptionTemplate.toLowerCase().includes('acceptance criteria')) {
141
+ descriptionTemplate += '\n\n## Acceptance Criteria\n' +
142
+ acs.map(ac => `- [ ] ${ac}`).join('\n') + '\n';
143
+ }
144
+ }
145
+ // Create the template
146
+ const template = await this.storage.createTicketTemplate({
147
+ name,
148
+ description,
149
+ titlePattern,
150
+ defaultPriority: priority,
151
+ defaultCategory: category,
152
+ descriptionTemplate,
153
+ suggestedSubtasks: subtasks.map(title => ({ title })),
154
+ defaultLabels: labels,
155
+ });
156
+ if (jsonMode) {
157
+ outputSuccessAsJson({
158
+ template: {
159
+ id: template.id,
160
+ name: template.name,
161
+ description: template.description,
162
+ titlePattern: template.titlePattern,
163
+ defaultPriority: template.defaultPriority,
164
+ defaultCategory: template.defaultCategory,
165
+ descriptionTemplate: template.descriptionTemplate,
166
+ suggestedSubtasks: template.suggestedSubtasks,
167
+ defaultLabels: template.defaultLabels,
168
+ },
169
+ }, createMetadata('ticket template create', flags));
170
+ return;
171
+ }
172
+ this.log(styles.success(`\nCreated template "${styles.emphasis(template.name)}"`));
173
+ this.log(styles.muted(` ID: ${template.id}`));
174
+ this.log('');
175
+ this.log(styles.muted(`Create ticket from template: prlt ticket template apply ${template.id}`));
176
+ return;
177
+ }
178
+ // Interactive mode
44
179
  // Get template name
45
180
  let name = args.name;
46
181
  if (!name) {
@@ -100,61 +235,79 @@ export default class TicketTemplateCreate extends PMOCommand {
100
235
  }]);
101
236
  category = selectedCategory || undefined;
102
237
  }
103
- // Ask about description template
104
- const { wantDescriptionTemplate } = await inquirer.prompt([{
105
- type: 'list',
106
- name: 'wantDescriptionTemplate',
107
- message: 'Add a description template?',
108
- choices: [
109
- { name: 'No', value: false },
110
- { name: 'Yes', value: true },
111
- ],
112
- }]);
113
- let descriptionTemplate;
114
- if (wantDescriptionTemplate) {
115
- const { template } = await inquirer.prompt([{
116
- type: 'editor',
117
- name: 'template',
118
- message: 'Description template (opens editor):',
119
- default: `## Summary\n\n## Details\n\n## Acceptance Criteria\n- [ ] \n`,
238
+ // Handle description template - use flag value or prompt
239
+ let descriptionTemplate = flags['description-template'];
240
+ if (descriptionTemplate === undefined) {
241
+ const { wantDescriptionTemplate } = await inquirer.prompt([{
242
+ type: 'list',
243
+ name: 'wantDescriptionTemplate',
244
+ message: 'Add a description template?',
245
+ choices: [
246
+ { name: 'No', value: false },
247
+ { name: 'Yes', value: true },
248
+ ],
120
249
  }]);
121
- descriptionTemplate = template || undefined;
122
- }
123
- // Ask about default subtasks
124
- const subtasks = [];
125
- const { wantSubtasks } = await inquirer.prompt([{
126
- type: 'list',
127
- name: 'wantSubtasks',
128
- message: 'Add default subtasks?',
129
- choices: [
130
- { name: 'No', value: false },
131
- { name: 'Yes', value: true },
132
- ],
133
- }]);
134
- if (wantSubtasks) {
135
- let addMore = true;
136
- while (addMore) {
137
- // eslint-disable-next-line no-await-in-loop -- Interactive loop for subtask creation
138
- const { subtaskTitle } = await inquirer.prompt([{
139
- type: 'input',
140
- name: 'subtaskTitle',
141
- message: 'Subtask title:',
142
- validate: (input) => input.length > 0 || 'Title is required',
143
- }]);
144
- subtasks.push(subtaskTitle);
145
- // eslint-disable-next-line no-await-in-loop -- Interactive loop continuation
146
- const { another } = await inquirer.prompt([{
147
- type: 'list',
148
- name: 'another',
149
- message: 'Add another subtask?',
150
- choices: [
151
- { name: 'No', value: false },
152
- { name: 'Yes', value: true },
153
- ],
250
+ if (wantDescriptionTemplate) {
251
+ const { template } = await inquirer.prompt([{
252
+ type: 'editor',
253
+ name: 'template',
254
+ message: 'Description template (opens editor):',
255
+ default: `## Summary\n\n## Details\n\n## Acceptance Criteria\n- [ ] \n`,
154
256
  }]);
155
- addMore = another;
257
+ descriptionTemplate = template || undefined;
156
258
  }
157
259
  }
260
+ // Handle subtasks - use flag values or prompt
261
+ const subtasks = flags.subtask ? [...flags.subtask] : [];
262
+ if (subtasks.length === 0) {
263
+ const { wantSubtasks } = await inquirer.prompt([{
264
+ type: 'list',
265
+ name: 'wantSubtasks',
266
+ message: 'Add default subtasks?',
267
+ choices: [
268
+ { name: 'No', value: false },
269
+ { name: 'Yes', value: true },
270
+ ],
271
+ }]);
272
+ if (wantSubtasks) {
273
+ let addMore = true;
274
+ while (addMore) {
275
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop for subtask creation
276
+ const { subtaskTitle } = await inquirer.prompt([{
277
+ type: 'input',
278
+ name: 'subtaskTitle',
279
+ message: 'Subtask title:',
280
+ validate: (input) => input.length > 0 || 'Title is required',
281
+ }]);
282
+ subtasks.push(subtaskTitle);
283
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop continuation
284
+ const { another } = await inquirer.prompt([{
285
+ type: 'list',
286
+ name: 'another',
287
+ message: 'Add another subtask?',
288
+ choices: [
289
+ { name: 'No', value: false },
290
+ { name: 'Yes', value: true },
291
+ ],
292
+ }]);
293
+ addMore = another;
294
+ }
295
+ }
296
+ }
297
+ // Handle ACs - add to description template if provided via flag
298
+ const acs = flags.ac || [];
299
+ if (acs.length > 0) {
300
+ if (!descriptionTemplate) {
301
+ descriptionTemplate = '## Description\n\n## Acceptance Criteria\n' +
302
+ acs.map(ac => `- [ ] ${ac}`).join('\n') + '\n';
303
+ }
304
+ else if (!descriptionTemplate.toLowerCase().includes('acceptance criteria')) {
305
+ descriptionTemplate += '\n\n## Acceptance Criteria\n' +
306
+ acs.map(ac => `- [ ] ${ac}`).join('\n') + '\n';
307
+ }
308
+ }
309
+ // Handle labels
310
+ const labels = flags.label ? [...flags.label] : [];
158
311
  // Show preview
159
312
  this.log(`\n${styles.emphasis('Template Preview:')}`);
160
313
  this.log(styles.muted(` Name: ${name}`));
@@ -179,6 +332,9 @@ export default class TicketTemplateCreate extends PMOCommand {
179
332
  this.log(styles.muted(` - ${subtask}`));
180
333
  }
181
334
  }
335
+ if (labels.length > 0) {
336
+ this.log(styles.muted(` Default labels: ${labels.join(', ')}`));
337
+ }
182
338
  // Confirm creation
183
339
  const { confirm } = await inquirer.prompt([{
184
340
  type: 'list',
@@ -202,11 +358,27 @@ export default class TicketTemplateCreate extends PMOCommand {
202
358
  defaultCategory: category,
203
359
  descriptionTemplate,
204
360
  suggestedSubtasks: subtasks.map(title => ({ title })),
205
- defaultLabels: [],
361
+ defaultLabels: labels,
206
362
  });
207
363
  this.log(styles.success(`\nCreated template "${styles.emphasis(template.name)}"`));
208
364
  this.log(styles.muted(` ID: ${template.id}`));
209
365
  this.log('');
210
366
  this.log(styles.muted(`Create ticket from template: prlt ticket template apply ${template.id}`));
211
367
  }
368
+ /**
369
+ * Check if any non-default flags were provided (indicating non-interactive intent)
370
+ */
371
+ hasNonDefaultFlags(flags) {
372
+ const nonDefaultFlags = [
373
+ 'description',
374
+ 'title-pattern',
375
+ 'description-template',
376
+ 'priority',
377
+ 'category',
378
+ 'subtask',
379
+ 'ac',
380
+ 'label',
381
+ ];
382
+ return nonDefaultFlags.some(flag => flags[flag] !== undefined);
383
+ }
212
384
  }
@@ -7,9 +7,10 @@ export default class TicketTemplateSave extends PMOCommand {
7
7
  name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
8
  };
9
9
  static flags: {
10
+ 'template-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
- machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  };
15
16
  execute(): Promise<void>;
@@ -2,6 +2,7 @@ import { Flags, Args } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
3
  import { PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
4
4
  import { styles } from '../../../lib/styles.js';
5
+ import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../../lib/prompt-json.js';
5
6
  export default class TicketTemplateSave extends PMOCommand {
6
7
  static description = 'Create a template from an existing ticket';
7
8
  static examples = [
@@ -20,20 +21,45 @@ export default class TicketTemplateSave extends PMOCommand {
20
21
  };
21
22
  static flags = {
22
23
  ...pmoBaseFlags,
24
+ 'template-name': Flags.string({
25
+ char: 'n',
26
+ description: 'Template name (alternative to positional arg, required in non-TTY/JSON mode)',
27
+ }),
23
28
  description: Flags.string({
24
29
  char: 'd',
25
30
  description: 'Template description',
26
31
  }),
32
+ json: Flags.boolean({
33
+ description: 'Output prompt configuration as JSON (for AI agents/scripts)',
34
+ default: false,
35
+ }),
27
36
  };
28
37
  async execute() {
29
38
  const { args, flags } = await this.parse(TicketTemplateSave);
39
+ // Check if JSON output mode is active
40
+ const jsonMode = shouldOutputJson(flags);
41
+ // Helper to handle errors in JSON mode
42
+ const handleError = (code, message) => {
43
+ if (jsonMode) {
44
+ outputErrorAsJson(code, message, createMetadata('ticket template save', flags));
45
+ }
46
+ this.error(message);
47
+ };
30
48
  // Get ticket ID - prompt with picker if not provided
31
49
  let ticketId = args.ticket;
32
50
  if (!ticketId) {
33
51
  const projectId = await this.requireProject();
34
52
  const tickets = await this.storage.listTickets(projectId);
35
53
  if (tickets.length === 0) {
36
- this.error('No tickets found in this project.\nCreate a ticket first: prlt ticket create');
54
+ return handleError('NO_TICKETS', 'No tickets found in this project.\nCreate a ticket first: prlt ticket create');
55
+ }
56
+ // In JSON mode, output prompt config for ticket selection
57
+ if (jsonMode) {
58
+ const choices = tickets.slice(0, 20).map(t => ({
59
+ name: `${t.id} - ${t.title}`,
60
+ value: t.id,
61
+ }));
62
+ outputPromptAsJson(buildPromptConfig('list', 'ticket', 'Select a ticket to save as template:', choices), createMetadata('ticket template save', flags));
37
63
  }
38
64
  const { selectedTicket } = await inquirer.prompt([{
39
65
  type: 'list',
@@ -49,23 +75,28 @@ export default class TicketTemplateSave extends PMOCommand {
49
75
  // Verify ticket exists
50
76
  const ticket = await this.storage.getTicket(ticketId);
51
77
  if (!ticket) {
52
- this.error(`Ticket not found: ${ticketId}\nRun 'prlt ticket list' to see available tickets.`);
78
+ return handleError('TICKET_NOT_FOUND', `Ticket not found: ${ticketId}\nRun 'prlt ticket list' to see available tickets.`);
53
79
  }
54
- // Get template name - prompt if not provided
55
- let templateName = args.name;
80
+ // Get template name - prefer flag over positional arg
81
+ let templateName = flags['template-name'] || args.name;
56
82
  if (!templateName) {
83
+ const defaultName = ticket.category || ticket.title.split(' ')[0];
84
+ // In JSON mode, output prompt config for template name
85
+ if (jsonMode) {
86
+ outputPromptAsJson(buildPromptConfig('input', 'name', 'Template name:', undefined, defaultName), createMetadata('ticket template save', flags));
87
+ }
57
88
  const { name } = await inquirer.prompt([{
58
89
  type: 'input',
59
90
  name: 'name',
60
91
  message: 'Template name:',
61
- default: ticket.category || ticket.title.split(' ')[0],
92
+ default: defaultName,
62
93
  validate: (input) => input.length > 0 || 'Name is required',
63
94
  }]);
64
95
  templateName = name;
65
96
  }
66
- // Get description if not provided
97
+ // Get description if not provided - only prompt interactively (not in JSON mode)
67
98
  let description = flags.description;
68
- if (description === undefined) {
99
+ if (description === undefined && !jsonMode) {
69
100
  const { desc } = await inquirer.prompt([{
70
101
  type: 'input',
71
102
  name: 'desc',
@@ -75,6 +106,26 @@ export default class TicketTemplateSave extends PMOCommand {
75
106
  }
76
107
  // Create template from ticket
77
108
  const template = await this.storage.createTicketTemplateFromTicket(ticketId, templateName, description);
109
+ // In JSON mode, output success as JSON
110
+ if (jsonMode) {
111
+ outputSuccessAsJson({
112
+ template: {
113
+ id: template.id,
114
+ name: template.name,
115
+ description: template.description,
116
+ titlePattern: template.titlePattern,
117
+ defaultPriority: template.defaultPriority,
118
+ defaultCategory: template.defaultCategory,
119
+ defaultStatusId: template.defaultStatusId,
120
+ defaultAssignee: template.defaultAssignee,
121
+ defaultOwner: template.defaultOwner,
122
+ defaultLabels: template.defaultLabels,
123
+ suggestedSubtasks: template.suggestedSubtasks,
124
+ },
125
+ sourceTicketId: ticketId,
126
+ }, createMetadata('ticket template save', flags));
127
+ return;
128
+ }
78
129
  this.log(styles.success(`\nCreated template "${styles.emphasis(template.name)}" from ticket ${ticketId}`));
79
130
  this.log(styles.muted(` ID: ${template.id}`));
80
131
  if (template.description) {
@@ -7,10 +7,10 @@ import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/
7
7
  export default class TicketUpdate extends PMOCommand {
8
8
  static description = 'Update priority/category for ticket(s)';
9
9
  static examples = [
10
- '<%= config.bin %> <%= command.id %> TKT-001 --priority HIGH',
10
+ '<%= config.bin %> <%= command.id %> TKT-001 --priority P1',
11
11
  '<%= config.bin %> <%= command.id %> TKT-001 --category bug',
12
12
  '<%= config.bin %> <%= command.id %> --bulk',
13
- '<%= config.bin %> <%= command.id %> --bulk --priority HIGH',
13
+ '<%= config.bin %> <%= command.id %> --bulk --priority P1',
14
14
  '<%= config.bin %> <%= command.id %> --json # Output choices as JSON',
15
15
  ];
16
16
  static args = {
@@ -106,18 +106,18 @@ export default class WorkReady extends PMOCommand {
106
106
  this.error(`Ticket "${ticketId}" not found.`);
107
107
  }
108
108
  // Get configured column name (from pmo_settings or default)
109
- // In Linear-style workflow, "ready" moves ticket to Done (review is implicit via PR)
110
- const targetColumnName = getWorkColumnSetting(db, 'done');
109
+ // "ready" moves ticket to Review column (work complete moves to Done)
110
+ const targetColumnName = getWorkColumnSetting(db, 'review');
111
111
  const board = await this.storage.getBoard(ticket.projectId);
112
112
  const columnNames = board.columns.map(col => col.name);
113
- const doneColumn = findColumnByName(columnNames, targetColumnName);
114
- if (!doneColumn) {
113
+ const reviewColumn = findColumnByName(columnNames, targetColumnName);
114
+ if (!reviewColumn) {
115
115
  db.close();
116
- this.error(`No "${targetColumnName}" column found in board configuration. Configure with: prlt config set column_done <column-name>`);
116
+ this.error(`No "${targetColumnName}" column found in board configuration. Configure with: prlt config set column_review <column-name>`);
117
117
  }
118
118
  const previousColumn = ticket.statusName;
119
- // Move to Done column (moveTicket also updates status_id)
120
- await this.storage.moveTicket(ticket.projectId, ticketId, doneColumn);
119
+ // Move to Review column (moveTicket also updates status_id)
120
+ await this.storage.moveTicket(ticket.projectId, ticketId, reviewColumn);
121
121
  // Auto-export to board.md if configured
122
122
  await autoExportToBoard(this.pmoPath, this.storage);
123
123
  // Mark any running executions for this ticket as completed
@@ -172,7 +172,7 @@ export default class WorkReady extends PMOCommand {
172
172
  this.log(styles.success(`Work ready: ${ticketId}`));
173
173
  this.log(styles.muted(` Title: ${ticket.title}`));
174
174
  this.log(styles.muted(` From: ${previousColumn}`));
175
- this.log(styles.muted(` To: ${doneColumn}`));
175
+ this.log(styles.muted(` To: ${reviewColumn}`));
176
176
  if (prUrl) {
177
177
  this.log(styles.muted(` PR: ${prUrl}`));
178
178
  }
@@ -119,14 +119,6 @@ export default class WorkSpawn extends PMOCommand {
119
119
  const { flags, argv } = await this.parse(WorkSpawn);
120
120
  // Check if JSON output mode is active
121
121
  const jsonMode = shouldOutputJson(flags);
122
- // This command requires project context (pass JSON mode config for AI agents)
123
- const projectId = await this.requireProject({
124
- jsonMode: {
125
- flags,
126
- commandName: 'work spawn',
127
- baseCommand: 'prlt work spawn',
128
- },
129
- });
130
122
  // Helper to handle errors in JSON mode
131
123
  const handleError = (code, message) => {
132
124
  if (jsonMode) {
@@ -137,6 +129,38 @@ export default class WorkSpawn extends PMOCommand {
137
129
  };
138
130
  // Parse ticket IDs from args (everything after flags)
139
131
  const ticketIdArgs = argv;
132
+ // Try to infer project from ticket IDs if provided
133
+ let projectId;
134
+ if (ticketIdArgs.length > 0) {
135
+ // Look up tickets to get their project IDs
136
+ const projectIds = new Set();
137
+ for (const ticketId of ticketIdArgs) {
138
+ // eslint-disable-next-line no-await-in-loop
139
+ const ticket = await this.storage.getTicket(ticketId);
140
+ if (ticket?.projectId) {
141
+ projectIds.add(ticket.projectId);
142
+ }
143
+ }
144
+ if (projectIds.size === 1) {
145
+ // All tickets from same project - use that project
146
+ projectId = [...projectIds][0];
147
+ }
148
+ else if (projectIds.size > 1) {
149
+ // Tickets from multiple projects - warn and require prompt
150
+ this.warn('Tickets are from multiple projects. Please specify --project.');
151
+ }
152
+ // If size === 0, tickets not found - will be handled later in validation
153
+ }
154
+ // Only call requireProject() if we couldn't infer from tickets
155
+ if (!projectId) {
156
+ projectId = await this.requireProject({
157
+ jsonMode: {
158
+ flags,
159
+ commandName: 'work spawn',
160
+ baseCommand: 'prlt work spawn',
161
+ },
162
+ });
163
+ }
140
164
  // Note: Docker check is handled by work:start command when spawning each ticket
141
165
  // This allows for the interactive devcontainer/host selection with retry loop
142
166
  // Get workspace info (for agent worktree paths)
@@ -42,11 +42,13 @@ export default class WorkWatch extends PMOCommand {
42
42
  limit: Flags.integer({
43
43
  char: 'l',
44
44
  description: 'Maximum concurrent executions',
45
+ min: 1,
45
46
  }),
46
47
  interval: Flags.integer({
47
48
  char: 'i',
48
49
  description: 'Polling interval in seconds',
49
50
  default: 5,
51
+ min: 1,
50
52
  }),
51
53
  once: Flags.boolean({
52
54
  description: 'Check once and exit (no continuous watching)',
@@ -1,4 +1,11 @@
1
1
  import { Agent, Repository, MountMode as DBMountMode } from '../database/index.js';
2
+ /**
3
+ * Format a list of agents for display in error messages.
4
+ * Truncates long lists to avoid overwhelming output.
5
+ */
6
+ export declare function formatAgentList(agents: {
7
+ name: string;
8
+ }[], maxShow?: number): string;
2
9
  export interface AgentStatus {
3
10
  name: string;
4
11
  exists: boolean;
@@ -9,6 +9,17 @@ import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAg
9
9
  import { isValidAgentName, getSuggestedAgentNames, generateEphemeralAgentName, getThemePersistentDir, getThemeEphemeralDir, extractBaseName, getAgentBaseName, } from '../themes.js';
10
10
  import { createDevcontainerConfig } from '../execution/devcontainer.js';
11
11
  import { getPMOContext } from '../pmo/index.js';
12
+ /**
13
+ * Format a list of agents for display in error messages.
14
+ * Truncates long lists to avoid overwhelming output.
15
+ */
16
+ export function formatAgentList(agents, maxShow = 10) {
17
+ const names = agents.map(a => a.name);
18
+ if (names.length <= maxShow) {
19
+ return names.join(', ');
20
+ }
21
+ return `${names.slice(0, maxShow).join(', ')} ...and ${names.length - maxShow} more. Run 'prlt agent list' to see all.`;
22
+ }
12
23
  /**
13
24
  * Find workspace root and return workspace information.
14
25
  *
@@ -217,14 +217,15 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
217
217
  }
218
218
  }
219
219
  }
220
- // Create devcontainer config for sandboxed execution (only for repos that were created)
220
+ // Create devcontainer config for sandboxed execution
221
221
  // Note: Agent metadata is stored in SQLite (agents table), not in config files
222
- if (!options?.skipDevcontainer && createdRepos.length > 0) {
222
+ // Always create devcontainer config (even if no repos were created) so agent rebuild works
223
+ if (!options?.skipDevcontainer) {
223
224
  console.log(styles.muted(` Creating devcontainer config...`));
224
225
  createDevcontainerConfig({
225
226
  agentName: agent,
226
227
  agentDir,
227
- repoWorktrees: mountMode === 'worktree' ? createdRepos : undefined, // Only pass repos for worktree mode
228
+ repoWorktrees: mountMode === 'worktree' && createdRepos.length > 0 ? createdRepos : undefined,
228
229
  mountMode,
229
230
  });
230
231
  }
@@ -237,10 +238,19 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
237
238
  }
238
239
  else {
239
240
  console.log(chalk.yellow('No repositories found in HQ. Creating placeholder agent directories.'));
240
- // Create placeholder directories for now
241
+ // Create placeholder directories with devcontainer configs
241
242
  for (const agent of agents) {
242
243
  const agentDir = path.join(workspacePath, agent);
243
244
  fs.mkdirSync(agentDir, { recursive: true });
245
+ // Create devcontainer config even without repos so agent rebuild works
246
+ if (!options?.skipDevcontainer) {
247
+ console.log(styles.muted(` Creating devcontainer config...`));
248
+ createDevcontainerConfig({
249
+ agentName: agent,
250
+ agentDir,
251
+ mountMode: mountMode,
252
+ });
253
+ }
244
254
  console.log(chalk.green(`✅ Placeholder agent ${agent} created`));
245
255
  }
246
256
  }