@proletariat/cli 0.3.24 → 0.3.26

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 (134) hide show
  1. package/dist/commands/action/create.js +3 -3
  2. package/dist/commands/action/index.js +2 -2
  3. package/dist/commands/action/update.js +3 -3
  4. package/dist/commands/agent/auth.js +1 -1
  5. package/dist/commands/agent/cleanup.js +6 -6
  6. package/dist/commands/agent/discover.js +1 -1
  7. package/dist/commands/agent/remove.js +4 -4
  8. package/dist/commands/autocomplete/setup.d.ts +2 -2
  9. package/dist/commands/autocomplete/setup.js +5 -5
  10. package/dist/commands/branch/create.js +31 -30
  11. package/dist/commands/category/create.js +4 -5
  12. package/dist/commands/category/delete.js +2 -3
  13. package/dist/commands/category/rename.js +2 -3
  14. package/dist/commands/claude.d.ts +2 -8
  15. package/dist/commands/claude.js +26 -26
  16. package/dist/commands/commit.d.ts +2 -8
  17. package/dist/commands/commit.js +4 -26
  18. package/dist/commands/config/index.d.ts +2 -10
  19. package/dist/commands/config/index.js +8 -34
  20. package/dist/commands/docker/index.d.ts +2 -2
  21. package/dist/commands/docker/index.js +8 -8
  22. package/dist/commands/epic/activate.js +9 -17
  23. package/dist/commands/epic/archive.js +13 -24
  24. package/dist/commands/epic/create.js +7 -6
  25. package/dist/commands/epic/delete.js +4 -5
  26. package/dist/commands/epic/move.js +28 -47
  27. package/dist/commands/epic/progress.js +10 -14
  28. package/dist/commands/epic/project.js +42 -59
  29. package/dist/commands/epic/reorder.js +25 -30
  30. package/dist/commands/epic/spec.d.ts +1 -0
  31. package/dist/commands/epic/spec.js +39 -40
  32. package/dist/commands/epic/ticket.d.ts +2 -0
  33. package/dist/commands/epic/ticket.js +63 -37
  34. package/dist/commands/feedback/index.d.ts +10 -0
  35. package/dist/commands/feedback/index.js +60 -0
  36. package/dist/commands/feedback/list.d.ts +12 -0
  37. package/dist/commands/feedback/list.js +126 -0
  38. package/dist/commands/feedback/submit.d.ts +16 -0
  39. package/dist/commands/feedback/submit.js +220 -0
  40. package/dist/commands/feedback/view.d.ts +15 -0
  41. package/dist/commands/feedback/view.js +109 -0
  42. package/dist/commands/gh/index.js +4 -0
  43. package/dist/commands/link/index.js +2 -2
  44. package/dist/commands/pmo/init.d.ts +2 -2
  45. package/dist/commands/pmo/init.js +7 -7
  46. package/dist/commands/project/spec.js +6 -6
  47. package/dist/commands/repo/create.d.ts +38 -0
  48. package/dist/commands/repo/create.js +283 -0
  49. package/dist/commands/repo/index.js +7 -0
  50. package/dist/commands/roadmap/add-project.js +9 -22
  51. package/dist/commands/roadmap/create.d.ts +0 -1
  52. package/dist/commands/roadmap/create.js +46 -40
  53. package/dist/commands/roadmap/delete.js +10 -24
  54. package/dist/commands/roadmap/generate.d.ts +1 -0
  55. package/dist/commands/roadmap/generate.js +21 -22
  56. package/dist/commands/roadmap/remove-project.js +14 -34
  57. package/dist/commands/roadmap/reorder.js +19 -26
  58. package/dist/commands/roadmap/update.js +27 -26
  59. package/dist/commands/roadmap/view.js +5 -12
  60. package/dist/commands/session/attach.d.ts +1 -8
  61. package/dist/commands/session/attach.js +93 -59
  62. package/dist/commands/session/health.d.ts +29 -0
  63. package/dist/commands/session/health.js +495 -0
  64. package/dist/commands/session/index.js +4 -0
  65. package/dist/commands/session/list.d.ts +0 -8
  66. package/dist/commands/session/list.js +130 -81
  67. package/dist/commands/spec/create.js +1 -1
  68. package/dist/commands/spec/edit.js +64 -35
  69. package/dist/commands/staff/add.d.ts +2 -2
  70. package/dist/commands/staff/add.js +15 -14
  71. package/dist/commands/staff/index.js +2 -2
  72. package/dist/commands/staff/remove.js +4 -4
  73. package/dist/commands/status/index.js +6 -7
  74. package/dist/commands/support/book.d.ts +10 -0
  75. package/dist/commands/support/book.js +54 -0
  76. package/dist/commands/support/discord.d.ts +10 -0
  77. package/dist/commands/support/discord.js +54 -0
  78. package/dist/commands/support/docs.d.ts +10 -0
  79. package/dist/commands/support/docs.js +54 -0
  80. package/dist/commands/support/index.d.ts +19 -0
  81. package/dist/commands/support/index.js +81 -0
  82. package/dist/commands/support/issues.d.ts +11 -0
  83. package/dist/commands/support/issues.js +77 -0
  84. package/dist/commands/support/logs.d.ts +18 -0
  85. package/dist/commands/support/logs.js +247 -0
  86. package/dist/commands/template/apply.js +10 -11
  87. package/dist/commands/template/create.js +18 -17
  88. package/dist/commands/template/index.d.ts +2 -2
  89. package/dist/commands/template/index.js +6 -6
  90. package/dist/commands/template/save.js +8 -7
  91. package/dist/commands/template/update.js +6 -7
  92. package/dist/commands/terminal/title.d.ts +2 -26
  93. package/dist/commands/terminal/title.js +4 -33
  94. package/dist/commands/theme/index.d.ts +2 -2
  95. package/dist/commands/theme/index.js +19 -18
  96. package/dist/commands/theme/set.d.ts +2 -2
  97. package/dist/commands/theme/set.js +5 -5
  98. package/dist/commands/ticket/create.js +52 -26
  99. package/dist/commands/ticket/delete.js +15 -13
  100. package/dist/commands/ticket/edit.js +59 -20
  101. package/dist/commands/ticket/epic.js +12 -10
  102. package/dist/commands/ticket/move.d.ts +7 -0
  103. package/dist/commands/ticket/move.js +132 -0
  104. package/dist/commands/ticket/project.js +11 -9
  105. package/dist/commands/ticket/reassign.js +23 -19
  106. package/dist/commands/ticket/spec.js +7 -5
  107. package/dist/commands/ticket/update.js +55 -53
  108. package/dist/commands/whoami.js +1 -0
  109. package/dist/commands/work/ready.js +7 -7
  110. package/dist/commands/work/revise.js +13 -11
  111. package/dist/commands/work/spawn.d.ts +1 -0
  112. package/dist/commands/work/spawn.js +225 -64
  113. package/dist/commands/work/start.d.ts +1 -0
  114. package/dist/commands/work/start.js +301 -173
  115. package/dist/hooks/init.js +4 -0
  116. package/dist/lib/execution/runners.js +21 -17
  117. package/dist/lib/execution/session-utils.d.ts +60 -0
  118. package/dist/lib/execution/session-utils.js +162 -0
  119. package/dist/lib/execution/spawner.d.ts +2 -0
  120. package/dist/lib/execution/spawner.js +42 -0
  121. package/dist/lib/flags/resolver.d.ts +2 -2
  122. package/dist/lib/flags/resolver.js +15 -0
  123. package/dist/lib/init/index.js +18 -0
  124. package/dist/lib/multiline-input.d.ts +63 -0
  125. package/dist/lib/multiline-input.js +360 -0
  126. package/dist/lib/pr/index.d.ts +4 -0
  127. package/dist/lib/pr/index.js +32 -14
  128. package/dist/lib/prompt-command.d.ts +3 -0
  129. package/dist/lib/prompt-json.d.ts +77 -6
  130. package/dist/lib/prompt-json.js +46 -0
  131. package/dist/lib/repos/git.d.ts +7 -0
  132. package/dist/lib/repos/git.js +20 -0
  133. package/oclif.manifest.json +2913 -2246
  134. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { execSync } from 'node:child_process';
