@proletariat/cli 0.3.9 → 0.3.11

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 (152) hide show
  1. package/README.md +25 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/action/index.js +1 -1
  4. package/dist/commands/action/run.js +8 -12
  5. package/dist/commands/agent/auth.d.ts +30 -0
  6. package/dist/commands/agent/auth.js +172 -0
  7. package/dist/commands/agent/discover.d.ts +9 -0
  8. package/dist/commands/agent/discover.js +67 -0
  9. package/dist/commands/agent/index.js +47 -12
  10. package/dist/commands/agent/list.d.ts +4 -1
  11. package/dist/commands/agent/list.js +78 -16
  12. package/dist/commands/agent/login.js +35 -31
  13. package/dist/commands/agent/restart.js +2 -0
  14. package/dist/commands/agent/shell.js +78 -19
  15. package/dist/commands/agent/staff/add.js +1 -12
  16. package/dist/commands/agent/staff/remove.js +9 -7
  17. package/dist/commands/agent/status.js +17 -4
  18. package/dist/commands/agent/temp/cleanup.js +7 -3
  19. package/dist/commands/agent/themes/index.js +4 -5
  20. package/dist/commands/agent/themes/list.js +5 -5
  21. package/dist/commands/agent/visit.js +17 -4
  22. package/dist/commands/branch/create.d.ts +4 -0
  23. package/dist/commands/branch/create.js +16 -8
  24. package/dist/commands/branch/index.js +1 -1
  25. package/dist/commands/branch/where.js +1 -0
  26. package/dist/commands/claude.d.ts +38 -0
  27. package/dist/commands/claude.js +899 -0
  28. package/dist/commands/commit.js +1 -1
  29. package/dist/commands/config/index.d.ts +12 -0
  30. package/dist/commands/config/index.js +271 -0
  31. package/dist/commands/docker/clean.js +2 -2
  32. package/dist/commands/docker/index.js +2 -2
  33. package/dist/commands/docker/list.js +3 -8
  34. package/dist/commands/docker/logs.js +2 -2
  35. package/dist/commands/docker/prune.js +1 -1
  36. package/dist/commands/docker/restart.js +2 -2
  37. package/dist/commands/docker/shell.js +2 -2
  38. package/dist/commands/docker/start.js +2 -2
  39. package/dist/commands/docker/status.js +1 -1
  40. package/dist/commands/docker/stop.js +2 -2
  41. package/dist/commands/docker/sync.js +2 -2
  42. package/dist/commands/epic/index.js +1 -1
  43. package/dist/commands/epic/link/index.js +25 -14
  44. package/dist/commands/epic/link/remove.js +2 -0
  45. package/dist/commands/epic/list.js +5 -5
  46. package/dist/commands/epic/progress.js +10 -4
  47. package/dist/commands/epic/spec.js +2 -0
  48. package/dist/commands/epic/ticket.js +3 -0
  49. package/dist/commands/execution/stop.js +1 -0
  50. package/dist/commands/init.js +4 -4
  51. package/dist/commands/project/index.js +1 -1
  52. package/dist/commands/project/spec.js +7 -0
  53. package/dist/commands/repo/add.js +1 -0
  54. package/dist/commands/repo/remove.js +1 -0
  55. package/dist/commands/roadmap/add-project.d.ts +18 -0
  56. package/dist/commands/roadmap/add-project.js +135 -0
  57. package/dist/commands/roadmap/create.d.ts +22 -0
  58. package/dist/commands/roadmap/create.js +156 -0
  59. package/dist/commands/roadmap/delete.d.ts +17 -0
  60. package/dist/commands/roadmap/delete.js +104 -0
  61. package/dist/commands/roadmap/generate.d.ts +22 -0
  62. package/dist/commands/roadmap/generate.js +201 -0
  63. package/dist/commands/roadmap/index.d.ts +13 -0
  64. package/dist/commands/roadmap/index.js +61 -0
  65. package/dist/commands/roadmap/list.d.ts +12 -0
  66. package/dist/commands/roadmap/list.js +42 -0
  67. package/dist/commands/roadmap/remove-project.d.ts +18 -0
  68. package/dist/commands/roadmap/remove-project.js +147 -0
  69. package/dist/commands/roadmap/reorder.d.ts +17 -0
  70. package/dist/commands/roadmap/reorder.js +157 -0
  71. package/dist/commands/roadmap/update.d.ts +19 -0
  72. package/dist/commands/roadmap/update.js +136 -0
  73. package/dist/commands/roadmap/view.d.ts +16 -0
  74. package/dist/commands/roadmap/view.js +103 -0
  75. package/dist/commands/spec/index.js +1 -1
  76. package/dist/commands/spec/link/index.js +24 -13
  77. package/dist/commands/spec/link/remove.js +2 -0
  78. package/dist/commands/status/index.js +1 -1
  79. package/dist/commands/status/list.js +0 -8
  80. package/dist/commands/template/delete.js +2 -0
  81. package/dist/commands/terminal/title.d.ts +12 -0
  82. package/dist/commands/terminal/title.js +48 -0
  83. package/dist/commands/ticket/complete.js +2 -0
  84. package/dist/commands/ticket/create.js +4 -2
  85. package/dist/commands/ticket/delete.js +2 -0
  86. package/dist/commands/ticket/edit.js +8 -2
  87. package/dist/commands/ticket/link/index.js +17 -3
  88. package/dist/commands/ticket/link/remove.js +2 -0
  89. package/dist/commands/ticket/list.js +1 -2
  90. package/dist/commands/ticket/move.js +2 -0
  91. package/dist/commands/ticket/project.js +3 -1
  92. package/dist/commands/ticket/reassign.js +2 -0
  93. package/dist/commands/ticket/spec.js +4 -2
  94. package/dist/commands/ticket/template/apply.js +4 -3
  95. package/dist/commands/ticket/template/create.js +2 -0
  96. package/dist/commands/ticket/template/index.js +1 -1
  97. package/dist/commands/ticket/update.js +2 -0
  98. package/dist/commands/work/index.js +1 -1
  99. package/dist/commands/work/revise.js +7 -1
  100. package/dist/commands/work/spawn.d.ts +2 -1
  101. package/dist/commands/work/spawn.js +131 -36
  102. package/dist/commands/work/start.d.ts +2 -1
  103. package/dist/commands/work/start.js +349 -69
  104. package/dist/commands/work/watch.js +10 -2
  105. package/dist/commands/workflow/create.js +3 -3
  106. package/dist/commands/workflow/switch.js +2 -1
  107. package/dist/commands/workspace/remove.js +0 -8
  108. package/dist/commands/workspace/use.js +1 -9
  109. package/dist/lib/agents/commands.js +18 -13
  110. package/dist/lib/database/index.d.ts +19 -12
  111. package/dist/lib/database/index.js +158 -42
  112. package/dist/lib/docker/resolve.js +1 -1
  113. package/dist/lib/execution/config.d.ts +6 -0
  114. package/dist/lib/execution/config.js +15 -2
  115. package/dist/lib/execution/devcontainer.d.ts +2 -0
  116. package/dist/lib/execution/devcontainer.js +41 -9
  117. package/dist/lib/execution/runners.d.ts +85 -3
  118. package/dist/lib/execution/runners.js +925 -228
  119. package/dist/lib/execution/spawner.d.ts +2 -2
  120. package/dist/lib/execution/spawner.js +4 -3
  121. package/dist/lib/execution/storage.d.ts +2 -1
  122. package/dist/lib/execution/storage.js +9 -13
  123. package/dist/lib/execution/types.d.ts +10 -1
  124. package/dist/lib/execution/types.js +3 -1
  125. package/dist/lib/init/index.js +1 -0
  126. package/dist/lib/machine-config.js +1 -1
  127. package/dist/lib/pmo/base-command.js +5 -9
  128. package/dist/lib/pmo/index.js +2 -0
  129. package/dist/lib/pmo/schema.d.ts +6 -0
  130. package/dist/lib/pmo/schema.js +36 -0
  131. package/dist/lib/pmo/storage/base.js +3 -3
  132. package/dist/lib/pmo/storage/index.d.ts +16 -1
  133. package/dist/lib/pmo/storage/index.js +45 -0
  134. package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
  135. package/dist/lib/pmo/storage/roadmaps.js +301 -0
  136. package/dist/lib/pmo/storage/specs.js +2 -0
  137. package/dist/lib/pmo/storage/types.d.ts +14 -0
  138. package/dist/lib/pmo/sync-manager.d.ts +1 -1
  139. package/dist/lib/pmo/sync-manager.js +1 -1
  140. package/dist/lib/pmo/types.d.ts +41 -0
  141. package/dist/lib/pmo/utils.d.ts +2 -0
  142. package/dist/lib/pmo/utils.js +22 -1
  143. package/dist/lib/repos/index.js +7 -1
  144. package/dist/lib/terminal.d.ts +31 -0
  145. package/dist/lib/terminal.js +48 -0
  146. package/dist/lib/themes.d.ts +21 -3
  147. package/dist/lib/themes.js +80 -23
  148. package/dist/lib/workspace-config.d.ts +80 -0
  149. package/dist/lib/workspace-config.js +100 -0
  150. package/oclif.manifest.json +4065 -3225
  151. package/package.json +10 -6
  152. package/LICENSE +0 -21
