@proletariat/cli 0.3.19 → 0.3.20

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 (46) hide show
  1. package/dist/commands/agent/staff/remove.d.ts +1 -0
  2. package/dist/commands/agent/staff/remove.js +34 -26
  3. package/dist/commands/agent/temp/cleanup.js +10 -17
  4. package/dist/commands/board/view.d.ts +15 -0
  5. package/dist/commands/board/view.js +136 -0
  6. package/dist/commands/config/index.js +6 -3
  7. package/dist/commands/execution/config.d.ts +34 -0
  8. package/dist/commands/execution/config.js +411 -0
  9. package/dist/commands/execution/index.js +6 -1
  10. package/dist/commands/execution/kill.d.ts +9 -0
  11. package/dist/commands/execution/kill.js +16 -0
  12. package/dist/commands/execution/view.d.ts +17 -0
  13. package/dist/commands/execution/view.js +288 -0
  14. package/dist/commands/phase/template/create.js +67 -20
  15. package/dist/commands/pr/index.js +6 -2
  16. package/dist/commands/pr/list.d.ts +17 -0
  17. package/dist/commands/pr/list.js +163 -0
  18. package/dist/commands/project/update.d.ts +19 -0
  19. package/dist/commands/project/update.js +163 -0
  20. package/dist/commands/roadmap/create.js +5 -0
  21. package/dist/commands/spec/delete.d.ts +18 -0
  22. package/dist/commands/spec/delete.js +111 -0
  23. package/dist/commands/spec/edit.d.ts +23 -0
  24. package/dist/commands/spec/edit.js +232 -0
  25. package/dist/commands/spec/index.js +5 -0
  26. package/dist/commands/status/create.js +38 -34
  27. package/dist/commands/template/phase/create.d.ts +1 -0
  28. package/dist/commands/template/phase/create.js +10 -1
  29. package/dist/commands/template/ticket/create.d.ts +20 -0
  30. package/dist/commands/template/ticket/create.js +87 -0
  31. package/dist/commands/template/ticket/save.d.ts +2 -0
  32. package/dist/commands/template/ticket/save.js +11 -0
  33. package/dist/commands/ticket/create.js +7 -0
  34. package/dist/commands/ticket/template/create.d.ts +9 -1
  35. package/dist/commands/ticket/template/create.js +224 -52
  36. package/dist/commands/ticket/template/save.d.ts +2 -1
  37. package/dist/commands/ticket/template/save.js +58 -7
  38. package/dist/commands/work/ready.js +8 -8
  39. package/dist/lib/agents/index.js +14 -4
  40. package/dist/lib/branch/index.js +24 -0
  41. package/dist/lib/execution/config.d.ts +2 -0
  42. package/dist/lib/execution/config.js +12 -0
  43. package/dist/lib/pmo/utils.d.ts +4 -2
  44. package/dist/lib/pmo/utils.js +4 -2
  45. package/oclif.manifest.json +3017 -2243
  46. package/package.json +2 -4
@@ -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) {
@@ -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
  }
@@ -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
  }