3
2
  import * as path from 'node:path';
4
3
  import Database from 'better-sqlite3';
5
4
  import { styles } from '../../lib/styles.js';
6
5
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
7
6
  import { ExecutionStorage } from '../../lib/execution/index.js';
7
+ import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
8
8
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
9
9
  export default class SessionList extends PMOCommand {
10
10
  static description = 'List active tmux sessions (host and container)';
@@ -28,6 +28,7 @@ export default class SessionList extends PMOCommand {
28
28
  // Get workspace info for execution records
29
29
  let executionStorage = null;
30
30
  let db = null;
31
+ let hasWorkspace = true;
31
32
  try {
32
33
  const workspaceInfo = getWorkspaceInfo();
33
34
  const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
@@ -35,47 +36,120 @@ export default class SessionList extends PMOCommand {
35
36
  executionStorage = new ExecutionStorage(db);
36
37
  }
37
38
  catch {
38
- this.log('');
39
- this.log(styles.muted('Not in a workspace. Run from a proletariat HQ directory.'));
40
- this.log('');
41
- return;
39
+ // Not in a workspace, but we can still discover tmux sessions
40
+ hasWorkspace = false;
42
41
  }
43
42
  try {
44
43
  // DB-driven approach: Start with executions, verify tmux sessions exist
45
- const runningExecutions = executionStorage.listExecutions({ status: 'running' }) || [];
46
- const startingExecutions = executionStorage.listExecutions({ status: 'starting' }) || [];
44
+ const runningExecutions = executionStorage?.listExecutions({ status: 'running' }) || [];
45
+ const startingExecutions = executionStorage?.listExecutions({ status: 'starting' }) || [];
47
46
  const activeExecutions = [...runningExecutions, ...startingExecutions];
48
47
  // Get list of actual tmux sessions for verification
49
- const hostTmuxSessions = this.getHostTmuxSessionNames();
50
- const containerTmuxSessions = this.getContainerTmuxSessionMap();
48
+ const hostTmuxSessions = getHostTmuxSessionNames();
49
+ const containerTmuxSessions = getContainerTmuxSessionMap();
50
+ // Flatten all container sessions for orphan detection
51
+ const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
52
+ // Track which tmux sessions we've matched to DB records
53
+ const matchedHostSessions = new Set();
54
+ const matchedContainerSessions = new Set();
51
55
  // Build verified session list from DB records
52
56
  const sessions = [];
53
57
  for (const exec of activeExecutions) {
54
- if (!exec.sessionId)
55
- continue; // Skip executions without sessionId
56
58
  const isContainer = exec.environment === 'devcontainer';
57
59
  let exists = false;
58
60
  let containerId;
59
- if (isContainer && exec.containerId) {
60
- // Check if session exists in container
61
- const containerSessions = containerTmuxSessions.get(exec.containerId);
62
- exists = containerSessions?.includes(exec.sessionId) ?? false;
63
- containerId = exec.containerId;
61
+ let actualSessionId = exec.sessionId;
62
+ // If sessionId is NULL, try to find session by naming convention
63
+ if (!exec.sessionId) {
64
+ if (isContainer && exec.containerId) {
65
+ const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
66
+ const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
67
+ if (match) {
68
+ actualSessionId = match;
69
+ exists = true;
70
+ containerId = exec.containerId;
71
+ }
72
+ }
73
+ else {
74
+ const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
75
+ if (match) {
76
+ actualSessionId = match;
77
+ exists = true;
78
+ }
79
+ }
80
+ // If still no match, skip this execution (truly has no session)
81
+ if (!actualSessionId) {
82
+ continue;
83
+ }
64
84
  }
65
85
  else {
66
- // Check if session exists on host
67
- exists = hostTmuxSessions.includes(exec.sessionId);
86
+ // sessionId is set, verify it exists
87
+ if (isContainer && exec.containerId) {
88
+ const containerSessions = containerTmuxSessions.get(exec.containerId);
89
+ exists = containerSessions?.includes(exec.sessionId) ?? false;
90
+ containerId = exec.containerId;
91
+ }
92
+ else {
93
+ exists = hostTmuxSessions.includes(exec.sessionId);
94
+ }
95
+ }
96
+ // Track matched sessions to detect orphans later
97
+ if (exists && actualSessionId) {
98
+ if (isContainer && containerId) {
99
+ matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
100
+ }
101
+ else {
102
+ matchedHostSessions.add(actualSessionId);
103
+ }
68
104
  }
69
105
  // Only include if session exists, unless --all flag
70
- if (exists || flags.all) {
106
+ // Note: actualSessionId is guaranteed non-null here due to continue above
107
+ if ((exists || flags.all) && actualSessionId) {
71
108
  sessions.push({
72
- sessionId: exec.sessionId,
109
+ sessionId: actualSessionId,
73
110
  ticketId: exec.ticketId,
74
111
  agentName: exec.agentName,
75
112
  status: exists ? exec.status : 'stale',
76
113
  environment: isContainer ? 'container' : 'host',
77
114
  containerId,
78
115
  exists,
116
+ source: 'db',
117
+ });
118
+ }
119
+ }
120
+ // Discover orphan sessions: tmux sessions matching prlt pattern but not in DB
121
+ // Host sessions
122
+ for (const sessionName of hostTmuxSessions) {
123
+ if (matchedHostSessions.has(sessionName))
124
+ continue;
125
+ const parsed = parseSessionName(sessionName);
126
+ if (parsed) {
127
+ sessions.push({
128
+ sessionId: sessionName,
129
+ ticketId: parsed.ticketId,
130
+ agentName: parsed.agentName,
131
+ status: 'orphan',
132
+ environment: 'host',
133
+ exists: true,
134
+ source: 'discovered',
135
+ });
136
+ }
137
+ }
138
+ // Container sessions
139
+ for (const { sessionName, containerId } of allContainerSessions) {
140
+ if (matchedContainerSessions.has(`${containerId}:${sessionName}`))
141
+ continue;
142
+ const parsed = parseSessionName(sessionName);
143
+ if (parsed) {
144
+ sessions.push({
145
+ sessionId: sessionName,
146
+ ticketId: parsed.ticketId,
147
+ agentName: parsed.agentName,
148
+ status: 'orphan',
149
+ environment: 'container',
150
+ containerId,
151
+ exists: true,
152
+ source: 'discovered',
79
153
  });
80
154
  }
81
155
  }
@@ -84,23 +158,30 @@ export default class SessionList extends PMOCommand {
84
158
  this.log(styles.header('🖥️ Active Sessions'));
85
159
  this.log('═'.repeat(90));
86
160
  this.log(styles.muted(' ' +
87
- padEnd('Session', 28) +
88
- padEnd('Ticket', 12) +
89
- padEnd('Agent', 14) +
90
- padEnd('Type', 15) +
161
+ 'Session'.padEnd(34) +
162
+ 'Ticket'.padEnd(12) +
163
+ 'Agent'.padEnd(18) +
164
+ 'Type'.padEnd(15) +
91
165
  'Status'));
92
- this.log(' ' + '─'.repeat(80));
166
+ this.log(' ' + '─'.repeat(88));
93
167
  for (const session of sessions) {
94
168
  const typeIcon = session.environment === 'container' ? '🐳 container' : '💻 host';
95
169
  const statusColor = session.status === 'running' ? styles.success :
96
170
  session.status === 'starting' ? styles.warning :
97
- session.status === 'stale' ? styles.error : styles.muted;
171
+ session.status === 'stale' ? styles.error :
172
+ session.status === 'orphan' ? styles.warning : styles.muted;
173
+ // For orphan sessions, append source indicator
174
+ const statusText = session.source === 'discovered' ? `${session.status}*` : session.status;
175
+ // Truncate long session names to fit column
176
+ const displaySession = session.sessionId.length > 32
177
+ ? session.sessionId.substring(0, 29) + '...'
178
+ : session.sessionId;
98
179
  this.log(' ' +
99
- padEnd(session.sessionId, 28) +
100
- padEnd(session.ticketId, 12) +
101
- padEnd(session.agentName, 14) +
102
- padEnd(typeIcon, 15) +
103
- statusColor(session.status));
180
+ displaySession.padEnd(34) +
181
+ session.ticketId.padEnd(12) +
182
+ session.agentName.padEnd(18) +
183
+ typeIcon.padEnd(15) +
184
+ statusColor(statusText));
104
185
  }
105
186
  this.log('');
106
187
  this.log('═'.repeat(90));
@@ -118,12 +199,27 @@ export default class SessionList extends PMOCommand {
118
199
  this.log(styles.muted(' Run `prlt work stop <work-id>` to clean up.'));
119
200
  this.log('');
120
201
  }
202
+ // Show orphan sessions note
203
+ const orphanSessions = sessions.filter(s => s.source === 'discovered');
204
+ if (orphanSessions.length > 0) {
205
+ this.log(styles.muted(`\n📋 ${orphanSessions.length} session(s) discovered from tmux (marked with *).`));
206
+ this.log(styles.muted(' These sessions match the prlt naming pattern but are not tracked in the database.'));
207
+ this.log('');
208
+ }
121
209
  }
122
210
  else {
123
211
  this.log('');
124
- this.log(styles.muted('No active sessions found.'));
125
- this.log('');
126
- this.log(styles.muted('Start work with: prlt work start <ticket-id>'));
212
+ if (!hasWorkspace) {
213
+ this.log(styles.muted('Not in a workspace and no prlt-pattern tmux sessions found.'));
214
+ this.log('');
215
+ this.log(styles.muted('Run from a proletariat HQ directory to see tracked sessions,'));
216
+ this.log(styles.muted('or start work with: prlt work start <ticket-id>'));
217
+ }
218
+ else {
219
+ this.log(styles.muted('No active sessions found.'));
220
+ this.log('');
221
+ this.log(styles.muted('Start work with: prlt work start <ticket-id>'));
222
+ }
127
223
  this.log('');
128
224
  }
129
225
  }
@@ -131,51 +227,4 @@ export default class SessionList extends PMOCommand {
131
227
  db?.close();
132
228
  }
133
229
  }
134
- /**
135
- * Get list of host tmux session names
136
- */
137
- getHostTmuxSessionNames() {
138
- try {
139
- execSync('which tmux', { stdio: 'pipe' });
140
- const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
141
- if (!output)
142
- return [];
143
- return output.split('\n');
144
- }
145
- catch {
146
- return [];
147
- }
148
- }
149
- /**
150
- * Get map of containerId -> tmux session names
151
- */
152
- getContainerTmuxSessionMap() {
153
- const sessionMap = new Map();
154
- try {
155
- const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
156
- if (!containersOutput)
157
- return sessionMap;
158
- for (const containerId of containersOutput.split('\n')) {
159
- try {
160
- const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
161
- if (tmuxOutput) {
162
- sessionMap.set(containerId, tmuxOutput.split('\n'));
163
- }
164
- }
165
- catch {
166
- // Container has no tmux sessions
167
- }
168
- }
169
- }
170
- catch {
171
- // Docker not available
172
- }
173
- return sessionMap;
174
- }
175
- }
176
- // =============================================================================
177
- // Helper Functions
178
- // =============================================================================
179
- function padEnd(str, length) {
180
- return str.padEnd(length);
181
230
  }
@@ -103,7 +103,7 @@ export default class SpecCreate extends PMOCommand {
103
103
  });
104
104
  resolver.addPrompt({
105
105
  flagName: 'problem',
106
- type: 'input',
106
+ type: 'multiline',
107
107
  message: 'Problem statement (optional):',
108
108
  when: (ctx) => ctx.flags.title !== undefined,
109
109
  });
@@ -1,9 +1,9 @@
1
1
  import { Flags, Args } from '@oclif/core';
2
- import inquirer from 'inquirer';
3
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
3
  import { styles } from '../../lib/styles.js';
5
4
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
6
5
  import { FlagResolver } from '../../lib/flags/index.js';
6
+ import { multiLineInput } from '../../lib/multiline-input.js';
7
7
  export default class SpecEdit extends PMOCommand {
8
8
  static description = 'Edit an existing spec';
9
9
  static examples = [
@@ -114,6 +114,25 @@ export default class SpecEdit extends PMOCommand {
114
114
  const hasFlags = flags.title || flags.status || flags.type || flags.problem ||
115
115
  flags.solution || flags.decisions;
116
116
  if (flags.interactive || !hasFlags) {
117
+ // In JSON mode without flags, output a form prompt instead of interactive prompts
118
+ if (jsonMode) {
119
+ const { outputPromptAsJson, buildFormPromptConfig } = await import('../../lib/prompt-json.js');
120
+ const formConfig = buildFormPromptConfig([
121
+ { type: 'input', name: 'title', message: 'Title:', default: spec.title },
122
+ { type: 'list', name: 'status', message: 'Status:', choices: statusChoices, default: spec.status },
123
+ { type: 'list', name: 'type', message: 'Type:', choices: typeChoices, default: spec.type || '' },
124
+ { type: 'multiline', name: 'problem', message: 'Problem statement:', default: spec.problem || '' },
125
+ { type: 'multiline', name: 'solution', message: 'Solution:', default: spec.solution || '' },
126
+ { type: 'multiline', name: 'decisions', message: 'Design decisions:', default: spec.decisions || '' },
127
+ ]);
128
+ formConfig.context = {
129
+ hint: `Edit spec with: prlt spec edit ${specId} --title "..." --problem "..." --json`,
130
+ specId,
131
+ currentValues: { title: spec.title, status: spec.status, type: spec.type, problem: spec.problem, solution: spec.solution, decisions: spec.decisions },
132
+ };
133
+ outputPromptAsJson(formConfig, createMetadata('spec edit', flags));
134
+ return; // outputPromptAsJson exits, but TypeScript doesn't know
135
+ }
117
136
  // Interactive mode - prompt for editable fields
118
137
  updates = await this.promptForEdits(spec, typeChoices, statusChoices);
119
138
  }
@@ -162,7 +181,8 @@ export default class SpecEdit extends PMOCommand {
162
181
  this.log(styles.muted(`View spec: prlt spec view ${updatedSpec.id}`));
163
182
  }
164
183
  async promptForEdits(spec, typeChoices, statusChoices) {
165
- const answers = await inquirer.prompt([
184
+ // First prompt for title, status, and type
185
+ const basicAnswers = await this.prompt([
166
186
  {
167
187
  type: 'input',
168
188
  name: 'title',
@@ -184,48 +204,57 @@ export default class SpecEdit extends PMOCommand {
184
204
  choices: typeChoices,
185
205
  default: spec.type || '',
186
206
  },
187
- {
188
- type: 'editor',
189
- name: 'problem',
190
- message: 'Problem statement (opens $EDITOR):',
191
- default: spec.problem || '',
192
- waitForUseInput: false,
193
- },
194
- {
195
- type: 'editor',
196
- name: 'solution',
197
- message: 'Solution (opens $EDITOR):',
198
- default: spec.solution || '',
199
- waitForUseInput: false,
200
- },
201
- {
202
- type: 'editor',
203
- name: 'decisions',
204
- message: 'Design decisions (opens $EDITOR):',
205
- default: spec.decisions || '',
206
- waitForUseInput: false,
207
- },
208
- ]);
207
+ ], null);
208
+ // Prompt for problem statement using multiline input
209
+ const problemResult = await multiLineInput({
210
+ message: 'Problem statement:',
211
+ default: spec.problem || '',
212
+ hint: 'Describe the problem this spec addresses. Ctrl+D to finish, Ctrl+C to cancel',
213
+ });
214
+ if (problemResult.cancelled) {
215
+ throw new Error('Edit cancelled');
216
+ }
217
+ // Prompt for solution using multiline input
218
+ const solutionResult = await multiLineInput({
219
+ message: 'Solution:',
220
+ default: spec.solution || '',
221
+ hint: 'Describe the proposed solution. Ctrl+D to finish, Ctrl+C to cancel',
222
+ });
223
+ if (solutionResult.cancelled) {
224
+ throw new Error('Edit cancelled');
225
+ }
226
+ // Prompt for decisions using multiline input
227
+ const decisionsResult = await multiLineInput({
228
+ message: 'Design decisions:',
229
+ default: spec.decisions || '',
230
+ hint: 'Document key design decisions. Ctrl+D to finish, Ctrl+C to cancel',
231
+ });
232
+ if (decisionsResult.cancelled) {
233
+ throw new Error('Edit cancelled');
234
+ }
209
235
  // Build updates object with only changed fields
210
236
  const updates = {};
211
- if (answers.title !== spec.title) {
212
- updates.title = answers.title;
237
+ if (basicAnswers.title !== spec.title) {
238
+ updates.title = basicAnswers.title;
213
239
  }
214
- if (answers.status !== spec.status) {
215
- updates.status = answers.status;
240
+ if (basicAnswers.status !== spec.status) {
241
+ updates.status = basicAnswers.status;
216
242
  }
217
- const newType = answers.type === '' ? undefined : answers.type;
243
+ const newType = basicAnswers.type === '' ? undefined : basicAnswers.type;
218
244
  if (newType !== spec.type) {
219
245
  updates.type = newType;
220
246
  }
221
- if (answers.problem !== (spec.problem || '')) {
222
- updates.problem = answers.problem || undefined;
247
+ if (problemResult.value !== (spec.problem || '')) {
248
+ // Preserve empty string to allow clearing the field
249
+ updates.problem = problemResult.value;
223
250
  }
224
- if (answers.solution !== (spec.solution || '')) {
225
- updates.solution = answers.solution || undefined;
251
+ if (solutionResult.value !== (spec.solution || '')) {
252
+ // Preserve empty string to allow clearing the field
253
+ updates.solution = solutionResult.value;
226
254
  }
227
- if (answers.decisions !== (spec.decisions || '')) {
228
- updates.decisions = answers.decisions || undefined;
255
+ if (decisionsResult.value !== (spec.decisions || '')) {
256
+ // Preserve empty string to allow clearing the field
257
+ updates.decisions = decisionsResult.value;
229
258
  }
230
259
  return updates;
231
260
  }
@@ -1,5 +1,5 @@
1
- import { Command } from '@oclif/core';
2
- export default class Add extends Command {
1
+ import { PromptCommand } from '../../lib/prompt-command.js';
2
+ export default class Add extends PromptCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static args: {
@@ -1,11 +1,12 @@
1
- import { Command, Args, Flags } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
+ import { PromptCommand } from '../../lib/prompt-command.js';
4
5
  import { getWorkspaceInfo, validateAgentNames, addAgentsToWorkspace } from '../../lib/agents/commands.js';
5
6
  import { ensureBuiltinThemes, BUILTIN_THEMES, isValidAgentName, normalizeAgentName } from '../../lib/themes.js';
6
7
  import { getTheme, getThemes, getAvailableThemeNames, getActiveTheme } from '../../lib/database/index.js';
7
8
  import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
8
- export default class Add extends Command {
9
+ export default class Add extends PromptCommand {
9
10
  static description = 'Add new agents to the workspace';
10
11
  static examples = [
11
12
  '<%= config.bin %> <%= command.id %> zeus',
@@ -82,13 +83,13 @@ export default class Add extends Command {
82
83
  return;
83
84
  }
84
85
  // Interactive selection from theme
85
- const { selected } = await inquirer.prompt([{
86
+ const { selected } = await this.prompt([{
86
87
  type: 'checkbox',
87
88
  name: 'selected',
88
89
  message: selectMessage,
89
90
  choices: nameChoices,
90
91
  validate: (input) => input.length > 0 || 'Please select at least one name'
91
- }]);
92
+ }], null);
92
93
  agentNames = selected;
93
94
  }
94
95
  // Interactive mode: show names from workspace's active theme
@@ -127,19 +128,19 @@ export default class Add extends Command {
127
128
  new inquirer.Separator(),
128
129
  { name: chalk.blue(nameChoices[nameChoices.length - 1].name), value: nameChoices[nameChoices.length - 1].value }
129
130
  ];
130
- const { selected } = await inquirer.prompt([{
131
+ const { selected } = await this.prompt([{
131
132
  type: 'checkbox',
132
133
  name: 'selected',
133
134
  message: selectMessage,
134
135
  choices,
135
136
  pageSize: 20,
136
137
  validate: (input) => input.length > 0 || 'Please select at least one name'
137
- }]);
138
+ }], null);
138
139
  // Check if custom was selected
139
140
  const hasCustom = selected.includes('__custom__');
140
141
  const themedSelections = selected.filter((s) => s !== '__custom__');
141
142
  if (hasCustom) {
142
- const { customNames } = await inquirer.prompt([{
143
+ const { customNames } = await this.prompt([{
143
144
  type: 'input',
144
145
  name: 'customNames',
145
146
  message: 'Enter custom agent names (space-separated):',
@@ -148,7 +149,7 @@ export default class Add extends Command {
148
149
  return 'Please enter at least one name';
149
150
  return true;
150
151
  }
151
- }]);
152
+ }], null);
152
153
  const rawNames = customNames.trim().split(/\s+/);
153
154
  const normalizedCustom = rawNames.map((n) => normalizeAgentName(n)).filter((n) => n && isValidAgentName(n));
154
155
  agentNames.push(...normalizedCustom);
@@ -187,14 +188,14 @@ export default class Add extends Command {
187
188
  new inquirer.Separator(),
188
189
  { name: chalk.blue(themeChoices[themeChoices.length - 1].name), value: themeChoices[themeChoices.length - 1].value }
189
190
  ];
190
- const { selectedTheme } = await inquirer.prompt([{
191
+ const { selectedTheme } = await this.prompt([{
191
192
  type: 'list',
192
193
  name: 'selectedTheme',
193
194
  message: selectMessage,
194
195
  choices: interactiveChoices
195
- }]);
196
+ }], null);
196
197
  if (selectedTheme === '__custom__') {
197
- const { customNames } = await inquirer.prompt([{
198
+ const { customNames } = await this.prompt([{
198
199
  type: 'input',
199
200
  name: 'customNames',
200
201
  message: 'Enter custom agent names (space-separated):',
@@ -203,7 +204,7 @@ export default class Add extends Command {
203
204
  return 'Please enter at least one name';
204
205
  return true;
205
206
  }
206
- }]);
207
+ }], null);
207
208
  const rawNames = customNames.trim().split(/\s+/);
208
209
  const normalizedCustom = rawNames.map((n) => ({
209
210
  original: n,
@@ -222,14 +223,14 @@ export default class Add extends Command {
222
223
  themeId = selectedTheme;
223
224
  const theme = getTheme(workspaceInfo.path, selectedTheme);
224
225
  const availableNames = getAvailableThemeNames(workspaceInfo.path, selectedTheme);
225
- const { selected } = await inquirer.prompt([{
226
+ const { selected } = await this.prompt([{
226
227
  type: 'checkbox',
227
228
  name: 'selected',
228
229
  message: `Select agents from ${theme?.display_name}:`,
229
230
  choices: availableNames.map(name => ({ name, value: name })),
230
231
  pageSize: 20,
231
232
  validate: (input) => input.length > 0 || 'Please select at least one name'
232
- }]);
233
+ }], null);
233
234
  agentNames = selected;
234
235
  }
235
236
  }
@@ -46,7 +46,7 @@ export default class Staff extends PMOCommand {
46
46
  }
47
47
  this.log(colors.primary('Staff Agents'));
48
48
  this.log(colors.textMuted('Persistent agents with dedicated worktrees.\n'));
49
- const { action } = await inquirer.prompt([{
49
+ const { action } = await this.prompt([{
50
50
  type: 'list',
51
51
  name: 'action',
52
52
  message,
@@ -57,7 +57,7 @@ export default class Staff extends PMOCommand {
57
57
  new inquirer.Separator(),
58
58
  { name: '❌ ' + menuChoices[3].name, value: menuChoices[3].value },
59
59
  ]
60
- }]);
60
+ }], null);
61
61
  if (action === 'cancel') {
62
62
  this.log(colors.textMuted('Operation cancelled.'));
63
63
  return;
@@ -71,7 +71,7 @@ export default class Remove extends PMOCommand {
71
71
  outputPromptAsJson(buildPromptConfig('list', 'name', selectMessage, agentChoices), createMetadata('staff remove', flags));
72
72
  return;
73
73
  }
74
- const { selected } = await inquirer.prompt([
74
+ const { selected } = await this.prompt([
75
75
  {
76
76
  type: 'list',
77
77
  name: 'selected',
@@ -82,7 +82,7 @@ export default class Remove extends PMOCommand {
82
82
  { name: '❌ ' + agentChoices[agentChoices.length - 1].name, value: agentChoices[agentChoices.length - 1].value }
83
83
  ]
84
84
  }
85
- ]);
85
+ ], null);
86
86
  if (selected === 'cancel') {
87
87
  this.log(colors.textMuted('Operation cancelled.'));
88
88
  return;
@@ -109,7 +109,7 @@ export default class Remove extends PMOCommand {
109
109
  outputPromptAsJson(buildPromptConfig('list', 'confirmed', confirmMessage, confirmChoices), createMetadata('staff remove', flags));
110
110
  return;
111
111
  }
112
- const { confirm } = await inquirer.prompt([
112
+ const { confirm } = await this.prompt([
113
113
  {
114
114
  type: 'list',
115
115
  name: 'confirm',
@@ -120,7 +120,7 @@ export default class Remove extends PMOCommand {
120
120
  ],
121
121
  default: 0 // Default to "No, cancel"
122
122
  }
123
- ]);
123
+ ], null);
124
124
  if (!confirm) {
125
125
  this.log(colors.textMuted('Removal cancelled.'));
126
126
  return;
@@ -1,4 +1,3 @@
1
- import inquirer from 'inquirer';
2
1
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
2
  import { FlagResolver, shouldOutputJson } from '../../lib/flags/index.js';
4
3
  export default class Status extends PMOCommand {
@@ -62,34 +61,34 @@ export default class Status extends PMOCommand {
62
61
  case 'update': {
63
62
  // First list statuses, then prompt for selection
64
63
  await this.config.runCommand('status:list', []);
65
- const { statusId } = await inquirer.prompt([{
64
+ const { statusId } = await this.prompt([{
66
65
  type: 'input',
67
66
  name: 'statusId',
68
67
  message: 'Status ID to update:',
69
68
  validate: (input) => input.length > 0 || 'Status ID is required',
70
- }]);
69
+ }], null);
71
70
  await this.config.runCommand('status:update', [statusId]);
72
71
  break;
73
72
  }
74
73
  case 'move': {
75
74
  await this.config.runCommand('status:list', []);
76
- const { statusId } = await inquirer.prompt([{
75
+ const { statusId } = await this.prompt([{
77
76
  type: 'input',
78
77
  name: 'statusId',
79
78
  message: 'Status ID to move:',
80
79
  validate: (input) => input.length > 0 || 'Status ID is required',
81
- }]);
80
+ }], null);
82
81
  await this.config.runCommand('status:move', [statusId]);
83
82
  break;
84
83
  }
85
84
  case 'delete': {
86
85
  await this.config.runCommand('status:list', []);
87
- const { statusId } = await inquirer.prompt([{
86
+ const { statusId } = await this.prompt([{
88
87
  type: 'input',
89
88
  name: 'statusId',
90
89
  message: 'Status ID to delete:',
91
90
  validate: (input) => input.length > 0 || 'Status ID is required',
92
- }]);
91
+ }], null);
93
92
  await this.config.runCommand('status:delete', [statusId]);
94
93
  break;
95
94
  }