@proletariat/cli 0.3.23 → 0.3.25

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 (235) hide show
  1. package/dist/commands/action/create.js +4 -4
  2. package/dist/commands/action/update.js +3 -3
  3. package/dist/commands/agent/{temp/cleanup.d.ts → cleanup.d.ts} +1 -1
  4. package/dist/commands/agent/{temp/cleanup.js → cleanup.js} +4 -4
  5. package/dist/commands/agent/index.js +8 -8
  6. package/dist/commands/branch/create.js +2 -2
  7. package/dist/commands/epic/activate.js +9 -17
  8. package/dist/commands/epic/archive.js +13 -24
  9. package/dist/commands/epic/create.d.ts +1 -0
  10. package/dist/commands/epic/create.js +46 -8
  11. package/dist/commands/epic/index.js +2 -2
  12. package/dist/commands/epic/move.js +28 -47
  13. package/dist/commands/epic/progress.js +10 -14
  14. package/dist/commands/epic/project.js +42 -59
  15. package/dist/commands/epic/reorder.js +25 -30
  16. package/dist/commands/epic/spec.d.ts +1 -0
  17. package/dist/commands/epic/spec.js +39 -40
  18. package/dist/commands/epic/ticket.d.ts +2 -0
  19. package/dist/commands/epic/ticket.js +63 -37
  20. package/dist/commands/feedback/index.d.ts +10 -0
  21. package/dist/commands/feedback/index.js +60 -0
  22. package/dist/commands/feedback/list.d.ts +12 -0
  23. package/dist/commands/feedback/list.js +126 -0
  24. package/dist/commands/feedback/submit.d.ts +16 -0
  25. package/dist/commands/feedback/submit.js +220 -0
  26. package/dist/commands/{template/phase/delete.d.ts → feedback/view.d.ts} +7 -5
  27. package/dist/commands/feedback/view.js +109 -0
  28. package/dist/commands/gh/index.js +4 -0
  29. package/dist/commands/{epic/link/remove.d.ts → link/create.d.ts} +6 -7
  30. package/dist/commands/link/create.js +141 -0
  31. package/dist/commands/{epic/link/relates.d.ts → link/index.d.ts} +4 -5
  32. package/dist/commands/link/index.js +87 -0
  33. package/dist/commands/{epic/link/duplicates.d.ts → link/list.d.ts} +7 -4
  34. package/dist/commands/link/list.js +182 -0
  35. package/dist/commands/{spec/link → link}/remove.d.ts +4 -5
  36. package/dist/commands/link/remove.js +120 -0
  37. package/dist/commands/mcp-server.d.ts +22 -0
  38. package/dist/commands/mcp-server.js +98 -0
  39. package/dist/commands/phase/create.js +1 -1
  40. package/dist/commands/project/create.d.ts +1 -0
  41. package/dist/commands/project/create.js +38 -4
  42. package/dist/commands/repo/create.d.ts +38 -0
  43. package/dist/commands/repo/create.js +283 -0
  44. package/dist/commands/repo/index.js +7 -0
  45. package/dist/commands/roadmap/add-project.js +9 -22
  46. package/dist/commands/roadmap/create.d.ts +0 -1
  47. package/dist/commands/roadmap/create.js +46 -40
  48. package/dist/commands/roadmap/delete.js +10 -24
  49. package/dist/commands/roadmap/generate.d.ts +1 -0
  50. package/dist/commands/roadmap/generate.js +21 -22
  51. package/dist/commands/roadmap/remove-project.js +14 -34
  52. package/dist/commands/roadmap/reorder.js +19 -26
  53. package/dist/commands/roadmap/update.js +27 -26
  54. package/dist/commands/roadmap/view.js +5 -12
  55. package/dist/commands/session/attach.d.ts +1 -8
  56. package/dist/commands/session/attach.js +93 -59
  57. package/dist/commands/session/list.d.ts +0 -8
  58. package/dist/commands/session/list.js +130 -81
  59. package/dist/commands/spec/create.d.ts +1 -0
  60. package/dist/commands/spec/create.js +44 -3
  61. package/dist/commands/spec/edit.js +63 -33
  62. package/dist/commands/spec/index.js +2 -2
  63. package/dist/commands/{agent/staff → staff}/add.js +10 -10
  64. package/dist/commands/{agent/staff → staff}/index.d.ts +1 -1
  65. package/dist/commands/{agent/staff → staff}/index.js +7 -7
  66. package/dist/commands/{agent/staff → staff}/list.js +3 -3
  67. package/dist/commands/{agent/staff → staff}/remove.d.ts +1 -1
  68. package/dist/commands/{agent/staff → staff}/remove.js +8 -8
  69. package/dist/commands/{template/phase/index.d.ts → support/book.d.ts} +2 -2
  70. package/dist/commands/support/book.js +54 -0
  71. package/dist/commands/{template/ticket/index.d.ts → support/discord.d.ts} +2 -2
  72. package/dist/commands/support/discord.js +54 -0
  73. package/dist/commands/support/docs.d.ts +10 -0
  74. package/dist/commands/support/docs.js +54 -0
  75. package/dist/commands/support/index.d.ts +19 -0
  76. package/dist/commands/support/index.js +81 -0
  77. package/dist/commands/support/issues.d.ts +11 -0
  78. package/dist/commands/support/issues.js +77 -0
  79. package/dist/commands/support/logs.d.ts +18 -0
  80. package/dist/commands/support/logs.js +247 -0
  81. package/dist/commands/{ticket/template → template}/apply.d.ts +8 -6
  82. package/dist/commands/template/apply.js +262 -0
  83. package/dist/commands/{ticket/template → template}/create.d.ts +5 -6
  84. package/dist/commands/template/create.js +238 -0
  85. package/dist/commands/template/index.js +48 -36
  86. package/dist/commands/{ticket/template → template}/save.d.ts +2 -2
  87. package/dist/commands/template/save.js +104 -0
  88. package/dist/commands/{phase/template → template}/update.d.ts +2 -2
  89. package/dist/commands/template/update.js +99 -0
  90. package/dist/commands/{agent/themes → theme}/add-names.d.ts +1 -1
  91. package/dist/commands/{agent/themes → theme}/add-names.js +6 -6
  92. package/dist/commands/{agent/themes → theme}/create.d.ts +1 -1
  93. package/dist/commands/{agent/themes → theme}/create.js +5 -5
  94. package/dist/commands/{agent/themes → theme}/index.d.ts +1 -1
  95. package/dist/commands/{agent/themes → theme}/index.js +10 -10
  96. package/dist/commands/{agent/themes → theme}/list.d.ts +1 -1
  97. package/dist/commands/{agent/themes → theme}/list.js +5 -5
  98. package/dist/commands/{agent/themes → theme}/set.d.ts +1 -1
  99. package/dist/commands/{agent/themes → theme}/set.js +7 -7
  100. package/dist/commands/ticket/create.d.ts +1 -0
  101. package/dist/commands/ticket/create.js +75 -15
  102. package/dist/commands/ticket/edit.js +44 -13
  103. package/dist/commands/ticket/index.js +6 -6
  104. package/dist/commands/ticket/move.d.ts +7 -0
  105. package/dist/commands/ticket/move.js +132 -0
  106. package/dist/commands/work/spawn.d.ts +1 -0
  107. package/dist/commands/work/spawn.js +72 -8
  108. package/dist/commands/work/start.js +6 -0
  109. package/dist/lib/execution/runners.js +21 -17
  110. package/dist/lib/execution/session-utils.d.ts +60 -0
  111. package/dist/lib/execution/session-utils.js +162 -0
  112. package/dist/lib/execution/spawner.d.ts +2 -0
  113. package/dist/lib/execution/spawner.js +42 -0
  114. package/dist/lib/flags/resolver.d.ts +2 -2
  115. package/dist/lib/flags/resolver.js +15 -0
  116. package/dist/lib/init/index.js +18 -0
  117. package/dist/lib/mcp/helpers.d.ts +43 -0
  118. package/dist/lib/mcp/helpers.js +57 -0
  119. package/dist/lib/mcp/index.d.ts +6 -0
  120. package/dist/lib/mcp/index.js +6 -0
  121. package/dist/lib/mcp/tools/action.d.ts +6 -0
  122. package/dist/lib/mcp/tools/action.js +88 -0
  123. package/dist/lib/mcp/tools/board.d.ts +6 -0
  124. package/dist/lib/mcp/tools/board.js +139 -0
  125. package/dist/lib/mcp/tools/category.d.ts +6 -0
  126. package/dist/lib/mcp/tools/category.js +84 -0
  127. package/dist/lib/mcp/tools/cli-passthrough.d.ts +15 -0
  128. package/dist/lib/mcp/tools/cli-passthrough.js +333 -0
  129. package/dist/lib/mcp/tools/epic.d.ts +6 -0
  130. package/dist/lib/mcp/tools/epic.js +178 -0
  131. package/dist/lib/mcp/tools/index.d.ts +18 -0
  132. package/dist/lib/mcp/tools/index.js +19 -0
  133. package/dist/lib/mcp/tools/phase.d.ts +6 -0
  134. package/dist/lib/mcp/tools/phase.js +131 -0
  135. package/dist/lib/mcp/tools/project.d.ts +6 -0
  136. package/dist/lib/mcp/tools/project.js +196 -0
  137. package/dist/lib/mcp/tools/roadmap.d.ts +6 -0
  138. package/dist/lib/mcp/tools/roadmap.js +123 -0
  139. package/dist/lib/mcp/tools/spec.d.ts +6 -0
  140. package/dist/lib/mcp/tools/spec.js +196 -0
  141. package/dist/lib/mcp/tools/status.d.ts +6 -0
  142. package/dist/lib/mcp/tools/status.js +109 -0
  143. package/dist/lib/mcp/tools/template.d.ts +6 -0
  144. package/dist/lib/mcp/tools/template.js +107 -0
  145. package/dist/lib/mcp/tools/ticket.d.ts +6 -0
  146. package/dist/lib/mcp/tools/ticket.js +393 -0
  147. package/dist/lib/mcp/tools/view.d.ts +6 -0
  148. package/dist/lib/mcp/tools/view.js +76 -0
  149. package/dist/lib/mcp/tools/work.d.ts +6 -0
  150. package/dist/lib/mcp/tools/work.js +132 -0
  151. package/dist/lib/mcp/tools/workflow.d.ts +6 -0
  152. package/dist/lib/mcp/tools/workflow.js +95 -0
  153. package/dist/lib/mcp/types.d.ts +17 -0
  154. package/dist/lib/mcp/types.js +4 -0
  155. package/dist/lib/multiline-input.d.ts +63 -0
  156. package/dist/lib/multiline-input.js +360 -0
  157. package/dist/lib/prompt-json.d.ts +57 -6
  158. package/dist/lib/prompt-json.js +45 -0
  159. package/dist/lib/repos/git.d.ts +7 -0
  160. package/dist/lib/repos/git.js +20 -0
  161. package/oclif.manifest.json +3690 -4995
  162. package/package.json +6 -4
  163. package/dist/commands/agent/temp/index.d.ts +0 -14
  164. package/dist/commands/agent/temp/index.js +0 -85
  165. package/dist/commands/agent/temp/list.d.ts +0 -7
  166. package/dist/commands/agent/temp/list.js +0 -108
  167. package/dist/commands/epic/link/block.d.ts +0 -14
  168. package/dist/commands/epic/link/block.js +0 -81
  169. package/dist/commands/epic/link/duplicates.js +0 -68
  170. package/dist/commands/epic/link/index.d.ts +0 -19
  171. package/dist/commands/epic/link/index.js +0 -272
  172. package/dist/commands/epic/link/relates.js +0 -68
  173. package/dist/commands/epic/link/remove.js +0 -93
  174. package/dist/commands/phase/template/apply.d.ts +0 -17
  175. package/dist/commands/phase/template/apply.js +0 -108
  176. package/dist/commands/phase/template/create.d.ts +0 -17
  177. package/dist/commands/phase/template/create.js +0 -104
  178. package/dist/commands/phase/template/delete.d.ts +0 -17
  179. package/dist/commands/phase/template/delete.js +0 -100
  180. package/dist/commands/phase/template/index.d.ts +0 -15
  181. package/dist/commands/phase/template/index.js +0 -130
  182. package/dist/commands/phase/template/list.d.ts +0 -16
  183. package/dist/commands/phase/template/list.js +0 -97
  184. package/dist/commands/phase/template/update.js +0 -89
  185. package/dist/commands/spec/link/depends.d.ts +0 -14
  186. package/dist/commands/spec/link/depends.js +0 -64
  187. package/dist/commands/spec/link/duplicates.d.ts +0 -14
  188. package/dist/commands/spec/link/duplicates.js +0 -63
  189. package/dist/commands/spec/link/index.d.ts +0 -19
  190. package/dist/commands/spec/link/index.js +0 -207
  191. package/dist/commands/spec/link/relates.d.ts +0 -14
  192. package/dist/commands/spec/link/relates.js +0 -63
  193. package/dist/commands/spec/link/remove.js +0 -96
  194. package/dist/commands/template/phase/apply.d.ts +0 -14
  195. package/dist/commands/template/phase/apply.js +0 -43
  196. package/dist/commands/template/phase/create.d.ts +0 -13
  197. package/dist/commands/template/phase/create.js +0 -38
  198. package/dist/commands/template/phase/delete.js +0 -36
  199. package/dist/commands/template/phase/index.js +0 -63
  200. package/dist/commands/template/phase/list.d.ts +0 -11
  201. package/dist/commands/template/phase/list.js +0 -36
  202. package/dist/commands/template/phase/update.d.ts +0 -14
  203. package/dist/commands/template/phase/update.js +0 -43
  204. package/dist/commands/template/ticket/apply.d.ts +0 -17
  205. package/dist/commands/template/ticket/apply.js +0 -60
  206. package/dist/commands/template/ticket/create.d.ts +0 -20
  207. package/dist/commands/template/ticket/create.js +0 -89
  208. package/dist/commands/template/ticket/delete.d.ts +0 -13
  209. package/dist/commands/template/ticket/delete.js +0 -38
  210. package/dist/commands/template/ticket/index.js +0 -63
  211. package/dist/commands/template/ticket/list.d.ts +0 -11
  212. package/dist/commands/template/ticket/list.js +0 -36
  213. package/dist/commands/template/ticket/save.d.ts +0 -15
  214. package/dist/commands/template/ticket/save.js +0 -46
  215. package/dist/commands/ticket/link/block.d.ts +0 -14
  216. package/dist/commands/ticket/link/block.js +0 -96
  217. package/dist/commands/ticket/link/duplicates.d.ts +0 -14
  218. package/dist/commands/ticket/link/duplicates.js +0 -95
  219. package/dist/commands/ticket/link/index.d.ts +0 -19
  220. package/dist/commands/ticket/link/index.js +0 -256
  221. package/dist/commands/ticket/link/relates.d.ts +0 -14
  222. package/dist/commands/ticket/link/relates.js +0 -95
  223. package/dist/commands/ticket/link/remove.d.ts +0 -16
  224. package/dist/commands/ticket/link/remove.js +0 -132
  225. package/dist/commands/ticket/template/apply.js +0 -252
  226. package/dist/commands/ticket/template/create.js +0 -386
  227. package/dist/commands/ticket/template/delete.d.ts +0 -17
  228. package/dist/commands/ticket/template/delete.js +0 -94
  229. package/dist/commands/ticket/template/index.d.ts +0 -15
  230. package/dist/commands/ticket/template/index.js +0 -120
  231. package/dist/commands/ticket/template/list.d.ts +0 -16
  232. package/dist/commands/ticket/template/list.js +0 -112
  233. package/dist/commands/ticket/template/save.js +0 -163
  234. /package/dist/commands/{agent/staff → staff}/add.d.ts +0 -0
  235. /package/dist/commands/{agent/staff → staff}/list.d.ts +0 -0