@@ -129,6 +129,14 @@ export function validateBranchName(name) {
129
129
  if (parts.length === 2) {
130
130
  // {type}/{description}
131
131
  const description = parts[1];
132
+ // Check if description looks like a ticket ID (user put ticket in wrong position)
133
+ if (isTicketId(description)) {
134
+ return {
135
+ valid: false,
136
+ error: `Segment "${description}" looks like a ticket ID, but ticket IDs must be the first segment. ` +
137
+ `Expected format: {ticketId}/{type}/{description}`,
138
+ };
139
+ }
132
140
  if (!isKebabCase(description)) {
133
141
  return {
134
142
  valid: false,
@@ -143,12 +151,28 @@ export function validateBranchName(name) {
143
151
  // {type}/{owner}/{description}
144
152
  const owner = parts[1];
145
153
  const description = parts[2];
154
+ // Check if owner looks like a ticket ID (user put ticket in wrong position)
155
+ if (isTicketId(owner)) {
156
+ return {
157
+ valid: false,
158
+ error: `Segment "${owner}" looks like a ticket ID, but it's in the owner position (segment 2). ` +
159
+ `Ticket IDs must be the first segment. Expected format: {ticketId}/{type}/{owner}/{description}`,
160
+ };
161
+ }
146
162
  if (!isKebabCase(owner)) {
147
163
  return {
148
164
  valid: false,
149
165
  error: `Owner name must be kebab-case: "${owner}"`,
150
166
  };
151
167
  }
168
+ // Check if description looks like a ticket ID (user put ticket in wrong position)
169
+ if (isTicketId(description)) {
170
+ return {
171
+ valid: false,
172
+ error: `Segment "${description}" looks like a ticket ID, but it's in the description position (segment 3). ` +
173
+ `Ticket IDs must be the first segment.`,
174
+ };
175
+ }
152
176
  if (!isKebabCase(description)) {
153
177
  return {
154
178
  valid: false,
@@ -14,6 +14,8 @@ declare const CONFIG_KEYS: {
14
14
  defaultMode: string;
15
15
  defaultExecutor: string;
16
16
  autoExecute: string;
17
+ outputMode: string;
18
+ sandboxed: string;
17
19
  tmuxSession: string;
18
20
  tmuxLayout: string;
19
21
  tmuxControlMode: string;
@@ -18,6 +18,8 @@ const CONFIG_KEYS = {
18
18
  defaultMode: 'execution.default_mode',
19
19
  defaultExecutor: 'execution.default_executor',
20
20
  autoExecute: 'execution.auto_execute',
21
+ outputMode: 'execution.output_mode',
22
+ sandboxed: 'execution.sandboxed',
21
23
  tmuxSession: 'execution.tmux.session',
22
24
  tmuxLayout: 'execution.tmux.layout',
23
25
  tmuxControlMode: 'execution.tmux.control_mode',
@@ -85,6 +87,16 @@ export function loadExecutionConfig(db) {
85
87
  if (autoExecute !== null) {
86
88
  config.autoExecute = autoExecute === 'true';
87
89
  }
90
+ // Load output mode
91
+ const outputMode = getSetting(db, CONFIG_KEYS.outputMode);
92
+ if (outputMode) {
93
+ config.outputMode = outputMode;
94
+ }
95
+ // Load sandboxed preference
96
+ const sandboxed = getSetting(db, CONFIG_KEYS.sandboxed);
97
+ if (sandboxed !== null) {
98
+ config.sandboxed = sandboxed === 'true';
99
+ }
88
100
  // Load tmux settings
89
101
  const tmuxSession = getSetting(db, CONFIG_KEYS.tmuxSession);
90
102
  if (tmuxSession) {
@@ -75,14 +75,16 @@ export declare function deepClone<T>(obj: T): T;
75
75
  /**
76
76
  * Default column names for work commands (Linear-style workflow)
77
77
  *
78
- * Linear-style: Backlog → Planned → In Progress → Done
78
+ * Linear-style: Backlog → Planned → In Progress → Review → Done
79
79
  * - planned: Move tickets here when scheduled/assigned
80
80
  * - in_progress: Move tickets here when work starts
81
- * - done: Move tickets here when work is complete (includes review/merged)
81
+ * - review: Move tickets here when work is ready for review
82
+ * - done: Move tickets here when work is complete (reviewed/merged)
82
83
  */
83
84
  export declare const DEFAULT_WORK_COLUMNS: {
84
85
  readonly planned: "Planned";
85
86
  readonly in_progress: "In Progress";
87
+ readonly review: "Review";
86
88
  readonly done: "Done";
87
89
  };
88
90
  export type WorkColumnType = keyof typeof DEFAULT_WORK_COLUMNS;
@@ -122,14 +122,16 @@ export function deepClone(obj) {
122
122
  /**
123
123
  * Default column names for work commands (Linear-style workflow)
124
124
  *
125
- * Linear-style: Backlog → Planned → In Progress → Done
125
+ * Linear-style: Backlog → Planned → In Progress → Review → Done
126
126
  * - planned: Move tickets here when scheduled/assigned
127
127
  * - in_progress: Move tickets here when work starts
128
- * - done: Move tickets here when work is complete (includes review/merged)
128
+ * - review: Move tickets here when work is ready for review
129
+ * - done: Move tickets here when work is complete (reviewed/merged)
129
130
  */
130
131
  export const DEFAULT_WORK_COLUMNS = {
131
132
  planned: 'Planned',
132
133
  in_progress: 'In Progress',
134
+ review: 'Review',
133
135
  done: 'Done',
134
136
  };
135
137
  /**