@@ -82,9 +82,10 @@ export default class TicketLink extends PMOCommand {
82
82
  await this.addDependency(this.storage, this.pmoPath, ticketId, targetId, dependencyType, ticket.title);
83
83
  return;
84
84
  }
85
- // Interactive mode: show menu in a loop
85
+ // Interactive mode: show menu in a loop - user interaction requires sequential processing
86
86
  let continueLoop = true;
87
87
  while (continueLoop) {
88
+ // eslint-disable-next-line no-await-in-loop
88
89
  const allTickets = await this.storage.listTickets(projectId);
89
90
  const otherTickets = allTickets.filter(t => t.id !== ticketId);
90
91
  const menuChoices = [
@@ -95,6 +96,7 @@ export default class TicketLink extends PMOCommand {
95
96
  { id: 'remove', name: 'Remove dependency' },
96
97
  { id: 'done', name: 'Done' },
97
98
  ];
99
+ // eslint-disable-next-line no-await-in-loop
98
100
  const action = await this.selectFromList({
99
101
  message: `Dependencies for ${ticket.id}:`,
100
102
  items: menuChoices,
@@ -117,15 +119,18 @@ export default class TicketLink extends PMOCommand {
117
119
  continue;
118
120
  }
119
121
  if (action === 'view') {
122
+ // eslint-disable-next-line no-await-in-loop
120
123
  await this.viewDependencies(this.storage, ticketId, ticket, flags.all);
121
124
  continue;
122
125
  }
123
126
  if (action === 'remove') {
127
+ // eslint-disable-next-line no-await-in-loop
124
128
  const dependencies = await this.storage.listTicketDependencies(ticketId);
125
129
  if (dependencies.length === 0) {
126
130
  this.log(styles.muted('\nNo dependencies to remove.'));
127
131
  continue;
128
132
  }
133
+ // eslint-disable-next-line no-await-in-loop
129
134
  const depChoices = await Promise.all(dependencies.map(async (dep) => {
130
135
  const depTicket = await this.storage.getTicket(dep.dependsOnTicketId);
131
136
  return {
@@ -134,6 +139,7 @@ export default class TicketLink extends PMOCommand {
134
139
  type: dep.dependencyType
135
140
  };
136
141
  }));
142
+ // eslint-disable-next-line no-await-in-loop
137
143
  const selected = await this.selectFromList({
138
144
  message: 'Select dependency to remove:',
139
145
  items: depChoices,
@@ -147,7 +153,9 @@ export default class TicketLink extends PMOCommand {
147
153
  }
148
154
  const selectedDep = depChoices.find(d => d.id === selected);
149
155
  if (selectedDep) {
156
+ // eslint-disable-next-line no-await-in-loop
150
157
  await this.storage.deleteTicketDependency(ticketId, selected, selectedDep.type);
158
+ // eslint-disable-next-line no-await-in-loop
151
159
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
152
160
  this.log(styles.success(`\n✅ Removed dependency: ${ticketId} → ${selected}`));
153
161
  }
@@ -158,6 +166,7 @@ export default class TicketLink extends PMOCommand {
158
166
  this.log(styles.muted('\nNo other tickets to link to.'));
159
167
  continue;
160
168
  }
169
+ // eslint-disable-next-line no-await-in-loop
161
170
  const targetId = await this.selectFromList({
162
171
  message: `Select ticket that ${ticketId} ${action === 'blocks' ? 'is blocked by' : action === 'relates_to' ? 'relates to' : 'duplicates'}:`,
163
172
  items: otherTickets,
@@ -167,6 +176,7 @@ export default class TicketLink extends PMOCommand {
167
176
  jsonMode: jsonMode ? { flags, commandName: 'ticket link' } : null,
168
177
  });
169
178
  if (targetId) {
179
+ // eslint-disable-next-line no-await-in-loop
170
180
  await this.addDependency(this.storage, this.pmoPath, ticketId, targetId, action, ticket.title);
171
181
  }
172
182
  }
@@ -215,8 +225,12 @@ export default class TicketLink extends PMOCommand {
215
225
  const otherDeps = dependencies.filter(d => d.dependencyType !== 'blocks');
216
226
  if (otherDeps.length > 0) {
217
227
  this.log(styles.muted('\n Related:'));
218
- for (const dep of otherDeps) {
219
- const relatedTicket = await this.storage.getTicket(dep.dependsOnTicketId);
228
+ // Fetch all related tickets in parallel
229
+ const relatedTickets = await Promise.all(otherDeps.map(async (dep) => ({
230
+ dep,
231
+ ticket: await this.storage.getTicket(dep.dependsOnTicketId)
232
+ })));
233
+ for (const { dep, ticket: relatedTicket } of relatedTickets) {
220
234
  if (relatedTicket) {
221
235
  this.log(` - ${dep.dependencyType}: ${relatedTicket.id} - ${relatedTicket.title}`);
222
236
  }
@@ -80,7 +80,9 @@ export default class TicketLinkRemove extends PMOCommand {
80
80
  this.log(styles.muted('\nCancelled.'));
81
81
  return;
82
82
  }
83
+ // Delete sequentially for data integrity
83
84
  for (const dep of dependencies) {
85
+ // eslint-disable-next-line no-await-in-loop
84
86
  await this.storage.deleteTicketDependency(args.id, dep.dependsOnTicketId, dep.dependencyType);
85
87
  }
86
88
  await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
@@ -58,9 +58,8 @@ export default class TicketList extends Command {
58
58
  const { flags } = await this.parse(TicketList);
59
59
  // When --all is set, we don't need to select a specific project
60
60
  // Otherwise, use the normal project selection flow
61
- let pmoContext;
62
61
  // Get PMO context - no project selection needed
63
- pmoContext = await getPMOContext({
62
+ const pmoContext = await getPMOContext({
64
63
  logger: (msg) => this.log(styles.muted(msg)),
65
64
  });
66
65
  try {
@@ -187,8 +187,10 @@ export default class TicketMove extends PMOCommand {
187
187
  // Move each ticket
188
188
  let successCount = 0;
189
189
  let failCount = 0;
190
+ // Process sequentially for clear success/failure logging
190
191
  for (const ticketId of selectedTickets) {
191
192
  try {
193
+ // eslint-disable-next-line no-await-in-loop
192
194
  await this.storage.moveTicket(projectId, ticketId, targetColumn);
193
195
  this.log(styles.success(`Moved ${ticketId} to ${targetColumn}`));
194
196
  successCount++;
@@ -235,15 +235,17 @@ export default class TicketProject extends PMOCommand {
235
235
  return;
236
236
  }
237
237
  }
238
- // Move each ticket using the storage method
238
+ // Move each ticket using the storage method - sequential for clear logging
239
239
  let lastMovedTicket;
240
240
  for (const ticketId of selectedTickets) {
241
241
  const ticket = tickets.find(t => t.id === ticketId);
242
242
  // Unlink from epic if needed
243
243
  if (ticket?.epicId) {
244
+ // eslint-disable-next-line no-await-in-loop
244
245
  await this.storage.updateTicket(ticketId, { epicId: undefined });
245
246
  }
246
247
  // Move ticket to new project
248
+ // eslint-disable-next-line no-await-in-loop
247
249
  lastMovedTicket = await this.storage.moveTicketToProject(ticketId, targetProjectId);
248
250
  this.log(styles.success(` Moved ${ticketId} to ${targetProjectId}`));
249
251
  }
@@ -252,8 +252,10 @@ export default class TicketReassign extends PMOCommand {
252
252
  // Reassign each ticket
253
253
  let successCount = 0;
254
254
  let failCount = 0;
255
+ // Process sequentially for clear success/failure logging
255
256
  for (const ticketId of selectedTickets) {
256
257
  try {
258
+ // eslint-disable-next-line no-await-in-loop
257
259
  await this.storage.updateTicket(ticketId, { assignee: targetAssignee || undefined });
258
260
  const action = targetAssignee ? `Reassigned to ${targetAssignee}` : 'Unassigned';
259
261
  this.log(styles.success(`${ticketId}: ${action}`));
@@ -214,9 +214,10 @@ export default class TicketSpec extends PMOCommand {
214
214
  this.log(styles.muted('No tickets selected.'));
215
215
  return;
216
216
  }
217
- // Handle unlink
217
+ // Handle unlink - sequential for clear logging
218
218
  if (flags.unlink) {
219
219
  for (const ticketId of selectedTickets) {
220
+ // eslint-disable-next-line no-await-in-loop
220
221
  await this.storage.updateTicket(ticketId, { specId: undefined });
221
222
  this.log(styles.success(` Unlinked spec from ${ticketId}`));
222
223
  }
@@ -248,8 +249,9 @@ export default class TicketSpec extends PMOCommand {
248
249
  if (!spec) {
249
250
  this.error(`Spec not found: ${specId}`);
250
251
  }
251
- // Assign spec to all selected tickets
252
+ // Assign spec to all selected tickets - sequential for clear logging
252
253
  for (const ticketId of selectedTickets) {
254
+ // eslint-disable-next-line no-await-in-loop
253
255
  await this.storage.updateTicket(ticketId, { specId });
254
256
  this.log(styles.success(` Linked ${ticketId} to ${specId}`));
255
257
  }
@@ -127,8 +127,8 @@ export default class TicketTemplateApply extends PMOCommand {
127
127
  let category = flags.category || template.defaultCategory;
128
128
  let assignee = flags.assignee || template.defaultAssignee;
129
129
  let owner = flags.owner || template.defaultOwner;
130
- let statusId = flags.status || template.defaultStatusId;
131
- let labels = flags.labels ? flags.labels.split(',').map(l => l.trim()).filter(l => l) : template.defaultLabels;
130
+ const statusId = flags.status || template.defaultStatusId;
131
+ const labels = flags.labels ? flags.labels.split(',').map(l => l.trim()).filter(Boolean) : template.defaultLabels;
132
132
  let description = flags.description || template.descriptionTemplate;
133
133
  // Interactive mode - prompt for values
134
134
  if (flags.interactive || !title) {
@@ -207,9 +207,10 @@ export default class TicketTemplateApply extends PMOCommand {
207
207
  description,
208
208
  epicId: flags.epic,
209
209
  });
210
- // Add subtasks from template (unless disabled)
210
+ // Add subtasks from template (unless disabled) - sequential for ordering
211
211
  if (!flags['no-subtasks'] && template.suggestedSubtasks.length > 0) {
212
212
  for (const subtask of template.suggestedSubtasks) {
213
+ // eslint-disable-next-line no-await-in-loop
213
214
  await this.storage.addSubtask(ticket.id, subtask.title);
214
215
  }
215
216
  }
@@ -134,6 +134,7 @@ export default class TicketTemplateCreate extends PMOCommand {
134
134
  if (wantSubtasks) {
135
135
  let addMore = true;
136
136
  while (addMore) {
137
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop for subtask creation
137
138
  const { subtaskTitle } = await inquirer.prompt([{
138
139
  type: 'input',
139
140
  name: 'subtaskTitle',
@@ -141,6 +142,7 @@ export default class TicketTemplateCreate extends PMOCommand {
141
142
  validate: (input) => input.length > 0 || 'Title is required',
142
143
  }]);
143
144
  subtasks.push(subtaskTitle);
145
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop continuation
144
146
  const { another } = await inquirer.prompt([{
145
147
  type: 'list',
146
148
  name: 'another',
@@ -2,7 +2,7 @@ import { Flags } 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, } from '../../../lib/prompt-json.js';
5
+ import { shouldOutputJson } from '../../../lib/prompt-json.js';
6
6
  export default class TicketTemplateIndex extends PMOCommand {
7
7
  static description = 'Interactive menu for ticket template operations';
8
8
  static aliases = ['ticket:templates'];
@@ -288,6 +288,7 @@ export default class TicketUpdate extends PMOCommand {
288
288
  // Update each ticket
289
289
  let successCount = 0;
290
290
  let failCount = 0;
291
+ // Process sequentially for clear success/failure logging
291
292
  for (const ticketId of selectedTickets) {
292
293
  try {
293
294
  const changes = {};
@@ -297,6 +298,7 @@ export default class TicketUpdate extends PMOCommand {
297
298
  if (updateCategory !== undefined) {
298
299
  changes.category = updateCategory || undefined;
299
300
  }
301
+ // eslint-disable-next-line no-await-in-loop
300
302
  await this.storage.updateTicket(ticketId, changes);
301
303
  const updates = [];
302
304
  if (updatePriority !== undefined)
@@ -1,6 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
- import { shouldOutputJson, } from '../../lib/prompt-json.js';
3
+ import { shouldOutputJson } from '../../lib/prompt-json.js';
4
4
  export default class Work extends PMOCommand {
5
5
  static description = 'Interactive menu for work operations (ownership, assignment, execution)';
6
6
  static examples = [
@@ -9,7 +9,7 @@ import { getWorkColumnSetting, findColumnByName } from '../../lib/pmo/utils.js';
9
9
  import { styles } from '../../lib/styles.js';
10
10
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
11
11
  import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
12
- import { runExecution, isDockerRunning } from '../../lib/execution/runners.js';
12
+ import { runExecution, isDockerRunning, isDevcontainerCliInstalled } from '../../lib/execution/runners.js';
13
13
  import { ExecutionStorage } from '../../lib/execution/storage.js';
14
14
  import { loadExecutionConfig, getTerminalApp, getShell, hasTerminalPreference, hasShellPreference } from '../../lib/execution/config.js';
15
15
  import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
@@ -84,6 +84,12 @@ export default class WorkRevise extends PMOCommand {
84
84
  'Please start Docker Desktop and try again.\n\n' +
85
85
  'Alternatively, use --run-on-host to run directly on your machine (bypasses sandbox).');
86
86
  }
87
+ // Early devcontainer CLI check
88
+ if (!flags['run-on-host'] && !isDevcontainerCliInstalled()) {
89
+ return handleError('DEVCONTAINER_CLI_NOT_INSTALLED', 'devcontainer CLI is not installed.\n\n' +
90
+ 'Install with: npm install -g @devcontainers/cli\n\n' +
91
+ 'Alternatively, use --run-on-host to run directly on your machine (bypasses sandbox).');
92
+ }
87
93
  // Get workspace info
88
94
  let workspaceInfo;
89
95
  try {
@@ -10,7 +10,7 @@ export default class WorkSpawn extends PMOCommand {
10
10
  column: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  strategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- mode: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
15
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
16
  'run-on-host': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -23,6 +23,7 @@ export default class WorkSpawn extends PMOCommand {
23
23
  'no-pr': import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
24
  action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
25
  session: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
26
+ focus: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
27
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
28
  };
28
29
  execute(): Promise<void>;
@@ -5,8 +5,7 @@ import Database from 'better-sqlite3';
5
5
  import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
6
6
  import { styles } from '../../lib/styles.js';
7
7
  import { getWorkspaceInfo, getTicketTmuxSession, killTmuxSession } from '../../lib/agents/commands.js';
8
- import { ExecutionStorage } from '../../lib/execution/storage.js';
9
- import { isDockerRunning } from '../../lib/execution/runners.js';
8
+ import { isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from '../../lib/execution/runners.js';
10
9
  import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
11
10
  export default class WorkSpawn extends PMOCommand {
12
11
  static description = 'Spawn work for multiple tickets by column (batch mode)';
@@ -49,10 +48,10 @@ export default class WorkSpawn extends PMOCommand {
49
48
  description: 'Show what would be spawned without executing',
50
49
  default: false,
51
50
  }),
52
- mode: Flags.string({
53
- char: 'm',
54
- description: 'Runtime mode for spawned agents',
55
- options: ['foreground', 'background', 'tmux', 'terminal', 'devcontainer', 'docker', 'vm'],
51
+ display: Flags.string({
52
+ char: 'd',
53
+ description: 'Display mode for spawned agents (foreground not available for batch)',
54
+ options: ['terminal', 'background'],
56
55
  }),
57
56
  executor: Flags.string({
58
57
  char: 'e',
@@ -106,6 +105,10 @@ export default class WorkSpawn extends PMOCommand {
106
105
  options: ['tmux', 'direct'],
107
106
  default: 'tmux',
108
107
  }),
108
+ focus: Flags.boolean({
109
+ description: 'Bring terminal to foreground when opening new tabs (default: opens in background)',
110
+ default: false,
111
+ }),
109
112
  };
110
113
  async execute() {
111
114
  const { flags, argv } = await this.parse(WorkSpawn);
@@ -133,10 +136,9 @@ export default class WorkSpawn extends PMOCommand {
133
136
  catch {
134
137
  return handleError('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt init" first.');
135
138
  }
136
- // Open database for execution storage
139
+ // Open database
137
140
  const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
138
141
  const db = new Database(dbPath);
139
- const executionStorage = new ExecutionStorage(db);
140
142
  try {
141
143
  // Get board to list available columns
142
144
  const board = await this.storage.getBoard(projectId);
@@ -446,9 +448,10 @@ export default class WorkSpawn extends PMOCommand {
446
448
  return;
447
449
  }
448
450
  // Batch mode settings - prompt once for all tickets
449
- let batchMode = flags.mode;
451
+ let batchDisplay = flags.display;
450
452
  let batchOutput = flags.output;
451
- let batchSkipPermissions = flags['skip-permissions'];
453
+ // Track permission mode - default to 'safe', check flag to determine if prompting needed
454
+ let batchPermissionMode = flags['skip-permissions'] ? 'danger' : 'safe';
452
455
  let batchCreatePr = flags['create-pr'];
453
456
  let batchNoPr = flags['no-pr'];
454
457
  let batchRunOnHost = flags['run-on-host'];
@@ -473,6 +476,11 @@ export default class WorkSpawn extends PMOCommand {
473
476
  name: `${a.id.padEnd(12)} - ${a.description || a.name}`,
474
477
  value: a.id,
475
478
  }));
479
+ // Add adhoc option at the end
480
+ actionChoices.push({
481
+ name: 'adhoc - Unstructured exploration/debugging',
482
+ value: '__adhoc__',
483
+ });
476
484
  const { selectedAction } = await inquirer.prompt([
477
485
  {
478
486
  type: 'list',
@@ -482,12 +490,27 @@ export default class WorkSpawn extends PMOCommand {
482
490
  default: 'implement',
483
491
  },
484
492
  ]);
485
- batchAction = selectedAction;
493
+ batchAction = selectedAction === '__adhoc__' ? 'adhoc' : selectedAction;
486
494
  }
487
495
  // Now fetch action details after selection is made
488
- selectedActionDetails = await this.storage.getAction(batchAction || 'implement');
496
+ if (batchAction === 'adhoc') {
497
+ // Adhoc is a synthetic action, not stored in database
498
+ selectedActionDetails = {
499
+ id: 'adhoc',
500
+ name: 'Ad-hoc',
501
+ description: 'Unstructured exploration and debugging',
502
+ prompt: 'You are working on an ad-hoc session for exploration and debugging. Help the user with whatever they need.',
503
+ modifiesCode: false,
504
+ defaultMoveToCategory: 'started',
505
+ isBuiltin: false,
506
+ createdAt: new Date(),
507
+ };
508
+ }
509
+ else {
510
+ selectedActionDetails = await this.storage.getAction(batchAction || 'implement');
511
+ }
489
512
  // Check if any explicit settings were provided via flags
490
- const hasExplicitSettings = flags.mode || flags.output || flags['skip-permissions'] ||
513
+ const hasExplicitSettings = flags.display || flags.output || flags['skip-permissions'] ||
491
514
  flags['create-pr'] || flags['no-pr'] || flags['run-on-host'];
492
515
  // Offer to use default settings if no explicit flags provided
493
516
  if (!hasExplicitSettings) {
@@ -511,14 +534,14 @@ export default class WorkSpawn extends PMOCommand {
511
534
  if (useDefaults) {
512
535
  // Apply defaults
513
536
  if (hasDevcontainer) {
514
- batchMode = 'devcontainer';
537
+ batchDisplay = 'devcontainer';
515
538
  batchDisplayMode = 'terminal';
516
539
  }
517
540
  else {
518
- batchMode = 'terminal';
541
+ batchDisplay = 'terminal';
519
542
  }
520
543
  batchOutput = 'interactive';
521
- batchSkipPermissions = false;
544
+ batchPermissionMode = 'safe';
522
545
  // For non-code-modifying actions, don't create PRs
523
546
  if (modifiesCode) {
524
547
  batchCreatePr = true;
@@ -532,20 +555,35 @@ export default class WorkSpawn extends PMOCommand {
532
555
  }
533
556
  }
534
557
  // Prompt for environment (devcontainer vs host) if devcontainer available and not already set
535
- if (hasDevcontainer && !batchRunOnHost && !batchMode) {
558
+ if (hasDevcontainer && !batchRunOnHost && !batchDisplay) {
559
+ // Check devcontainer prerequisites upfront
560
+ const dockerRunning = isDockerRunning();
561
+ const devcontainerCliInstalled = isDevcontainerCliInstalled();
562
+ const devcontainerReady = dockerRunning && devcontainerCliInstalled;
563
+ // Build missing requirements message for devcontainer option
564
+ let devcontainerLabel = '🐳 devcontainer (sandboxed, recommended)';
565
+ if (!devcontainerReady) {
566
+ const missing = [];
567
+ if (!dockerRunning)
568
+ missing.push('Docker');
569
+ if (!devcontainerCliInstalled)
570
+ missing.push('devcontainer CLI');
571
+ devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
572
+ }
536
573
  let environmentSelected = false;
537
574
  while (!environmentSelected) {
575
+ // eslint-disable-next-line no-await-in-loop -- Interactive loop with retry on Docker check
538
576
  const { selectedEnvironment } = await inquirer.prompt([
539
577
  {
540
578
  type: 'list',
541
579
  name: 'selectedEnvironment',
542
580
  message: 'Where should agents run?',
543
581
  choices: [
544
- { name: '🐳 devcontainer (sandboxed, recommended)', value: 'devcontainer' },
582
+ { name: devcontainerLabel, value: 'devcontainer', disabled: !devcontainerReady },
545
583
  { name: '💻 host (runs directly on your machine)', value: 'host' },
546
584
  { name: '✗ cancel', value: 'cancel' },
547
585
  ],
548
- default: 'devcontainer',
586
+ default: devcontainerReady ? 'devcontainer' : 'host',
549
587
  },
550
588
  ]);
551
589
  if (selectedEnvironment === 'cancel') {
@@ -554,6 +592,7 @@ export default class WorkSpawn extends PMOCommand {
554
592
  return;
555
593
  }
556
594
  if (selectedEnvironment === 'devcontainer') {
595
+ // Double-check prerequisites (in case user retried after starting Docker)
557
596
  if (!isDockerRunning()) {
558
597
  this.log('');
559
598
  this.warn('Docker is not running.\n' +
@@ -562,10 +601,59 @@ export default class WorkSpawn extends PMOCommand {
562
601
  this.log('');
563
602
  continue;
564
603
  }
565
- batchMode = 'devcontainer';
604
+ if (!isDevcontainerCliInstalled()) {
605
+ this.log('');
606
+ this.warn('devcontainer CLI is not installed.\n' +
607
+ 'Install with: npm install -g @devcontainers/cli\n' +
608
+ 'Or select "host" to run directly on your machine.');
609
+ this.log('');
610
+ continue;
611
+ }
612
+ // Check GitHub token is available for git push operations
613
+ if (!isGitHubTokenAvailable()) {
614
+ const tokenChoices = [
615
+ { name: 'Yes, continue anyway (git push may fail)', value: 'continue' },
616
+ { name: 'No, let me run gh auth login first', value: 'cancel' },
617
+ { name: 'Switch to host mode instead', value: 'host' },
618
+ ];
619
+ const tokenMessage = 'GitHub token not found. Git push may fail. Continue without token?';
620
+ if (jsonMode) {
621
+ outputPromptAsJson(buildPromptConfig('list', 'tokenAction', tokenMessage, tokenChoices), createMetadata('work spawn', flags));
622
+ db.close();
623
+ return;
624
+ }
625
+ this.log('');
626
+ this.warn('GitHub token not found.\n' +
627
+ 'Git push operations may fail inside containers.\n' +
628
+ 'Run `gh auth login` to authenticate, or continue without token.');
629
+ this.log('');
630
+ // eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
631
+ const { tokenAction } = await inquirer.prompt([
632
+ {
633
+ type: 'list',
634
+ name: 'tokenAction',
635
+ message: tokenMessage,
636
+ choices: tokenChoices,
637
+ default: 'continue',
638
+ },
639
+ ]);
640
+ if (tokenAction === 'cancel') {
641
+ db.close();
642
+ this.log(styles.muted('Run `gh auth login` and try again.'));
643
+ return;
644
+ }
645
+ if (tokenAction === 'host') {
646
+ batchRunOnHost = true;
647
+ environmentSelected = true;
648
+ continue;
649
+ }
650
+ // tokenAction === 'continue' - fall through to devcontainer setup
651
+ }
652
+ batchDisplay = 'devcontainer';
566
653
  environmentSelected = true;
567
654
  // For devcontainer, prompt for display mode
568
655
  // Simplified: tmux is always used inside container for session persistence
656
+ // eslint-disable-next-line no-await-in-loop -- Follow-up prompt after selection
569
657
  const { selectedDisplay } = await inquirer.prompt([
570
658
  {
571
659
  type: 'list',
@@ -589,7 +677,7 @@ export default class WorkSpawn extends PMOCommand {
589
677
  }
590
678
  }
591
679
  // Prompt for display mode if not already set (for host mode without devcontainer)
592
- if (!batchMode) {
680
+ if (!batchDisplay) {
593
681
  const { selectedMode } = await inquirer.prompt([
594
682
  {
595
683
  type: 'list',
@@ -601,15 +689,15 @@ export default class WorkSpawn extends PMOCommand {
601
689
  ],
602
690
  },
603
691
  ]);
604
- batchMode = selectedMode;
692
+ batchDisplay = selectedMode;
605
693
  }
606
694
  // Default to interactive output mode (streaming UI)
607
695
  // Can be overridden via --output flag if needed
608
696
  if (!batchOutput) {
609
697
  batchOutput = 'interactive';
610
698
  }
611
- // Prompt for permissions mode if not provided
612
- if (!batchSkipPermissions) {
699
+ // Prompt for permissions mode if not explicitly set via --skip-permissions flag
700
+ if (!flags['skip-permissions']) {
613
701
  const { permissionMode } = await inquirer.prompt([
614
702
  {
615
703
  type: 'list',
@@ -622,7 +710,7 @@ export default class WorkSpawn extends PMOCommand {
622
710
  default: 'danger',
623
711
  },
624
712
  ]);
625
- batchSkipPermissions = permissionMode === 'danger';
713
+ batchPermissionMode = permissionMode;
626
714
  }
627
715
  // Prompt for PR creation if not provided AND action modifies code
628
716
  // Skip this prompt entirely for non-code-modifying actions (like groom)
@@ -661,6 +749,7 @@ export default class WorkSpawn extends PMOCommand {
661
749
  // Spawn each ticket - work:start will create ephemeral agents on-demand
662
750
  let successCount = 0;
663
751
  let failCount = 0;
752
+ // Process sequentially for clear logging and resource management
664
753
  for (const ticket of ticketsToSpawn) {
665
754
  try {
666
755
  this.log(styles.muted(`Starting ${ticket.id} with ephemeral agent...`));
@@ -669,24 +758,26 @@ export default class WorkSpawn extends PMOCommand {
669
758
  // Pass --ephemeral to signal work:start should create an ephemeral agent
670
759
  const startArgs = [ticket.id, '--project', projectId, '--ephemeral'];
671
760
  if (flags['per-ticket']) {
672
- // Per-ticket mode: only pass mode flag, let start prompt for the rest
673
- if (batchMode)
674
- startArgs.push('--mode', batchMode);
675
- if (batchDisplayMode)
676
- startArgs.push('--display', batchDisplayMode);
761
+ // Per-ticket mode: only pass display flag, let start prompt for the rest
762
+ // batchDisplayMode is for devcontainer, batchDisplay is for host
763
+ const displayToUse = batchDisplayMode || batchDisplay;
764
+ if (displayToUse && displayToUse !== 'devcontainer')
765
+ startArgs.push('--display', displayToUse);
677
766
  if (flags.executor)
678
767
  startArgs.push('--executor', flags.executor);
679
768
  if (batchRunOnHost)
680
769
  startArgs.push('--run-on-host');
681
770
  if (flags.force)
682
771
  startArgs.push('--force');
772
+ if (flags.focus)
773
+ startArgs.push('--focus');
683
774
  }
684
775
  else {
685
776
  // Batch mode: pass all settings to skip prompts
686
- if (batchMode)
687
- startArgs.push('--mode', batchMode);
688
- if (batchDisplayMode)
689
- startArgs.push('--display', batchDisplayMode);
777
+ // batchDisplayMode is for devcontainer, batchDisplay is for host
778
+ const displayToUse = batchDisplayMode || batchDisplay;
779
+ if (displayToUse && displayToUse !== 'devcontainer')
780
+ startArgs.push('--display', displayToUse);
690
781
  if (flags.executor)
691
782
  startArgs.push('--executor', flags.executor);
692
783
  if (batchRunOnHost)
@@ -695,8 +786,8 @@ export default class WorkSpawn extends PMOCommand {
695
786
  startArgs.push('--force');
696
787
  if (batchOutput)
697
788
  startArgs.push('--output', batchOutput);
698
- if (batchSkipPermissions)
699
- startArgs.push('--skip-permissions');
789
+ // Always pass permission mode to skip the prompt in work:start
790
+ startArgs.push('--permission-mode', batchPermissionMode);
700
791
  if (batchCreatePr)
701
792
  startArgs.push('--create-pr');
702
793
  if (batchNoPr)
@@ -706,7 +797,11 @@ export default class WorkSpawn extends PMOCommand {
706
797
  // Pass session manager (tmux inside container by default)
707
798
  if (flags.session)
708
799
  startArgs.push('--session', flags.session);
800
+ // Pass focus flag (brings terminal to foreground)
801
+ if (flags.focus)
802
+ startArgs.push('--focus');
709
803
  }
804
+ // eslint-disable-next-line no-await-in-loop
710
805
  await this.config.runCommand('work:start', startArgs);
711
806
  successCount++;
712
807
  }