@proletariat/cli 0.3.36 → 0.3.41

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 (65) hide show
  1. package/README.md +37 -2
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/branch/where.js +6 -17
  4. package/dist/commands/epic/ticket.js +7 -24
  5. package/dist/commands/execution/config.js +4 -14
  6. package/dist/commands/execution/logs.js +6 -0
  7. package/dist/commands/execution/view.js +8 -0
  8. package/dist/commands/init.js +4 -8
  9. package/dist/commands/mcp-server.js +2 -1
  10. package/dist/commands/pmo/init.js +12 -40
  11. package/dist/commands/qa/index.d.ts +54 -0
  12. package/dist/commands/qa/index.js +762 -0
  13. package/dist/commands/repo/view.js +2 -8
  14. package/dist/commands/session/attach.js +4 -4
  15. package/dist/commands/session/health.js +4 -4
  16. package/dist/commands/session/list.js +1 -19
  17. package/dist/commands/session/peek.js +6 -6
  18. package/dist/commands/session/poke.js +2 -2
  19. package/dist/commands/ticket/epic.js +17 -43
  20. package/dist/commands/work/spawn-all.js +1 -1
  21. package/dist/commands/work/spawn.js +15 -4
  22. package/dist/commands/work/start.js +17 -9
  23. package/dist/commands/work/watch.js +1 -1
  24. package/dist/commands/workspace/prune.js +3 -3
  25. package/dist/hooks/init.js +21 -10
  26. package/dist/lib/agents/commands.d.ts +5 -0
  27. package/dist/lib/agents/commands.js +143 -97
  28. package/dist/lib/database/drizzle-schema.d.ts +465 -0
  29. package/dist/lib/database/drizzle-schema.js +53 -0
  30. package/dist/lib/database/index.d.ts +47 -1
  31. package/dist/lib/database/index.js +138 -20
  32. package/dist/lib/execution/runners.d.ts +34 -0
  33. package/dist/lib/execution/runners.js +134 -7
  34. package/dist/lib/execution/session-utils.d.ts +5 -0
  35. package/dist/lib/execution/session-utils.js +45 -3
  36. package/dist/lib/execution/spawner.js +15 -2
  37. package/dist/lib/execution/storage.d.ts +1 -1
  38. package/dist/lib/execution/storage.js +17 -2
  39. package/dist/lib/execution/types.d.ts +1 -0
  40. package/dist/lib/mcp/tools/index.d.ts +1 -0
  41. package/dist/lib/mcp/tools/index.js +1 -0
  42. package/dist/lib/mcp/tools/tmux.d.ts +16 -0
  43. package/dist/lib/mcp/tools/tmux.js +182 -0
  44. package/dist/lib/mcp/tools/work.js +52 -0
  45. package/dist/lib/pmo/schema.d.ts +1 -1
  46. package/dist/lib/pmo/schema.js +1 -0
  47. package/dist/lib/pmo/storage/base.js +207 -0
  48. package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
  49. package/dist/lib/pmo/storage/dependencies.js +11 -3
  50. package/dist/lib/pmo/storage/epics.js +1 -1
  51. package/dist/lib/pmo/storage/helpers.d.ts +4 -4
  52. package/dist/lib/pmo/storage/helpers.js +36 -26
  53. package/dist/lib/pmo/storage/projects.d.ts +2 -0
  54. package/dist/lib/pmo/storage/projects.js +207 -119
  55. package/dist/lib/pmo/storage/specs.d.ts +2 -0
  56. package/dist/lib/pmo/storage/specs.js +274 -188
  57. package/dist/lib/pmo/storage/tickets.d.ts +2 -0
  58. package/dist/lib/pmo/storage/tickets.js +350 -290
  59. package/dist/lib/pmo/storage/views.d.ts +2 -0
  60. package/dist/lib/pmo/storage/views.js +183 -130
  61. package/dist/lib/prompt-json.d.ts +5 -0
  62. package/dist/lib/prompt-json.js +9 -0
  63. package/oclif.manifest.json +3293 -3190
  64. package/package.json +11 -6
  65. package/LICENSE +0 -190
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { colors, format } from '../../lib/colors.js';
4
4
  import { findHQRoot, getWorkspaceRepoInfo, } from '../../lib/repos/index.js';