@@ -19,16 +19,9 @@ export default class SessionAttach extends PMOCommand {
19
19
  /**
20
20
  * Get verified sessions from DB that have actual tmux processes
21
21
  * DB-driven approach: Start with executions, verify tmux sessions exist
22
+ * Also discovers orphan sessions matching prlt naming pattern but not in DB
22
23
  */
23
24
  private getVerifiedSessions;
24
- /**
25
- * Get list of host tmux session names
26
- */
27
- private getHostTmuxSessionNames;
28
- /**
29
- * Get map of containerId -> tmux session names
30
- */
31
- private getContainerTmuxSessionMap;
32
25
  /**
33
26
  * Attach to session in current terminal
34
27
  */
@@ -7,6 +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
11
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
11
12
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
12
13
  export default class SessionAttach extends PMOCommand {
@@ -122,6 +123,7 @@ export default class SessionAttach extends PMOCommand {
122
123
  /**
123
124
  * Get verified sessions from DB that have actual tmux processes
124
125
  * DB-driven approach: Start with executions, verify tmux sessions exist
126
+ * Also discovers orphan sessions matching prlt naming pattern but not in DB
125
127
  */
126
128
  getVerifiedSessions() {
127
129
  const sessions = [];
@@ -134,87 +136,119 @@ export default class SessionAttach extends PMOCommand {
134
136
  executionStorage = new ExecutionStorage(db);
135
137
  }
136
138
  catch {
137
- return sessions; // Not in workspace
139
+ // Not in workspace, but we can still discover tmux sessions
138
140
  }
139
141
  try {
140
- // Get active executions from DB
141
- const activeExecutions = [
142
+ // Get actual tmux sessions for verification
143
+ const hostTmuxSessions = getHostTmuxSessionNames();
144
+ const containerTmuxSessions = getContainerTmuxSessionMap();
145
+ // Flatten all container sessions for orphan detection
146
+ const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
147
+ // Track which tmux sessions we've matched to DB records
148
+ const matchedHostSessions = new Set();
149
+ const matchedContainerSessions = new Set();
150
+ // Get active executions from DB (if available)
151
+ const activeExecutions = executionStorage ? [
142
152
  ...(executionStorage.listExecutions({ status: 'running' }) || []),
143
153
  ...(executionStorage.listExecutions({ status: 'starting' }) || []),
144
- ];
145
- // Get actual tmux sessions for verification
146
- const hostTmuxSessions = this.getHostTmuxSessionNames();
147
- const containerTmuxSessions = this.getContainerTmuxSessionMap();
154
+ ] : [];
148
155
  for (const exec of activeExecutions) {
149
- if (!exec.sessionId)
150
- continue;
151
156
  const isContainer = exec.environment === 'devcontainer';
152
157
  let exists = false;
153
- if (isContainer && exec.containerId) {
154
- const containerSessions = containerTmuxSessions.get(exec.containerId);
155
- exists = containerSessions?.includes(exec.sessionId) ?? false;
158
+ let containerId;
159
+ let actualSessionId = exec.sessionId;
160
+ // If sessionId is NULL, try to find session by naming convention
161
+ if (!exec.sessionId) {
162
+ if (isContainer && exec.containerId) {
163
+ const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
164
+ const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
165
+ if (match) {
166
+ actualSessionId = match;
167
+ exists = true;
168
+ containerId = exec.containerId;
169
+ }
170
+ }
171
+ else {
172
+ const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
173
+ if (match) {
174
+ actualSessionId = match;
175
+ exists = true;
176
+ }
177
+ }
178
+ // If still no match, skip this execution
179
+ if (!actualSessionId) {
180
+ continue;
181
+ }
156
182
  }
157
183
  else {
158
- exists = hostTmuxSessions.includes(exec.sessionId);
184
+ // sessionId is set, verify it exists
185
+ if (isContainer && exec.containerId) {
186
+ const containerSessions = containerTmuxSessions.get(exec.containerId);
187
+ exists = containerSessions?.includes(exec.sessionId) ?? false;
188
+ containerId = exec.containerId;
189
+ }
190
+ else {
191
+ exists = hostTmuxSessions.includes(exec.sessionId);
192
+ }
159
193
  }
160
- // Only include sessions that actually exist
161
- if (exists) {
194
+ // Track matched sessions
195
+ if (exists && actualSessionId) {
196
+ if (isContainer && containerId) {
197
+ matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
198
+ }
199
+ else {
200
+ matchedHostSessions.add(actualSessionId);
201
+ }
162
202
  sessions.push({
163
- name: exec.sessionId,
164
- sessionId: exec.sessionId,
203
+ name: actualSessionId,
204
+ sessionId: actualSessionId,
165
205
  type: isContainer ? 'container' : 'host',
166
- containerId: exec.containerId,
206
+ containerId,
167
207
  ticketId: exec.ticketId,
168
208
  agentName: exec.agentName,
209
+ source: 'db',
169
210
  });
170
211
  }
171
212
  }
172
- }
173
- finally {
174
- db?.close();
175
- }
176
- return sessions;
177
- }
178
- /**
179
- * Get list of host tmux session names
180
- */
181
- getHostTmuxSessionNames() {
182
- try {
183
- execSync('which tmux', { stdio: 'pipe' });
184
- const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
185
- if (!output)
186
- return [];
187
- return output.split('\n');
188
- }
189
- catch {
190
- return [];
191
- }
192
- }
193
- /**
194
- * Get map of containerId -> tmux session names
195
- */
196
- getContainerTmuxSessionMap() {
197
- const sessionMap = new Map();
198
- try {
199
- const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
200
- if (!containersOutput)
201
- return sessionMap;
202
- for (const containerId of containersOutput.split('\n')) {
203
- try {
204
- const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
205
- if (tmuxOutput) {
206
- sessionMap.set(containerId, tmuxOutput.split('\n'));
207
- }
213
+ // Discover orphan sessions: tmux sessions matching prlt pattern but not in DB
214
+ // Host sessions
215
+ for (const sessionName of hostTmuxSessions) {
216
+ if (matchedHostSessions.has(sessionName))
217
+ continue;
218
+ const parsed = parseSessionName(sessionName);
219
+ if (parsed) {
220
+ sessions.push({
221
+ name: sessionName,
222
+ sessionId: sessionName,
223
+ type: 'host',
224
+ ticketId: parsed.ticketId,
225
+ agentName: parsed.agentName,
226
+ source: 'discovered',
227
+ });
208
228
  }
209
- catch {
210
- // Container has no tmux sessions
229
+ }
230
+ // Container sessions
231
+ for (const { sessionName, containerId } of allContainerSessions) {
232
+ if (matchedContainerSessions.has(`${containerId}:${sessionName}`))
233
+ continue;
234
+ const parsed = parseSessionName(sessionName);
235
+ if (parsed) {
236
+ sessions.push({
237
+ name: sessionName,
238
+ sessionId: sessionName,
239
+ type: 'container',
240
+ containerId,
241
+ ticketId: parsed.ticketId,
242
+ agentName: parsed.agentName,
243
+ source: 'discovered',
244
+ });
211
245
  }
212
246
  }
213
247
  }
214
- catch {
215
- // Docker not available
248
+ finally {
249
+ db?.close();
216
250
  }
217
- return sessionMap;
251
+ return sessions;
218
252
  }
219
253
  /**
220
254
  * Attach to session in current terminal
@@ -11,12 +11,4 @@ export default class SessionList extends PMOCommand {
11
11
  promptIfMultiple: boolean;
12
12
  };
13
13
  execute(): Promise<void>;
14
- /**
15
- * Get list of host tmux session names
16
- */
17
- private getHostTmuxSessionNames;
18
- /**
19
- * Get map of containerId -> tmux session names
20
- */
21
- private getContainerTmuxSessionMap;
22
14
  }
@@ -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
  }
@@ -11,6 +11,7 @@ export default class SpecCreate extends PMOCommand {
11
11
  type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  problem: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
13
  interactive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
16
  project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  };
@@ -2,7 +2,7 @@ import { Flags, Args } from '@oclif/core';
2
2
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
3
  import { styles } from '../../lib/styles.js';
4
4
  import { slugify } from '../../lib/pmo/utils.js';
5
- import { shouldOutputJson } from '../../lib/prompt-json.js';
5
+ import { shouldOutputJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
6
6
  import { FlagResolver } from '../../lib/flags/index.js';
7
7
  export default class SpecCreate extends PMOCommand {
8
8
  static description = 'Create a new spec';
@@ -10,6 +10,7 @@ export default class SpecCreate extends PMOCommand {
10
10
  '<%= config.bin %> <%= command.id %> "User Authentication"',
11
11
  '<%= config.bin %> <%= command.id %> --title "API Design" --type product',
12
12
  '<%= config.bin %> <%= command.id %> -i # Interactive mode',
13
+ '<%= config.bin %> <%= command.id %> --title "Test" --dry-run --json # Validate without creating',
13
14
  ];
14
15
  static args = {
15
16
  title: Args.string({
@@ -21,7 +22,7 @@ export default class SpecCreate extends PMOCommand {
21
22
  ...pmoBaseFlags,
22
23
  title: Flags.string({
23
24
  char: 't',
24
- description: 'Spec title',
25
+ description: 'Spec title [required for non-interactive]',
25
26
  }),
26
27
  status: Flags.string({
27
28
  char: 's',
@@ -41,6 +42,10 @@ export default class SpecCreate extends PMOCommand {
41
42
  description: 'Interactive mode',
42
43
  default: false,
43
44
  }),
45
+ 'dry-run': Flags.boolean({
46
+ description: 'Validate inputs without creating spec (use with --json for structured output)',
47
+ default: false,
48
+ }),
44
49
  };
45
50
  async execute() {
46
51
  const { args, flags } = await this.parse(SpecCreate);
@@ -98,7 +103,7 @@ export default class SpecCreate extends PMOCommand {
98
103
  });
99
104
  resolver.addPrompt({
100
105
  flagName: 'problem',
101
- type: 'input',
106
+ type: 'multiline',
102
107
  message: 'Problem statement (optional):',
103
108
  when: (ctx) => ctx.flags.title !== undefined,
104
109
  });
@@ -108,6 +113,42 @@ export default class SpecCreate extends PMOCommand {
108
113
  const specType = resolved.type === '' ? undefined : resolved.type;
109
114
  // Generate ID from title
110
115
  const specId = slugify(resolved.title);
116
+ // Check if spec already exists
117
+ const existing = await this.storage.getSpec(specId);
118
+ if (existing) {
119
+ if (flags['dry-run']) {
120
+ if (jsonMode) {
121
+ outputDryRunErrorsAsJson([{ field: 'id', error: `Spec "${specId}" already exists` }], createMetadata('spec create', flags));
122
+ }
123
+ }
124
+ this.error(`Spec "${specId}" already exists.`);
125
+ }
126
+ // Handle dry-run: show what would be created without actually creating
127
+ if (flags['dry-run']) {
128
+ const wouldCreate = {
129
+ id: specId,
130
+ title: resolved.title,
131
+ status: resolved.status || 'draft',
132
+ ...(specType && { type: specType }),
133
+ ...(resolved.problem && { problem: resolved.problem }),
134
+ };
135
+ if (jsonMode) {
136
+ outputDryRunSuccessAsJson('spec', wouldCreate, createMetadata('spec create', flags));
137
+ }
138
+ // Human-readable dry-run output
139
+ this.log(styles.warning('\n[DRY RUN] Would create spec:'));
140
+ this.log(styles.muted(` ID: ${specId}`));
141
+ this.log(styles.muted(` Title: ${resolved.title}`));
142
+ this.log(styles.muted(` Status: ${resolved.status || 'draft'}`));
143
+ if (specType) {
144
+ this.log(styles.muted(` Type: ${specType}`));
145
+ }
146
+ if (resolved.problem) {
147
+ this.log(styles.muted(` Problem: ${resolved.problem}`));
148
+ }
149
+ this.log(styles.muted('\n(No spec was created)'));
150
+ return;
151
+ }
111
152
  // Create spec in database
112
153
  const spec = await this.storage.createSpec({
113
154
  id: specId,