5
- import { openWorkspaceDatabase } from '../../lib/database/index.js';
5
+ import { getWorktreesForRepo } from '../../lib/database/index.js';
6
6
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
7
7
  import { visualPadEnd } from '../../lib/string-utils.js';
8
8
  export default class View extends PMOCommand {
@@ -102,13 +102,7 @@ export default class View extends PMOCommand {
102
102
  }
103
103
  }
104
104
  // Get agent worktree info
105
- const db = openWorkspaceDatabase(hqPath);
106
- const worktrees = db.prepare(`
107
- SELECT agent_name, is_clean, commits_ahead, branch
108
- FROM agent_worktrees
109
- WHERE repo_name = ?
110
- `).all(repoName);
111
- db.close();
105
+ const worktrees = getWorktreesForRepo(hqPath, repoName);
112
106
  if (worktrees.length > 0) {
113
107
  this.log(format.subtitle('\n👥 Agent Worktrees:'));
114
108
  for (const wt of worktrees) {
@@ -7,7 +7,7 @@ import Database from 'better-sqlite3';
7
7
  import { styles } from '../../lib/styles.js';
8
8
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
9
9
  import { ExecutionStorage } from '../../lib/execution/index.js';
10
- import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
10
+ import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
11
11
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
12
12
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
13
13
  export default class SessionAttach extends PMOCommand {
@@ -154,7 +154,7 @@ export default class SessionAttach extends PMOCommand {
154
154
  // If sessionId is NULL, try to find session by naming convention
155
155
  if (!exec.sessionId) {
156
156
  if (isContainer && exec.containerId) {
157
- const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
157
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
158
158
  const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
159
159
  if (match) {
160
160
  actualSessionId = match;
@@ -177,8 +177,8 @@ export default class SessionAttach extends PMOCommand {
177
177
  else {
178
178
  // sessionId is set, verify it exists
179
179
  if (isContainer && exec.containerId) {
180
- const containerSessions = containerTmuxSessions.get(exec.containerId);
181
- exists = containerSessions?.includes(exec.sessionId) ?? false;
180
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
181
+ exists = containerSessions.includes(exec.sessionId);
182
182
  containerId = exec.containerId;
183
183
  }
184
184
  else {
@@ -5,7 +5,7 @@ import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
7
  import { ExecutionStorage } from '../../lib/execution/index.js';
8
- import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
8
+ import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
9
9
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
10
10
  import { visualPadEnd } from '../../lib/string-utils.js';
11
11
  import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
@@ -168,7 +168,7 @@ export default class SessionHealth extends PMOCommand {
168
168
  // Try to find session if sessionId is NULL
169
169
  if (!exec.sessionId) {
170
170
  if (isContainer && exec.containerId) {
171
- const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
171
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
172
172
  const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
173
173
  if (match) {
174
174
  actualSessionId = match;
@@ -188,8 +188,8 @@ export default class SessionHealth extends PMOCommand {
188
188
  }
189
189
  else {
190
190
  if (isContainer && exec.containerId) {
191
- const containerSessions = containerTmuxSessions.get(exec.containerId);
192
- exists = containerSessions?.includes(exec.sessionId) ?? false;
191
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
192
+ exists = containerSessions.includes(exec.sessionId);
193
193
  containerId = exec.containerId;
194
194
  }
195
195
  else {
@@ -4,28 +4,10 @@ import Database from 'better-sqlite3';
4
4
  import { styles } from '../../lib/styles.js';
5
5
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
6
6
  import { ExecutionStorage } from '../../lib/execution/index.js';
7
- import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
7
+ import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
8
8
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
9
9
  import { shouldOutputJson } from '../../lib/prompt-json.js';
10
10
  import { visualPadEnd } from '../../lib/string-utils.js';
11
- /**
12
- * Find container sessions using prefix matching.
13
- * Handles cases where the stored containerId format differs from docker ps output
14
- * (e.g., full 64-char ID vs 12-char short ID).
15
- */
16
- function findContainerSessionsByPrefix(containerTmuxSessions, containerId) {
17
- // Try exact match first
18
- const exact = containerTmuxSessions.get(containerId);
19
- if (exact)
20
- return exact;
21
- // Fall back to prefix matching (handles short vs full ID mismatches)
22
- for (const [key, sessions] of containerTmuxSessions) {
23
- if (key.startsWith(containerId) || containerId.startsWith(key)) {
24
- return sessions;
25
- }
26
- }
27
- return [];
28
- }
29
11
  export default class SessionList extends PMOCommand {
30
12
  static description = 'List active tmux sessions (host and container)';
31
13
  static examples = [
@@ -4,7 +4,7 @@ import Database from 'better-sqlite3';
4
4
  import { styles } from '../../lib/styles.js';
5
5
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
6
6
  import { ExecutionStorage } from '../../lib/execution/index.js';
7
- import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
7
+ import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, captureTmuxPane, } from '../../lib/execution/session-utils.js';
8
8
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
9
9
  import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
10
10
  // =============================================================================
@@ -41,7 +41,7 @@ export default class SessionPeek extends PMOCommand {
41
41
  const { args, flags } = await this.parse(SessionPeek);
42
42
  const jsonMode = shouldOutputJson(flags);
43
43
  // Discover all verified sessions
44
- const sessions = this.getVerifiedSessions(jsonMode, flags);
44
+ const sessions = this.getVerifiedSessions();
45
45
  if (sessions.length === 0) {
46
46
  if (jsonMode) {
47
47
  outputErrorAsJson('NO_SESSIONS', 'No active sessions found.', createMetadata('session peek', flags));
@@ -201,7 +201,7 @@ export default class SessionPeek extends PMOCommand {
201
201
  * Get verified sessions from DB that have actual tmux processes.
202
202
  * Same discovery pattern as attach.ts and list.ts.
203
203
  */
204
- getVerifiedSessions(jsonMode, flags) {
204
+ getVerifiedSessions() {
205
205
  const sessions = [];
206
206
  let executionStorage = null;
207
207
  let db = null;
@@ -232,7 +232,7 @@ export default class SessionPeek extends PMOCommand {
232
232
  let actualSessionId = exec.sessionId;
233
233
  if (!exec.sessionId) {
234
234
  if (isContainer && exec.containerId) {
235
- const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
235
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
236
236
  const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
237
237
  if (match) {
238
238
  actualSessionId = match;
@@ -252,8 +252,8 @@ export default class SessionPeek extends PMOCommand {
252
252
  }
253
253
  else {
254
254
  if (isContainer && exec.containerId) {
255
- const containerSessions = containerTmuxSessions.get(exec.containerId);
256
- exists = containerSessions?.includes(exec.sessionId) ?? false;
255
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
256
+ exists = containerSessions.includes(exec.sessionId);
257
257
  containerId = exec.containerId;
258
258
  }
259
259
  else {
@@ -5,7 +5,7 @@ import Database from 'better-sqlite3';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
7
  import { ExecutionStorage } from '../../lib/execution/index.js';
8
- import { getHostTmuxSessionNames, getContainerTmuxSessionMap, findSessionForExecution, } from '../../lib/execution/session-utils.js';
8
+ import { getHostTmuxSessionNames, getContainerTmuxSessionMap, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
9
9
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
10
10
  import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
11
11
  // =============================================================================
@@ -183,7 +183,7 @@ export default class SessionPoke extends PMOCommand {
183
183
  if (!exec.sessionId) {
184
184
  if (isContainer && exec.containerId) {
185
185
  const containerTmuxSessions = getContainerTmuxSessionMap();
186
- const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
186
+ const containerSessions = findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
187
187
  const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
188
188
  if (match) {
189
189
  actualSessionId = match;
@@ -68,11 +68,10 @@ export default class TicketEpic extends PMOCommand {
68
68
  const projectId = await this.requireProject();
69
69
  // Get all epics
70
70
  const epics = await this.storage.listEpics(projectId);
71
- // Get epic_id for each ticket via direct DB query
72
- const db = this.storage.db;
71
+ // Helper to get ticket's epic ID via storage layer
73
72
  const getTicketEpicId = (ticketId) => {
74
- const row = db.prepare(`SELECT epic_id FROM pmo_tickets WHERE id = ?`).get(ticketId);
75
- return row?.epic_id || null;
73
+ const ticket = allTickets.find((t) => t.id === ticketId);
74
+ return ticket?.epicId ?? null;
76
75
  };
77
76
  let ticketId = args.id;
78
77
  let epicId = args['epic-id'];
@@ -119,11 +118,7 @@ export default class TicketEpic extends PMOCommand {
119
118
  }
120
119
  const currentEpic = epics.find(e => e.id === currentEpicId);
121
120
  // Update the ticket
122
- db.prepare(`
123
- UPDATE pmo_tickets
124
- SET epic_id = NULL, updated_at = ?
125
- WHERE id = ?
126
- `).run(Date.now(), ticketId);
121
+ await this.storage.unlinkTicketFromEpic(ticketId);
127
122
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
128
123
  this.log(styles.success(`\n✅ Unlinked ${styles.emphasis(ticketId)} from ${currentEpic?.title || currentEpicId}`));
129
124
  this.log(styles.muted(` Title: ${ticket.title}`));
@@ -156,11 +151,7 @@ export default class TicketEpic extends PMOCommand {
156
151
  }
157
152
  if (selected === '__none__') {
158
153
  // Unlink
159
- db.prepare(`
160
- UPDATE pmo_tickets
161
- SET epic_id = NULL, updated_at = ?
162
- WHERE id = ?
163
- `).run(Date.now(), ticketId);
154
+ await this.storage.unlinkTicketFromEpic(ticketId);
164
155
  const currentEpic = epics.find(e => e.id === currentEpicId);
165
156
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
166
157
  this.log(styles.success(`\n✅ Unlinked ${styles.emphasis(ticketId)} from ${currentEpic?.title || currentEpicId}`));
@@ -180,11 +171,7 @@ export default class TicketEpic extends PMOCommand {
180
171
  return;
181
172
  }
182
173
  // Update the ticket
183
- db.prepare(`
184
- UPDATE pmo_tickets
185
- SET epic_id = ?, updated_at = ?
186
- WHERE id = ?
187
- `).run(epicId, Date.now(), ticketId);
174
+ await this.storage.linkTicketToEpic(ticketId, epicId);
188
175
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
189
176
  this.log(styles.success(`\n✅ Linked ${styles.emphasis(ticketId)} to ${styles.emphasis(epicId)}`));
190
177
  this.log(styles.muted(` Title: ${ticket.title}`));
@@ -202,35 +189,21 @@ export default class TicketEpic extends PMOCommand {
202
189
  this.log(styles.warning('No tickets found.'));
203
190
  return;
204
191
  }
205
- // Get epics from database
206
- const db = this.storage.db;
207
- const epics = db.prepare(`
208
- SELECT id, title, status FROM pmo_epics
209
- WHERE project_id = ?
210
- ORDER BY status, title
211
- `).all(projectId);
192
+ // Get epics via storage layer
193
+ const epics = await this.storage.listEpics(projectId);
212
194
  // Filter tickets if --from-epic specified
213
195
  let filteredTickets = allTickets;
214
196
  if (flags['from-epic']) {
215
- // Get tickets with matching epic_id via metadata or direct query
216
- const epicTickets = db.prepare(`
217
- SELECT id FROM pmo_tickets
218
- WHERE project_id = ? AND epic_id = ?
219
- `).all(projectId, flags['from-epic']);
220
- const epicTicketIds = new Set(epicTickets.map(t => t.id));
221
- filteredTickets = allTickets.filter(t => epicTicketIds.has(t.id));
197
+ filteredTickets = allTickets.filter(t => t.epicId === flags['from-epic']);
222
198
  }
223
199
  if (filteredTickets.length === 0) {
224
200
  this.log(styles.warning('No tickets found matching filter.'));
225
201
  return;
226
202
  }
227
- // Get current epic for each ticket
203
+ // Get current epic for each ticket (already available on Ticket objects)
228
204
  const ticketEpics = new Map();
229
205
  for (const ticket of filteredTickets) {
230
- const row = db.prepare(`
231
- SELECT epic_id FROM pmo_tickets WHERE id = ?
232
- `).get(ticket.id);
233
- ticketEpics.set(ticket.id, row?.epic_id || null);
206
+ ticketEpics.set(ticket.id, ticket.epicId ?? null);
234
207
  }
235
208
  // Select tickets to link
236
209
  const { selectedTickets } = await this.prompt([{
@@ -302,11 +275,12 @@ export default class TicketEpic extends PMOCommand {
302
275
  let failCount = 0;
303
276
  for (const ticketId of selectedTickets) {
304
277
  try {
305
- db.prepare(`
306
- UPDATE pmo_tickets
307
- SET epic_id = ?, updated_at = ?
308
- WHERE id = ?
309
- `).run(targetEpic, Date.now(), ticketId);
278
+ if (targetEpic) {
279
+ await this.storage.linkTicketToEpic(ticketId, targetEpic);
280
+ }
281
+ else {
282
+ await this.storage.unlinkTicketFromEpic(ticketId);
283
+ }
310
284
  const action = targetEpic ? `Linked to ${targetEpic}` : 'Removed epic link';
311
285
  this.log(styles.success(`${ticketId}: ${action}`));
312
286
  successCount++;
@@ -24,7 +24,7 @@ export default class WorkSpawnAll extends PMOCommand {
24
24
  default: false,
25
25
  }),
26
26
  'create-pr': Flags.boolean({
27
- description: 'Create PR when work is ready',
27
+ description: 'Create PR when work is ready (canonical flag for PR behavior)',
28
28
  default: false,
29
29
  }),
30
30
  executor: Flags.string({
@@ -25,6 +25,7 @@ export default class WorkSpawn extends PMOCommand {
25
25
  '<%= config.bin %> <%= command.id %> --count 5 --category ship --action implement # Filtered by category',
26
26
  '<%= config.bin %> <%= command.id %> --count 5 --priority P0 --action implement # Filtered by priority',
27
27
  '<%= config.bin %> <%= command.id %> --count 10 --diet --category ship,grow --action groom # Combined',
28
+ '<%= config.bin %> <%= command.id %> TKT-001 TKT-002 --create-pr # Create PR when work is ready',
28
29
  ];
29
30
  static flags = {
30
31
  ...pmoBaseFlags,
@@ -93,11 +94,11 @@ export default class WorkSpawn extends PMOCommand {
93
94
  default: false,
94
95
  }),
95
96
  'create-pr': Flags.boolean({
96
- description: 'Create PR when work is ready (batch mode only)',
97
+ description: 'Create PR when work is ready (canonical flag for PR behavior, batch mode only)',
97
98
  default: false,
98
99
  }),
99
100
  'no-pr': Flags.boolean({
100
- description: 'Do not create PR when work is ready (batch mode only)',
101
+ description: '[deprecated: use --create-pr instead] Skip PR creation (batch mode only)',
101
102
  default: false,
102
103
  }),
103
104
  action: Flags.string({
@@ -153,6 +154,10 @@ export default class WorkSpawn extends PMOCommand {
153
154
  }
154
155
  this.error(message);
155
156
  };
157
+ // Deprecation guidance for --no-pr
158
+ if (flags['no-pr']) {
159
+ this.warn('--no-pr is deprecated. Omit --create-pr instead (PR creation is off by default). --no-pr will continue to work.');
160
+ }
156
161
  // Parse ticket IDs from args (everything after flags)
157
162
  const ticketIdArgs = argv;
158
163
  // Try to infer project from ticket IDs if provided
@@ -294,6 +299,7 @@ export default class WorkSpawn extends PMOCommand {
294
299
  else if (allFlagsProvided && !flags.yes) {
295
300
  // All flags provided but no --yes: return confirmation_needed with plan
296
301
  const metadata = createMetadata('work spawn', flags);
302
+ metadata.resolvedPRMode = flags['create-pr'] ? 'create-pr' : 'no-pr';
297
303
  // Build the confirm command with --yes
298
304
  const ticketIds = ticketsToSpawn.map(t => t.id).join(' ');
299
305
  let confirmCmd = `prlt work spawn ${ticketIds}`;
@@ -594,6 +600,7 @@ export default class WorkSpawn extends PMOCommand {
594
600
  // In JSON mode without --yes, return confirmation_needed
595
601
  if (jsonMode && !flags.yes) {
596
602
  const metadata = createMetadata('work spawn', flags);
603
+ metadata.resolvedPRMode = flags['create-pr'] ? 'create-pr' : 'no-pr';
597
604
  const ticketIds = ticketsToSpawn.map(t => t.id).join(' ');
598
605
  let confirmCmd = `prlt work spawn ${ticketIds}`;
599
606
  if (flags.action)
@@ -1394,12 +1401,16 @@ export default class WorkSpawn extends PMOCommand {
1394
1401
  db.close();
1395
1402
  // Output results
1396
1403
  if (jsonMode) {
1397
- // Output JSON execution results
1398
- outputExecutionResultAsJson(executionResults, successCount, failCount, createMetadata('work spawn', flags));
1404
+ // Output JSON execution results with resolved PR mode
1405
+ const spawnMetadata = createMetadata('work spawn', flags);
1406
+ spawnMetadata.resolvedPRMode = flags['create-pr'] ? 'create-pr' : 'no-pr';
1407
+ outputExecutionResultAsJson(executionResults, successCount, failCount, spawnMetadata);
1399
1408
  }
1400
1409
  else {
1410
+ const resolvedPRMode = flags['create-pr'] ? 'create-pr' : 'no-pr';
1401
1411
  this.log('');
1402
1412
  this.log(styles.success(`✓ Spawn results: ${successCount} started, ${failCount} failed`));
1413
+ this.log(styles.muted(` PR mode: ${resolvedPRMode}`));
1403
1414
  }
1404
1415
  }
1405
1416
  catch (error) {
@@ -71,6 +71,7 @@ export default class WorkStart extends PMOCommand {
71
71
  static description = 'Start work on a ticket (launches an agent to implement it)';
72
72
  static examples = [
73
73
  '<%= config.bin %> <%= command.id %> TKT-001',
74
+ '<%= config.bin %> <%= command.id %> TKT-001 --create-pr # Create PR when work is ready',
74
75
  '<%= config.bin %> <%= command.id %> TKT-001 --mode foreground',
75
76
  '<%= config.bin %> <%= command.id %> TKT-001 --mode tmux',
76
77
  '<%= config.bin %> <%= command.id %> TKT-001 --mode terminal',
@@ -137,11 +138,11 @@ export default class WorkStart extends PMOCommand {
137
138
  default: false,
138
139
  }),
139
140
  'create-pr': Flags.boolean({
140
- description: 'Create PR when work is ready',
141
+ description: 'Create PR when work is ready (canonical flag for PR behavior)',
141
142
  default: false,
142
143
  }),
143
144
  'no-pr': Flags.boolean({
144
- description: 'Do not create PR when work is ready',
145
+ description: '[deprecated: use --create-pr instead] Skip PR creation when work is ready',
145
146
  default: false,
146
147
  }),
147
148
  output: Flags.string({
@@ -193,6 +194,10 @@ export default class WorkStart extends PMOCommand {
193
194
  if (flags['create-pr'] && flags['no-pr']) {
194
195
  this.error('--create-pr and --no-pr are mutually exclusive');
195
196
  }
197
+ // Deprecation guidance for --no-pr
198
+ if (flags['no-pr']) {
199
+ this.warn('--no-pr is deprecated. Omit --create-pr instead (PR creation is off by default). --no-pr will continue to work.');
200
+ }
196
201
  // Handle --skip-permissions flag (alias for --permission-mode danger)
197
202
  // Check for conflicting flags first
198
203
  if (flags['skip-permissions'] && flags['permission-mode']) {
@@ -272,6 +277,7 @@ export default class WorkStart extends PMOCommand {
272
277
  if (allFlagsProvided && !flags.yes) {
273
278
  // All flags provided but no --yes: return confirmation_needed with plan
274
279
  const metadata = createMetadata('work start', flags);
280
+ metadata.resolvedPRMode = flags['create-pr'] ? 'create-pr' : 'no-pr';
275
281
  // Build the confirm command with --yes
276
282
  let confirmCmd = `prlt work start ${ticketId}`;
277
283
  if (flags.action)
@@ -1281,9 +1287,7 @@ export default class WorkStart extends PMOCommand {
1281
1287
  this.log(styles.warning(` Permissions: ⚠️ danger (--dangerously-skip-permissions)`));
1282
1288
  }
1283
1289
  this.log(styles.muted(` Output: ${outputMode === 'interactive' ? 'streaming (watch Claude work)' : 'print (final result only)'}`));
1284
- if (ghAvailable) {
1285
- this.log(styles.muted(` Create PR: ${createPR ? 'yes (when work is ready)' : 'no'}`));
1286
- }
1290
+ this.log(styles.muted(` PR mode: ${createPR ? 'create-pr' : 'no-pr'}${ghAvailable ? '' : ' (gh CLI not available)'}`));
1287
1291
  this.log(styles.muted(` Worktree: ${worktreePath}`));
1288
1292
  this.log(styles.muted(` Branch: ${branch}`));
1289
1293
  this.log('');
@@ -1585,7 +1589,9 @@ export default class WorkStart extends PMOCommand {
1585
1589
  });
1586
1590
  // Output results
1587
1591
  if (jsonMode) {
1588
- // Output JSON execution result
1592
+ // Output JSON execution result with resolved PR mode
1593
+ const metadata = createMetadata('work start', flags);
1594
+ metadata.resolvedPRMode = createPR ? 'create-pr' : 'no-pr';
1589
1595
  outputExecutionResultAsJson([{
1590
1596
  workId: execution.id,
1591
1597
  ticketId: ticket.id,
@@ -1593,7 +1599,7 @@ export default class WorkStart extends PMOCommand {
1593
1599
  sessionId: result.sessionId,
1594
1600
  containerId: result.containerId,
1595
1601
  status: 'running',
1596
- }], 1, 0, createMetadata('work start', flags));
1602
+ }], 1, 0, metadata);
1597
1603
  }
1598
1604
  else {
1599
1605
  this.log('');
@@ -1608,13 +1614,15 @@ export default class WorkStart extends PMOCommand {
1608
1614
  else {
1609
1615
  executionStorage.updateStatus(execution.id, 'failed');
1610
1616
  if (jsonMode) {
1611
- // Output JSON failure result
1617
+ // Output JSON failure result with resolved PR mode
1618
+ const failMetadata = createMetadata('work start', flags);
1619
+ failMetadata.resolvedPRMode = createPR ? 'create-pr' : 'no-pr';
1612
1620
  outputExecutionResultAsJson([{
1613
1621
  workId: execution.id,
1614
1622
  ticketId: ticket.id,
1615
1623
  agent: assignedAgent,
1616
1624
  status: 'failed',
1617
- }], 0, 1, createMetadata('work start', flags));
1625
+ }], 0, 1, failMetadata);
1618
1626
  }
1619
1627
  else {
1620
1628
  this.error(`Failed to start work: ${result.error}`);
@@ -60,7 +60,7 @@ export default class WorkWatch extends PMOCommand {
60
60
  default: false,
61
61
  }),
62
62
  'create-pr': Flags.boolean({
63
- description: 'Create PR when work is ready',
63
+ description: 'Create PR when work is ready (canonical flag for PR behavior)',
64
64
  default: false,
65
65
  }),
66
66
  };
@@ -29,10 +29,10 @@ export default class WorkspacePrune extends PromptCommand {
29
29
  };
30
30
  async run() {
31
31
  const { flags } = await this.parse(WorkspacePrune);
32
- // In non-TTY mode without --json (CI, scripts, piped), default to dry-run unless --force is set.
33
- // In --json mode, we use confirmation_needed output instead of auto-dry-run so agents can review and confirm.
32
+ // In non-TTY mode (CI, scripts, piped), default to dry-run unless --force is set.
33
+ // This applies regardless of output format (text or JSON).
34
34
  const nonTTY = isNonTTY();
35
- const effectiveDryRun = flags['dry-run'] || (!shouldOutputJson(flags) && nonTTY && !flags.force);
35
+ const effectiveDryRun = flags['dry-run'] || (nonTTY && !flags.force);
36
36
  // Find stale entries
37
37
  const staleWorkspaces = this.findStaleWorkspaces();
38
38
  const staleAgents = this.findStaleAgents();
@@ -8,13 +8,24 @@ import { findHQRoot } from '../lib/workspace.js';
8
8
  * - No workspaces are registered in machine config (~/.proletariat/config.json)
9
9
  * - AND they're not currently inside a valid HQ directory
10
10
  */
11
- const hook = async function ({ id, config }) {
12
- // Skip for init command to avoid infinite loop
13
- if (id === 'init') {
11
+ const hook = async function ({ id, argv, config }) {
12
+ // Skip for commands that work without an HQ
13
+ const hqOptionalCommands = ['init', 'commit', 'claude', 'pmo:init'];
14
+ if (id && hqOptionalCommands.some(cmd => id === cmd || id.startsWith(cmd + ':'))) {
14
15
  return;
15
16
  }
16
- // Skip when --help flag is present - help should always be available
17
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
17
+ // Skip when running under oclif tooling (manifest, readme generation)
18
+ // These run commands to scan metadata and should not trigger the init flow
19
+ if (process.env.OCLIF_COMPILATION || process.argv[1]?.includes('oclif')) {
20
+ return;
21
+ }
22
+ // Skip when --help or --version flags are present - these should always be available
23
+ // Check both process.argv (production CLI) and the oclif-provided argv
24
+ // (programmatic invocation via @oclif/test runCommand)
25
+ if (process.argv.includes('--help') || process.argv.includes('-h') ||
26
+ argv?.includes('--help') || argv?.includes('-h') ||
27
+ process.argv.includes('--version') || process.argv.includes('-v') ||
28
+ argv?.includes('--version') || argv?.includes('-v')) {
18
29
  return;
19
30
  }
20
31
  // Skip for help-related commands/flags
@@ -22,10 +33,10 @@ const hook = async function ({ id, config }) {
22
33
  if (!id || id === 'help') {
23
34
  // Check if this is first-time user running bare `prlt`
24
35
  if (!id && isFirstTimeUser()) {
25
- // Run init command
36
+ // Run init command - in TTY it prompts interactively,
37
+ // in non-TTY it outputs a JSON prompt for the HQ name
26
38
  const { run } = await import('@oclif/core');
27
39
  await run(['init'], config);
28
- // Exit after init completes to prevent showing help
29
40
  process.exit(0);
30
41
  }
31
42
  return;
@@ -33,11 +44,11 @@ const hook = async function ({ id, config }) {
33
44
  // For all other commands, check if first-time user
34
45
  if (isFirstTimeUser()) {
35
46
  const chalk = await import('chalk');
36
- console.log(chalk.default.yellow('\n⚠️ No workspace found. Let\'s set one up first.\n'));
37
- // Run init command
47
+ console.log(chalk.default.yellow('\n⚠️ No headquarters found. Let\'s set one up first.\n'));
48
+ // Run init command - in TTY it prompts interactively,
49
+ // in non-TTY it outputs a JSON prompt for the HQ name
38
50
  const { run } = await import('@oclif/core');
39
51
  await run(['init'], config);
40
- // Exit after init - user should re-run their original command
41
52
  console.log(chalk.default.blue(`\n✅ Setup complete! You can now run: prlt ${id}\n`));
42
53
  process.exit(0);
43
54
  }
@@ -111,6 +111,11 @@ export interface EphemeralAgentResult {
111
111
  /**
112
112
  * Create an ephemeral agent on-demand for a spawn operation.
113
113
  * Creates worktree in agents/temp/{name}/
114
+ *
115
+ * Concurrency-safe: if the generated name collides at the DB level
116
+ * (e.g. a parallel process inserted the same name first), we clean up
117
+ * the on-disk artifacts, regenerate a name, and retry up to
118
+ * EPHEMERAL_CREATE_MAX_RETRIES times.
114
119
  */
115
120
  export declare function createEphemeralAgent(workspaceInfo: WorkspaceInfo, options?: EphemeralAgentOptions): Promise<EphemeralAgentResult>;
116
121
  /**