@proletariat/cli 0.1.4 → 0.3.0

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 (526) hide show
  1. package/README.md +510 -255
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +23 -0
  6. package/dist/commands/action/create.d.ts +21 -0
  7. package/dist/commands/action/create.js +126 -0
  8. package/dist/commands/action/delete.d.ts +17 -0
  9. package/dist/commands/action/delete.js +78 -0
  10. package/dist/commands/action/index.d.ts +15 -0
  11. package/dist/commands/action/index.js +107 -0
  12. package/dist/commands/action/list.d.ts +14 -0
  13. package/dist/commands/action/list.js +89 -0
  14. package/dist/commands/action/run.d.ts +19 -0
  15. package/dist/commands/action/run.js +179 -0
  16. package/dist/commands/action/show.d.ts +15 -0
  17. package/dist/commands/action/show.js +47 -0
  18. package/dist/commands/action/update.d.ts +22 -0
  19. package/dist/commands/action/update.js +168 -0
  20. package/dist/commands/agent/index.d.ts +13 -0
  21. package/dist/commands/agent/index.js +131 -0
  22. package/dist/commands/agent/list.d.ts +7 -0
  23. package/dist/commands/agent/list.js +126 -0
  24. package/dist/commands/agent/login.d.ts +16 -0
  25. package/dist/commands/agent/login.js +146 -0
  26. package/dist/commands/agent/rebuild.d.ts +18 -0
  27. package/dist/commands/agent/rebuild.js +133 -0
  28. package/dist/commands/agent/restart.d.ts +17 -0
  29. package/dist/commands/agent/restart.js +116 -0
  30. package/dist/commands/agent/shell.d.ts +23 -0
  31. package/dist/commands/agent/shell.js +378 -0
  32. package/dist/commands/agent/staff/add.d.ts +15 -0
  33. package/dist/commands/agent/staff/add.js +281 -0
  34. package/dist/commands/agent/staff/index.d.ts +14 -0
  35. package/dist/commands/agent/staff/index.js +90 -0
  36. package/dist/commands/agent/staff/list.d.ts +7 -0
  37. package/dist/commands/agent/staff/list.js +90 -0
  38. package/dist/commands/agent/staff/remove.d.ts +16 -0
  39. package/dist/commands/agent/staff/remove.js +137 -0
  40. package/dist/commands/agent/status.d.ts +17 -0
  41. package/dist/commands/agent/status.js +139 -0
  42. package/dist/commands/agent/temp/cleanup.d.ts +23 -0
  43. package/dist/commands/agent/temp/cleanup.js +388 -0
  44. package/dist/commands/agent/temp/index.d.ts +14 -0
  45. package/dist/commands/agent/temp/index.js +82 -0
  46. package/dist/commands/agent/temp/list.d.ts +7 -0
  47. package/dist/commands/agent/temp/list.js +108 -0
  48. package/dist/commands/agent/themes/add-names.d.ts +10 -0
  49. package/dist/commands/agent/themes/add-names.js +67 -0
  50. package/dist/commands/agent/themes/create.d.ts +13 -0
  51. package/dist/commands/agent/themes/create.js +66 -0
  52. package/dist/commands/agent/themes/index.d.ts +9 -0
  53. package/dist/commands/agent/themes/index.js +194 -0
  54. package/dist/commands/agent/themes/list.d.ts +6 -0
  55. package/dist/commands/agent/themes/list.js +41 -0
  56. package/dist/commands/agent/themes/set.d.ts +12 -0
  57. package/dist/commands/agent/themes/set.js +77 -0
  58. package/dist/commands/agent/visit.d.ts +16 -0
  59. package/dist/commands/agent/visit.js +88 -0
  60. package/dist/commands/autocomplete/setup.d.ts +14 -0
  61. package/dist/commands/autocomplete/setup.js +154 -0
  62. package/dist/commands/board/index.d.ts +17 -0
  63. package/dist/commands/board/index.js +255 -0
  64. package/dist/commands/board/watch.d.ts +13 -0
  65. package/dist/commands/board/watch.js +52 -0
  66. package/dist/commands/branch/create.d.ts +50 -0
  67. package/dist/commands/branch/create.js +624 -0
  68. package/dist/commands/branch/index.d.ts +13 -0
  69. package/dist/commands/branch/index.js +50 -0
  70. package/dist/commands/branch/list.d.ts +17 -0
  71. package/dist/commands/branch/list.js +120 -0
  72. package/dist/commands/branch/validate.d.ts +15 -0
  73. package/dist/commands/branch/validate.js +73 -0
  74. package/dist/commands/commit.d.ts +71 -0
  75. package/dist/commands/commit.js +499 -0
  76. package/dist/commands/docker/clean.d.ts +13 -0
  77. package/dist/commands/docker/clean.js +224 -0
  78. package/dist/commands/docker/index.d.ts +19 -0
  79. package/dist/commands/docker/index.js +274 -0
  80. package/dist/commands/docker/list.d.ts +16 -0
  81. package/dist/commands/docker/list.js +200 -0
  82. package/dist/commands/docker/logs.d.ts +14 -0
  83. package/dist/commands/docker/logs.js +118 -0
  84. package/dist/commands/docker/prune.d.ts +14 -0
  85. package/dist/commands/docker/prune.js +211 -0
  86. package/dist/commands/docker/restart.d.ts +14 -0
  87. package/dist/commands/docker/restart.js +129 -0
  88. package/dist/commands/docker/shell.d.ts +14 -0
  89. package/dist/commands/docker/shell.js +103 -0
  90. package/dist/commands/docker/start.d.ts +12 -0
  91. package/dist/commands/docker/start.js +92 -0
  92. package/dist/commands/docker/status.d.ts +7 -0
  93. package/dist/commands/docker/status.js +40 -0
  94. package/dist/commands/docker/stop.d.ts +14 -0
  95. package/dist/commands/docker/stop.js +134 -0
  96. package/dist/commands/docker/sync.d.ts +15 -0
  97. package/dist/commands/docker/sync.js +112 -0
  98. package/dist/commands/epic/activate.d.ts +13 -0
  99. package/dist/commands/epic/activate.js +118 -0
  100. package/dist/commands/epic/archive.d.ts +14 -0
  101. package/dist/commands/epic/archive.js +132 -0
  102. package/dist/commands/epic/create.d.ts +15 -0
  103. package/dist/commands/epic/create.js +137 -0
  104. package/dist/commands/epic/index.d.ts +13 -0
  105. package/dist/commands/epic/index.js +88 -0
  106. package/dist/commands/epic/link/block.d.ts +14 -0
  107. package/dist/commands/epic/link/block.js +79 -0
  108. package/dist/commands/epic/link/duplicates.d.ts +14 -0
  109. package/dist/commands/epic/link/duplicates.js +66 -0
  110. package/dist/commands/epic/link/index.d.ts +19 -0
  111. package/dist/commands/epic/link/index.js +242 -0
  112. package/dist/commands/epic/link/relates.d.ts +14 -0
  113. package/dist/commands/epic/link/relates.js +66 -0
  114. package/dist/commands/epic/link/remove.d.ts +16 -0
  115. package/dist/commands/epic/link/remove.js +89 -0
  116. package/dist/commands/epic/list.d.ts +11 -0
  117. package/dist/commands/epic/list.js +87 -0
  118. package/dist/commands/epic/move.d.ts +15 -0
  119. package/dist/commands/epic/move.js +184 -0
  120. package/dist/commands/epic/progress.d.ts +16 -0
  121. package/dist/commands/epic/progress.js +166 -0
  122. package/dist/commands/epic/project.d.ts +15 -0
  123. package/dist/commands/epic/project.js +219 -0
  124. package/dist/commands/epic/reorder.d.ts +21 -0
  125. package/dist/commands/epic/reorder.js +160 -0
  126. package/dist/commands/epic/spec.d.ts +15 -0
  127. package/dist/commands/epic/spec.js +191 -0
  128. package/dist/commands/epic/ticket.d.ts +18 -0
  129. package/dist/commands/epic/ticket.js +291 -0
  130. package/dist/commands/epic/view.d.ts +13 -0
  131. package/dist/commands/epic/view.js +117 -0
  132. package/dist/commands/execution/index.d.ts +13 -0
  133. package/dist/commands/execution/index.js +70 -0
  134. package/dist/commands/execution/list.d.ts +15 -0
  135. package/dist/commands/execution/list.js +144 -0
  136. package/dist/commands/execution/logs.d.ts +18 -0
  137. package/dist/commands/execution/logs.js +161 -0
  138. package/dist/commands/execution/stop.d.ts +22 -0
  139. package/dist/commands/execution/stop.js +248 -0
  140. package/dist/commands/gh/index.d.ts +9 -0
  141. package/dist/commands/gh/index.js +53 -0
  142. package/dist/commands/gh/login.d.ts +6 -0
  143. package/dist/commands/gh/login.js +57 -0
  144. package/dist/commands/gh/status.d.ts +6 -0
  145. package/dist/commands/gh/status.js +48 -0
  146. package/dist/commands/gh/token.d.ts +6 -0
  147. package/dist/commands/gh/token.js +59 -0
  148. package/dist/commands/init.d.ts +26 -0
  149. package/dist/commands/init.js +200 -0
  150. package/dist/commands/phase/create.d.ts +22 -0
  151. package/dist/commands/phase/create.js +123 -0
  152. package/dist/commands/phase/delete.d.ts +17 -0
  153. package/dist/commands/phase/delete.js +73 -0
  154. package/dist/commands/phase/list.d.ts +12 -0
  155. package/dist/commands/phase/list.js +76 -0
  156. package/dist/commands/phase/move.d.ts +17 -0
  157. package/dist/commands/phase/move.js +115 -0
  158. package/dist/commands/phase/template/apply.d.ts +17 -0
  159. package/dist/commands/phase/template/apply.js +106 -0
  160. package/dist/commands/phase/template/create.d.ts +16 -0
  161. package/dist/commands/phase/template/create.js +58 -0
  162. package/dist/commands/phase/template/delete.d.ts +17 -0
  163. package/dist/commands/phase/template/delete.js +98 -0
  164. package/dist/commands/phase/template/index.d.ts +15 -0
  165. package/dist/commands/phase/template/index.js +128 -0
  166. package/dist/commands/phase/template/list.d.ts +16 -0
  167. package/dist/commands/phase/template/list.js +95 -0
  168. package/dist/commands/phase/template/update.d.ts +17 -0
  169. package/dist/commands/phase/template/update.js +89 -0
  170. package/dist/commands/phase/update.d.ts +23 -0
  171. package/dist/commands/phase/update.js +174 -0
  172. package/dist/commands/pmo/init.d.ts +25 -0
  173. package/dist/commands/pmo/init.js +341 -0
  174. package/dist/commands/pr/create.d.ts +17 -0
  175. package/dist/commands/pr/create.js +242 -0
  176. package/dist/commands/pr/index.d.ts +9 -0
  177. package/dist/commands/pr/index.js +68 -0
  178. package/dist/commands/pr/link.d.ts +14 -0
  179. package/dist/commands/pr/link.js +212 -0
  180. package/dist/commands/pr/status.d.ts +12 -0
  181. package/dist/commands/pr/status.js +161 -0
  182. package/dist/commands/project/archive.d.ts +17 -0
  183. package/dist/commands/project/archive.js +83 -0
  184. package/dist/commands/project/create.d.ts +22 -0
  185. package/dist/commands/project/create.js +143 -0
  186. package/dist/commands/project/delete.d.ts +17 -0
  187. package/dist/commands/project/delete.js +128 -0
  188. package/dist/commands/project/index.d.ts +13 -0
  189. package/dist/commands/project/index.js +64 -0
  190. package/dist/commands/project/list.d.ts +14 -0
  191. package/dist/commands/project/list.js +96 -0
  192. package/dist/commands/project/spec.d.ts +18 -0
  193. package/dist/commands/project/spec.js +216 -0
  194. package/dist/commands/project/unarchive.d.ts +15 -0
  195. package/dist/commands/project/unarchive.js +35 -0
  196. package/dist/commands/project/view.d.ts +16 -0
  197. package/dist/commands/project/view.js +94 -0
  198. package/dist/commands/repo/add.d.ts +21 -0
  199. package/dist/commands/repo/add.js +118 -0
  200. package/dist/commands/repo/index.d.ts +13 -0
  201. package/dist/commands/repo/index.js +114 -0
  202. package/dist/commands/repo/list.d.ts +13 -0
  203. package/dist/commands/repo/list.js +96 -0
  204. package/dist/commands/repo/remove.d.ts +23 -0
  205. package/dist/commands/repo/remove.js +217 -0
  206. package/dist/commands/repo/view.d.ts +15 -0
  207. package/dist/commands/repo/view.js +99 -0
  208. package/dist/commands/session/attach.d.ts +40 -0
  209. package/dist/commands/session/attach.js +307 -0
  210. package/dist/commands/session/index.d.ts +13 -0
  211. package/dist/commands/session/index.js +64 -0
  212. package/dist/commands/session/list.d.ts +21 -0
  213. package/dist/commands/session/list.js +181 -0
  214. package/dist/commands/spec/create.d.ts +19 -0
  215. package/dist/commands/spec/create.js +130 -0
  216. package/dist/commands/spec/index.d.ts +13 -0
  217. package/dist/commands/spec/index.js +68 -0
  218. package/dist/commands/spec/link/depends.d.ts +14 -0
  219. package/dist/commands/spec/link/depends.js +64 -0
  220. package/dist/commands/spec/link/duplicates.d.ts +14 -0
  221. package/dist/commands/spec/link/duplicates.js +63 -0
  222. package/dist/commands/spec/link/index.d.ts +19 -0
  223. package/dist/commands/spec/link/index.js +200 -0
  224. package/dist/commands/spec/link/relates.d.ts +14 -0
  225. package/dist/commands/spec/link/relates.js +63 -0
  226. package/dist/commands/spec/link/remove.d.ts +16 -0
  227. package/dist/commands/spec/link/remove.js +94 -0
  228. package/dist/commands/spec/list.d.ts +12 -0
  229. package/dist/commands/spec/list.js +75 -0
  230. package/dist/commands/spec/plan.d.ts +15 -0
  231. package/dist/commands/spec/plan.js +108 -0
  232. package/dist/commands/spec/ticket.d.ts +18 -0
  233. package/dist/commands/spec/ticket.js +160 -0
  234. package/dist/commands/spec/view.d.ts +15 -0
  235. package/dist/commands/spec/view.js +163 -0
  236. package/dist/commands/status/create.d.ts +21 -0
  237. package/dist/commands/status/create.js +140 -0
  238. package/dist/commands/status/delete.d.ts +13 -0
  239. package/dist/commands/status/delete.js +77 -0
  240. package/dist/commands/status/index.d.ts +14 -0
  241. package/dist/commands/status/index.js +91 -0
  242. package/dist/commands/status/list.d.ts +12 -0
  243. package/dist/commands/status/list.js +93 -0
  244. package/dist/commands/status/move.d.ts +14 -0
  245. package/dist/commands/status/move.js +120 -0
  246. package/dist/commands/status/update.d.ts +20 -0
  247. package/dist/commands/status/update.js +180 -0
  248. package/dist/commands/template/delete.d.ts +15 -0
  249. package/dist/commands/template/delete.js +142 -0
  250. package/dist/commands/template/index.d.ts +10 -0
  251. package/dist/commands/template/index.js +64 -0
  252. package/dist/commands/template/list.d.ts +18 -0
  253. package/dist/commands/template/list.js +157 -0
  254. package/dist/commands/template/phase/apply.d.ts +14 -0
  255. package/dist/commands/template/phase/apply.js +41 -0
  256. package/dist/commands/template/phase/create.d.ts +12 -0
  257. package/dist/commands/template/phase/create.js +29 -0
  258. package/dist/commands/template/phase/delete.d.ts +13 -0
  259. package/dist/commands/template/phase/delete.js +34 -0
  260. package/dist/commands/template/phase/index.d.ts +10 -0
  261. package/dist/commands/template/phase/index.js +62 -0
  262. package/dist/commands/template/phase/list.d.ts +11 -0
  263. package/dist/commands/template/phase/list.js +34 -0
  264. package/dist/commands/template/phase/update.d.ts +13 -0
  265. package/dist/commands/template/phase/update.js +35 -0
  266. package/dist/commands/template/ticket/apply.d.ts +17 -0
  267. package/dist/commands/template/ticket/apply.js +58 -0
  268. package/dist/commands/template/ticket/delete.d.ts +13 -0
  269. package/dist/commands/template/ticket/delete.js +34 -0
  270. package/dist/commands/template/ticket/index.d.ts +10 -0
  271. package/dist/commands/template/ticket/index.js +62 -0
  272. package/dist/commands/template/ticket/list.d.ts +11 -0
  273. package/dist/commands/template/ticket/list.js +34 -0
  274. package/dist/commands/template/ticket/save.d.ts +13 -0
  275. package/dist/commands/template/ticket/save.js +35 -0
  276. package/dist/commands/ticket/bulk.d.ts +13 -0
  277. package/dist/commands/ticket/bulk.js +145 -0
  278. package/dist/commands/ticket/complete.d.ts +16 -0
  279. package/dist/commands/ticket/complete.js +170 -0
  280. package/dist/commands/ticket/create.d.ts +22 -0
  281. package/dist/commands/ticket/create.js +390 -0
  282. package/dist/commands/ticket/delete.d.ts +16 -0
  283. package/dist/commands/ticket/delete.js +178 -0
  284. package/dist/commands/ticket/edit.d.ts +27 -0
  285. package/dist/commands/ticket/edit.js +322 -0
  286. package/dist/commands/ticket/epic.d.ts +20 -0
  287. package/dist/commands/ticket/epic.js +333 -0
  288. package/dist/commands/ticket/index.d.ts +13 -0
  289. package/dist/commands/ticket/index.js +103 -0
  290. package/dist/commands/ticket/link/block.d.ts +14 -0
  291. package/dist/commands/ticket/link/block.js +94 -0
  292. package/dist/commands/ticket/link/duplicates.d.ts +14 -0
  293. package/dist/commands/ticket/link/duplicates.js +93 -0
  294. package/dist/commands/ticket/link/index.d.ts +19 -0
  295. package/dist/commands/ticket/link/index.js +239 -0
  296. package/dist/commands/ticket/link/relates.d.ts +14 -0
  297. package/dist/commands/ticket/link/relates.js +93 -0
  298. package/dist/commands/ticket/link/remove.d.ts +16 -0
  299. package/dist/commands/ticket/link/remove.js +128 -0
  300. package/dist/commands/ticket/list.d.ts +24 -0
  301. package/dist/commands/ticket/list.js +431 -0
  302. package/dist/commands/ticket/move.d.ts +18 -0
  303. package/dist/commands/ticket/move.js +212 -0
  304. package/dist/commands/ticket/project.d.ts +18 -0
  305. package/dist/commands/ticket/project.js +254 -0
  306. package/dist/commands/ticket/reassign.d.ts +19 -0
  307. package/dist/commands/ticket/reassign.js +279 -0
  308. package/dist/commands/ticket/spec.d.ts +18 -0
  309. package/dist/commands/ticket/spec.js +259 -0
  310. package/dist/commands/ticket/status.d.ts +13 -0
  311. package/dist/commands/ticket/status.js +87 -0
  312. package/dist/commands/ticket/template/apply.d.ts +25 -0
  313. package/dist/commands/ticket/template/apply.js +249 -0
  314. package/dist/commands/ticket/template/create.d.ts +19 -0
  315. package/dist/commands/ticket/template/create.js +210 -0
  316. package/dist/commands/ticket/template/delete.d.ts +17 -0
  317. package/dist/commands/ticket/template/delete.js +92 -0
  318. package/dist/commands/ticket/template/index.d.ts +15 -0
  319. package/dist/commands/ticket/template/index.js +118 -0
  320. package/dist/commands/ticket/template/list.d.ts +16 -0
  321. package/dist/commands/ticket/template/list.js +110 -0
  322. package/dist/commands/ticket/template/save.d.ts +14 -0
  323. package/dist/commands/ticket/template/save.js +110 -0
  324. package/dist/commands/ticket/update.d.ts +18 -0
  325. package/dist/commands/ticket/update.js +325 -0
  326. package/dist/commands/ticket/view.d.ts +13 -0
  327. package/dist/commands/ticket/view.js +80 -0
  328. package/dist/commands/whoami.d.ts +9 -0
  329. package/dist/commands/whoami.js +103 -0
  330. package/dist/commands/work/complete.d.ts +13 -0
  331. package/dist/commands/work/complete.js +121 -0
  332. package/dist/commands/work/index.d.ts +13 -0
  333. package/dist/commands/work/index.js +70 -0
  334. package/dist/commands/work/ready.d.ts +24 -0
  335. package/dist/commands/work/ready.js +290 -0
  336. package/dist/commands/work/revise.d.ts +19 -0
  337. package/dist/commands/work/revise.js +377 -0
  338. package/dist/commands/work/spawn-all.d.ts +17 -0
  339. package/dist/commands/work/spawn-all.js +58 -0
  340. package/dist/commands/work/spawn.d.ts +29 -0
  341. package/dist/commands/work/spawn.js +728 -0
  342. package/dist/commands/work/start.d.ts +39 -0
  343. package/dist/commands/work/start.js +1393 -0
  344. package/dist/commands/work/watch.d.ts +31 -0
  345. package/dist/commands/work/watch.js +359 -0
  346. package/dist/commands/workflow/create.d.ts +18 -0
  347. package/dist/commands/workflow/create.js +119 -0
  348. package/dist/commands/workflow/delete.d.ts +17 -0
  349. package/dist/commands/workflow/delete.js +119 -0
  350. package/dist/commands/workflow/index.d.ts +15 -0
  351. package/dist/commands/workflow/index.js +75 -0
  352. package/dist/commands/workflow/list.d.ts +15 -0
  353. package/dist/commands/workflow/list.js +75 -0
  354. package/dist/commands/workflow/switch.d.ts +13 -0
  355. package/dist/commands/workflow/switch.js +117 -0
  356. package/dist/commands/workflow/view.d.ts +16 -0
  357. package/dist/commands/workflow/view.js +114 -0
  358. package/dist/commands/workspace/add.d.ts +12 -0
  359. package/dist/commands/workspace/add.js +74 -0
  360. package/dist/commands/workspace/list.d.ts +9 -0
  361. package/dist/commands/workspace/list.js +153 -0
  362. package/dist/commands/workspace/remove.d.ts +13 -0
  363. package/dist/commands/workspace/remove.js +98 -0
  364. package/dist/commands/workspace/use.d.ts +12 -0
  365. package/dist/commands/workspace/use.js +111 -0
  366. package/dist/hooks/init.d.ts +11 -0
  367. package/dist/hooks/init.js +57 -0
  368. package/dist/index.d.ts +1 -0
  369. package/dist/index.js +1 -0
  370. package/dist/lib/agents/commands.d.ts +189 -0
  371. package/dist/lib/agents/commands.js +893 -0
  372. package/dist/lib/agents/index.d.ts +54 -0
  373. package/dist/lib/agents/index.js +382 -0
  374. package/dist/lib/branch/index.d.ts +120 -0
  375. package/dist/lib/branch/index.js +334 -0
  376. package/dist/lib/colors.d.ts +94 -0
  377. package/dist/lib/colors.js +68 -0
  378. package/dist/lib/commands/docker-command.d.ts +21 -0
  379. package/dist/lib/commands/docker-command.js +27 -0
  380. package/dist/lib/database/index.d.ts +176 -0
  381. package/dist/lib/database/index.js +581 -0
  382. package/dist/lib/docker/resolve.d.ts +38 -0
  383. package/dist/lib/docker/resolve.js +175 -0
  384. package/dist/lib/execution/config.d.ts +150 -0
  385. package/dist/lib/execution/config.js +541 -0
  386. package/dist/lib/execution/devcontainer.d.ts +85 -0
  387. package/dist/lib/execution/devcontainer.js +594 -0
  388. package/dist/lib/execution/index.d.ts +10 -0
  389. package/dist/lib/execution/index.js +10 -0
  390. package/dist/lib/execution/runners.d.ts +53 -0
  391. package/dist/lib/execution/runners.js +1182 -0
  392. package/dist/lib/execution/spawner.d.ts +85 -0
  393. package/dist/lib/execution/spawner.js +548 -0
  394. package/dist/lib/execution/storage.d.ts +159 -0
  395. package/dist/lib/execution/storage.js +425 -0
  396. package/dist/lib/execution/types.d.ts +145 -0
  397. package/dist/lib/execution/types.js +157 -0
  398. package/dist/lib/init/index.d.ts +75 -0
  399. package/dist/lib/init/index.js +355 -0
  400. package/dist/lib/machine-config.d.ts +170 -0
  401. package/dist/lib/machine-config.js +386 -0
  402. package/dist/lib/pmo/base-command.d.ts +195 -0
  403. package/dist/lib/pmo/base-command.js +319 -0
  404. package/dist/lib/pmo/create-spec-folders.d.ts +43 -0
  405. package/dist/lib/pmo/create-spec-folders.js +64 -0
  406. package/dist/lib/pmo/epic-files.d.ts +56 -0
  407. package/dist/lib/pmo/epic-files.js +195 -0
  408. package/dist/lib/pmo/find-pmo.d.ts +14 -0
  409. package/dist/lib/pmo/find-pmo.js +172 -0
  410. package/dist/lib/pmo/index.d.ts +109 -0
  411. package/dist/lib/pmo/index.js +501 -0
  412. package/dist/lib/pmo/markdown.d.ts +31 -0
  413. package/dist/lib/pmo/markdown.js +245 -0
  414. package/dist/lib/pmo/pmo-context.d.ts +27 -0
  415. package/dist/lib/pmo/pmo-context.js +44 -0
  416. package/dist/lib/pmo/schema.d.ts +82 -0
  417. package/dist/lib/pmo/schema.js +531 -0
  418. package/dist/lib/pmo/spec-parser.d.ts +25 -0
  419. package/dist/lib/pmo/spec-parser.js +205 -0
  420. package/dist/lib/pmo/spec-types.d.ts +43 -0
  421. package/dist/lib/pmo/spec-types.js +7 -0
  422. package/dist/lib/pmo/storage/actions.d.ts +34 -0
  423. package/dist/lib/pmo/storage/actions.js +177 -0
  424. package/dist/lib/pmo/storage/base.d.ts +47 -0
  425. package/dist/lib/pmo/storage/base.js +858 -0
  426. package/dist/lib/pmo/storage/dependencies.d.ts +61 -0
  427. package/dist/lib/pmo/storage/dependencies.js +267 -0
  428. package/dist/lib/pmo/storage/epics.d.ts +46 -0
  429. package/dist/lib/pmo/storage/epics.js +243 -0
  430. package/dist/lib/pmo/storage/helpers.d.ts +33 -0
  431. package/dist/lib/pmo/storage/helpers.js +148 -0
  432. package/dist/lib/pmo/storage/index.d.ts +186 -0
  433. package/dist/lib/pmo/storage/index.js +689 -0
  434. package/dist/lib/pmo/storage/phases.d.ts +65 -0
  435. package/dist/lib/pmo/storage/phases.js +392 -0
  436. package/dist/lib/pmo/storage/projects.d.ts +79 -0
  437. package/dist/lib/pmo/storage/projects.js +303 -0
  438. package/dist/lib/pmo/storage/specs.d.ts +77 -0
  439. package/dist/lib/pmo/storage/specs.js +389 -0
  440. package/dist/lib/pmo/storage/statuses.d.ts +63 -0
  441. package/dist/lib/pmo/storage/statuses.js +404 -0
  442. package/dist/lib/pmo/storage/subtasks.d.ts +37 -0
  443. package/dist/lib/pmo/storage/subtasks.js +184 -0
  444. package/dist/lib/pmo/storage/templates.d.ts +40 -0
  445. package/dist/lib/pmo/storage/templates.js +210 -0
  446. package/dist/lib/pmo/storage/tickets.d.ts +57 -0
  447. package/dist/lib/pmo/storage/tickets.js +453 -0
  448. package/dist/lib/pmo/storage/types.d.ts +200 -0
  449. package/dist/lib/pmo/storage/types.js +5 -0
  450. package/dist/lib/pmo/storage/views.d.ts +44 -0
  451. package/dist/lib/pmo/storage/views.js +355 -0
  452. package/dist/lib/pmo/storage-sqlite.d.ts +7 -0
  453. package/dist/lib/pmo/storage-sqlite.js +7 -0
  454. package/dist/lib/pmo/sync-manager.d.ts +92 -0
  455. package/dist/lib/pmo/sync-manager.js +229 -0
  456. package/dist/lib/pmo/types.d.ts +710 -0
  457. package/dist/lib/pmo/types.js +108 -0
  458. package/dist/lib/pmo/utils.d.ts +122 -0
  459. package/dist/lib/pmo/utils.js +174 -0
  460. package/dist/lib/pmo/watcher.d.ts +43 -0
  461. package/dist/lib/pmo/watcher.js +208 -0
  462. package/dist/lib/pr/index.d.ts +150 -0
  463. package/dist/lib/pr/index.js +483 -0
  464. package/dist/lib/prompt-json.d.ts +231 -0
  465. package/dist/lib/prompt-json.js +213 -0
  466. package/dist/lib/repos/index.d.ts +81 -0
  467. package/dist/lib/repos/index.js +679 -0
  468. package/dist/lib/styles.d.ts +98 -0
  469. package/dist/lib/styles.js +195 -0
  470. package/dist/lib/themes.d.ts +128 -0
  471. package/dist/lib/themes.js +301 -0
  472. package/dist/lib/ui/BoardUI.d.ts +21 -0
  473. package/dist/lib/ui/BoardUI.js +85 -0
  474. package/dist/lib/ui/ClaimTicketUI.d.ts +17 -0
  475. package/dist/lib/ui/ClaimTicketUI.js +64 -0
  476. package/dist/lib/ui/CreateTicketUI.d.ts +13 -0
  477. package/dist/lib/ui/CreateTicketUI.js +101 -0
  478. package/dist/lib/workspace.d.ts +66 -0
  479. package/dist/lib/workspace.js +204 -0
  480. package/oclif.manifest.json +10593 -0
  481. package/package.json +104 -52
  482. package/LICENSE +0 -21
  483. package/dist/bin/prlt.d.ts +0 -11
  484. package/dist/bin/prlt.d.ts.map +0 -1
  485. package/dist/bin/prlt.js +0 -144
  486. package/dist/bin/prlt.js.map +0 -1
  487. package/dist/lib/config/index.d.ts +0 -14
  488. package/dist/lib/config/index.d.ts.map +0 -1
  489. package/dist/lib/config/index.js +0 -139
  490. package/dist/lib/config/index.js.map +0 -1
  491. package/dist/lib/config/upgrade.d.ts +0 -2
  492. package/dist/lib/config/upgrade.d.ts.map +0 -1
  493. package/dist/lib/config/upgrade.js +0 -173
  494. package/dist/lib/config/upgrade.js.map +0 -1
  495. package/dist/lib/themes/index.d.ts +0 -8
  496. package/dist/lib/themes/index.d.ts.map +0 -1
  497. package/dist/lib/themes/index.js +0 -80
  498. package/dist/lib/themes/index.js.map +0 -1
  499. package/dist/lib/utils/helpers.d.ts +0 -4
  500. package/dist/lib/utils/helpers.d.ts.map +0 -1
  501. package/dist/lib/utils/helpers.js +0 -39
  502. package/dist/lib/utils/helpers.js.map +0 -1
  503. package/dist/lib/utils/logger.d.ts +0 -4
  504. package/dist/lib/utils/logger.d.ts.map +0 -1
  505. package/dist/lib/utils/logger.js +0 -28
  506. package/dist/lib/utils/logger.js.map +0 -1
  507. package/dist/lib/workspace/index.d.ts +0 -13
  508. package/dist/lib/workspace/index.d.ts.map +0 -1
  509. package/dist/lib/workspace/index.js +0 -116
  510. package/dist/lib/workspace/index.js.map +0 -1
  511. package/dist/lib/worktree/index.d.ts +0 -7
  512. package/dist/lib/worktree/index.d.ts.map +0 -1
  513. package/dist/lib/worktree/index.js +0 -362
  514. package/dist/lib/worktree/index.js.map +0 -1
  515. package/dist/lib/worktree/migrate.d.ts +0 -2
  516. package/dist/lib/worktree/migrate.d.ts.map +0 -1
  517. package/dist/lib/worktree/migrate.js +0 -212
  518. package/dist/lib/worktree/migrate.js.map +0 -1
  519. package/dist/lib/worktree/repair.d.ts +0 -3
  520. package/dist/lib/worktree/repair.d.ts.map +0 -1
  521. package/dist/lib/worktree/repair.js +0 -140
  522. package/dist/lib/worktree/repair.js.map +0 -1
  523. package/dist/types/index.d.ts +0 -57
  524. package/dist/types/index.d.ts.map +0 -1
  525. package/dist/types/index.js +0 -3
  526. package/dist/types/index.js.map +0 -1
@@ -0,0 +1,1393 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import inquirer from 'inquirer';
6
+ import Database from 'better-sqlite3';
7
+ import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
8
+ import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
9
+ import { getWorkColumnSetting, findColumnByName } from '../../lib/pmo/utils.js';
10
+ import { styles } from '../../lib/styles.js';
11
+ import { getWorkspaceInfo, createEphemeralAgent, getTicketTmuxSession, killTmuxSession, } from '../../lib/agents/commands.js';
12
+ import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
13
+ import { runExecution, isDockerRunning } from '../../lib/execution/runners.js';
14
+ import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
15
+ import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName } from '../../lib/execution/config.js';
16
+ import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
17
+ import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
18
+ /**
19
+ * Try to execute a git command, return true if successful
20
+ */
21
+ function tryGitCommand(cmd, cwd) {
22
+ try {
23
+ execSync(cmd, { cwd, stdio: 'pipe' });
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Check if a directory is a git repository
32
+ */
33
+ function isGitRepo(dir) {
34
+ return tryGitCommand('git rev-parse --git-dir', dir);
35
+ }
36
+ /**
37
+ * Find the first existing branch from a list of candidates
38
+ */
39
+ function findBaseBranch(repoPath, candidates = ['origin/main', 'origin/master']) {
40
+ for (const branch of candidates) {
41
+ if (tryGitCommand(`git rev-parse --verify ${branch}`, repoPath)) {
42
+ return branch;
43
+ }
44
+ }
45
+ return 'HEAD';
46
+ }
47
+ /**
48
+ * Get active staff agents that exist on disk.
49
+ * Warns about any agents in DB that are missing their directory.
50
+ */
51
+ function getActiveStaffAgents(workspaceInfo, log) {
52
+ const result = [];
53
+ for (const agent of workspaceInfo.agents) {
54
+ if (agent.type !== 'persistent' || agent.status !== 'active')
55
+ continue;
56
+ const agentDir = agent.worktree_path
57
+ ? path.join(workspaceInfo.path, agent.worktree_path)
58
+ : path.join(workspaceInfo.path, 'agents', 'staff', agent.name);
59
+ if (fs.existsSync(agentDir)) {
60
+ result.push(agent);
61
+ }
62
+ else {
63
+ log(styles.warning(`⚠ Agent '${agent.name}' in database but directory missing - skipping`));
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+ export default class WorkStart extends PMOCommand {
69
+ static description = 'Start work on a ticket (launches an agent to implement it)';
70
+ static examples = [
71
+ '<%= config.bin %> <%= command.id %> TKT-001',
72
+ '<%= config.bin %> <%= command.id %> TKT-001 --mode foreground',
73
+ '<%= config.bin %> <%= command.id %> TKT-001 --mode tmux',
74
+ '<%= config.bin %> <%= command.id %> TKT-001 --mode terminal',
75
+ '<%= config.bin %> <%= command.id %> # Interactive mode',
76
+ '<%= config.bin %> <%= command.id %> --all # Spawn all backlog tickets',
77
+ ];
78
+ static args = {
79
+ ticketId: Args.string({
80
+ description: 'Ticket ID - prompts with dropdown if not provided',
81
+ required: false,
82
+ }),
83
+ };
84
+ static flags = {
85
+ ...pmoBaseFlags,
86
+ json: Flags.boolean({
87
+ description: 'Output prompt configuration as JSON (for AI agents/scripts)',
88
+ default: false,
89
+ }),
90
+ all: Flags.boolean({
91
+ char: 'a',
92
+ description: 'Start work on all unassigned backlog tickets (batch mode)',
93
+ default: false,
94
+ }),
95
+ mode: Flags.string({
96
+ char: 'm',
97
+ description: 'Runtime mode',
98
+ options: ['foreground', 'background', 'tmux', 'terminal', 'devcontainer', 'docker', 'vm'],
99
+ }),
100
+ executor: Flags.string({
101
+ char: 'e',
102
+ description: 'Override executor',
103
+ options: ['claude-code', 'codex', 'aider', 'custom'],
104
+ }),
105
+ action: Flags.string({
106
+ char: 'A',
107
+ description: 'Action to perform (e.g., implement, groom, review)',
108
+ }),
109
+ prompt: Flags.string({
110
+ char: 'p',
111
+ description: 'Custom prompt (overrides action)',
112
+ }),
113
+ watch: Flags.boolean({
114
+ char: 'w',
115
+ description: 'Stream output in real-time',
116
+ default: false,
117
+ }),
118
+ force: Flags.boolean({
119
+ char: 'f',
120
+ description: 'Start even if work already in progress',
121
+ default: false,
122
+ }),
123
+ 'vm-host': Flags.string({
124
+ description: 'VM host for vm mode',
125
+ }),
126
+ 'run-on-host': Flags.boolean({
127
+ description: 'Run on host even if devcontainer exists (bypasses sandbox)',
128
+ default: false,
129
+ }),
130
+ reconfigure: Flags.boolean({
131
+ description: 'Re-prompt for terminal app preference',
132
+ default: false,
133
+ }),
134
+ 'skip-permissions': Flags.boolean({
135
+ description: 'Skip permission prompts (danger mode)',
136
+ default: false,
137
+ }),
138
+ 'create-pr': Flags.boolean({
139
+ description: 'Create PR when work is ready',
140
+ default: false,
141
+ }),
142
+ 'no-pr': Flags.boolean({
143
+ description: 'Do not create PR when work is ready',
144
+ default: false,
145
+ }),
146
+ output: Flags.string({
147
+ char: 'o',
148
+ description: 'Output mode',
149
+ options: ['interactive', 'print'],
150
+ }),
151
+ display: Flags.string({
152
+ char: 'd',
153
+ description: 'Display mode for devcontainer (where to show output)',
154
+ options: ['terminal', 'background'],
155
+ }),
156
+ session: Flags.string({
157
+ char: 's',
158
+ description: 'Session manager inside container (tmux runs agent in tmux inside container)',
159
+ options: ['tmux', 'direct'],
160
+ default: 'tmux',
161
+ }),
162
+ agent: Flags.string({
163
+ description: 'Agent to assign (skips interactive selection)',
164
+ }),
165
+ ephemeral: Flags.boolean({
166
+ description: 'Create an ephemeral agent on-demand (auto-generates name)',
167
+ default: false,
168
+ }),
169
+ };
170
+ async execute() {
171
+ const { args, flags } = await this.parse(WorkStart);
172
+ const projectId = flags.project;
173
+ // Check if JSON output mode is active
174
+ const jsonMode = shouldOutputJson(flags);
175
+ // Helper to handle errors in JSON mode
176
+ const handleError = (code, message) => {
177
+ if (jsonMode) {
178
+ outputErrorAsJson(code, message, createMetadata('work start', flags));
179
+ this.exit(1);
180
+ }
181
+ this.error(message);
182
+ };
183
+ // Get workspace info (for agent worktree paths)
184
+ let workspaceInfo;
185
+ try {
186
+ workspaceInfo = getWorkspaceInfo();
187
+ }
188
+ catch {
189
+ return handleError('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt init" first.');
190
+ }
191
+ // Open database for execution storage
192
+ const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
193
+ const db = new Database(dbPath);
194
+ const executionStorage = new ExecutionStorage(db);
195
+ try {
196
+ // Handle batch mode (--all)
197
+ if (flags.all) {
198
+ await this.runBatchMode(workspaceInfo, db, executionStorage, flags);
199
+ return;
200
+ }
201
+ // Get ticketId - prompt if not provided
202
+ let ticketId = args.ticketId;
203
+ if (!ticketId) {
204
+ // Get all tickets, optionally filtered by project if -P/--project flag is provided
205
+ const allTickets = await this.storage.listTickets(projectId);
206
+ if (allTickets.length === 0) {
207
+ db.close();
208
+ return handleError('NO_TICKETS', 'No tickets found. Create a ticket first with "prlt ticket create".');
209
+ }
210
+ const selected = await this.selectFromList({
211
+ message: 'Select ticket to work on:',
212
+ items: allTickets,
213
+ getName: (t) => `[${t.priority || 'None'}] ${t.id} - ${t.title} (${t.assignee ? `assignee: ${t.assignee}` : 'unassigned'})`,
214
+ getValue: (t) => t.id,
215
+ getCommand: (t) => `prlt work start ${t.id} --json`,
216
+ jsonMode: jsonMode ? { flags, commandName: 'work start' } : null,
217
+ });
218
+ if (!selected) {
219
+ db.close();
220
+ return;
221
+ }
222
+ ticketId = selected;
223
+ }
224
+ // Get ticket
225
+ const ticket = await this.storage.getTicket(ticketId);
226
+ if (!ticket) {
227
+ db.close();
228
+ return handleError('TICKET_NOT_FOUND', `Ticket "${ticketId}" not found.`);
229
+ }
230
+ // Check if ticket is blocked by dependencies
231
+ const isBlocked = await this.storage.isTicketBlocked(ticketId);
232
+ if (isBlocked && !flags.force) {
233
+ const blockers = await this.storage.getTicketBlockers(ticketId);
234
+ const incompleteBlockers = blockers.filter(b => b.status !== 'done' && b.status !== 'canceled');
235
+ this.log('');
236
+ this.log(styles.warning(`⚠️ ${ticketId} is blocked by:`));
237
+ for (const blocker of incompleteBlockers) {
238
+ this.log(styles.muted(` - ${blocker.id}: ${blocker.title} (${blocker.status})`));
239
+ }
240
+ this.log('');
241
+ const { startAnyway } = await inquirer.prompt([
242
+ {
243
+ type: 'list',
244
+ name: 'startAnyway',
245
+ message: 'Start anyway?',
246
+ choices: [
247
+ { name: 'No, cancel', value: false },
248
+ { name: 'Yes, start despite blockers', value: true },
249
+ ],
250
+ default: false,
251
+ },
252
+ ]);
253
+ if (!startAnyway) {
254
+ db.close();
255
+ this.log(styles.muted('Cancelled.'));
256
+ return;
257
+ }
258
+ }
259
+ // Check for existing tmux session for this ticket
260
+ const existingSession = getTicketTmuxSession(ticketId);
261
+ if (existingSession && !flags.force) {
262
+ this.log('');
263
+ this.log(styles.warning(`Ticket ${ticketId} has an active tmux session (${existingSession.agent})`));
264
+ const { sessionAction } = await inquirer.prompt([
265
+ {
266
+ type: 'list',
267
+ name: 'sessionAction',
268
+ message: 'What would you like to do?',
269
+ choices: [
270
+ { name: 'Attach to existing session', value: 'attach' },
271
+ { name: 'Spawn new agent (keeps existing session)', value: 'spawn' },
272
+ { name: 'Kill session and respawn', value: 'kill' },
273
+ { name: 'Cancel', value: 'cancel' },
274
+ ],
275
+ },
276
+ ]);
277
+ if (sessionAction === 'cancel') {
278
+ db.close();
279
+ this.log(styles.muted('Cancelled.'));
280
+ return;
281
+ }
282
+ if (sessionAction === 'attach') {
283
+ // Attach to existing session
284
+ execSync(`tmux attach -t "${existingSession.sessionName}"`, { stdio: 'inherit' });
285
+ db.close();
286
+ return;
287
+ }
288
+ if (sessionAction === 'kill') {
289
+ killTmuxSession(existingSession.sessionName);
290
+ this.log(styles.success(`Killed session ${existingSession.sessionName}`));
291
+ }
292
+ // For 'spawn', we continue with creating a new agent
293
+ }
294
+ // Agent selection: ephemeral flag, agent flag, ticket assignee, or prompt
295
+ let agentName;
296
+ let agentWorktreePath;
297
+ let isEphemeralAgent = flags.ephemeral;
298
+ if (flags.ephemeral) {
299
+ // Create ephemeral agent on-demand
300
+ this.log(styles.muted('Creating ephemeral agent...'));
301
+ const ephemeralResult = await createEphemeralAgent(workspaceInfo, {
302
+ skipDevcontainer: flags['run-on-host'],
303
+ log: (msg) => this.log(msg),
304
+ });
305
+ agentName = ephemeralResult.name;
306
+ agentWorktreePath = ephemeralResult.worktreePath;
307
+ this.log(styles.success(`Created ephemeral agent: ${agentName}`));
308
+ }
309
+ else if (flags.agent) {
310
+ // Agent specified via flag
311
+ agentName = flags.agent;
312
+ }
313
+ else {
314
+ // Note: We no longer auto-reuse ticket.assignee to enable parallel work
315
+ // (e.g., groom + implement, or multiple implementations on same ticket)
316
+ // No agent specified - default to creating ephemeral agent (new behavior)
317
+ // Or prompt for agent selection if staff agents exist
318
+ // Get staff agents that exist on disk (warns about missing directories)
319
+ const activeStaffAgents = getActiveStaffAgents(workspaceInfo, (msg) => this.log(msg));
320
+ if (activeStaffAgents.length > 0) {
321
+ // Get list of busy agents (already running something)
322
+ const busyAgentNames = new Set();
323
+ for (const agent of activeStaffAgents) {
324
+ const runningExecutions = executionStorage.getAgentRunningExecutions(agent.name);
325
+ if (runningExecutions.length > 0) {
326
+ busyAgentNames.add(agent.name);
327
+ }
328
+ }
329
+ // Prompt to assign an agent
330
+ const agentChoices = [];
331
+ // Add ephemeral option first
332
+ agentChoices.push({ name: 'Create new ephemeral agent (recommended)', value: '__ephemeral__' });
333
+ agentChoices.push(new inquirer.Separator());
334
+ // Only show staff agents that exist on disk
335
+ const availableAgents = activeStaffAgents.filter(a => !busyAgentNames.has(a.name));
336
+ const busyAgents = activeStaffAgents.filter(a => busyAgentNames.has(a.name));
337
+ if (availableAgents.length > 0) {
338
+ agentChoices.push(new inquirer.Separator('── Available Staff Agents ──'));
339
+ for (const a of availableAgents) {
340
+ agentChoices.push({ name: a.name, value: a.name });
341
+ }
342
+ }
343
+ if (busyAgents.length > 0) {
344
+ agentChoices.push(new inquirer.Separator('── Busy (already working) ──'));
345
+ for (const a of busyAgents) {
346
+ const runningExecs = executionStorage.getAgentRunningExecutions(a.name);
347
+ const ticketIds = runningExecs.map(e => e.ticketId).join(', ');
348
+ agentChoices.push({ name: `${a.name} (working on ${ticketIds})`, value: a.name, disabled: 'busy' });
349
+ }
350
+ }
351
+ const { selectedAgent } = await inquirer.prompt([
352
+ {
353
+ type: 'list',
354
+ name: 'selectedAgent',
355
+ message: `Select agent for ${ticketId}:`,
356
+ choices: agentChoices,
357
+ },
358
+ ]);
359
+ if (selectedAgent === '__ephemeral__') {
360
+ // Create ephemeral agent
361
+ this.log(styles.muted('Creating ephemeral agent...'));
362
+ const ephemeralResult = await createEphemeralAgent(workspaceInfo, {
363
+ skipDevcontainer: flags['run-on-host'],
364
+ log: (msg) => this.log(msg),
365
+ });
366
+ agentName = ephemeralResult.name;
367
+ agentWorktreePath = ephemeralResult.worktreePath;
368
+ isEphemeralAgent = true;
369
+ this.log(styles.success(`Created ephemeral agent: ${agentName}`));
370
+ }
371
+ else {
372
+ agentName = selectedAgent;
373
+ }
374
+ }
375
+ else {
376
+ // No pre-registered agents - create ephemeral agent by default
377
+ this.log(styles.muted('Creating ephemeral agent...'));
378
+ const ephemeralResult = await createEphemeralAgent(workspaceInfo, {
379
+ skipDevcontainer: flags['run-on-host'],
380
+ log: (msg) => this.log(msg),
381
+ });
382
+ agentName = ephemeralResult.name;
383
+ agentWorktreePath = ephemeralResult.worktreePath;
384
+ isEphemeralAgent = true;
385
+ this.log(styles.success(`Created ephemeral agent: ${agentName}`));
386
+ }
387
+ }
388
+ // At this point agentName is guaranteed to be set
389
+ const assignedAgent = agentName;
390
+ // Validate agent - for non-ephemeral agents, check if it exists in workspace
391
+ let agentInfo = workspaceInfo.agents.find((a) => a.name === assignedAgent);
392
+ if (!isEphemeralAgent && !agentInfo) {
393
+ db.close();
394
+ this.error(`Agent "${assignedAgent}" not found in workspace.\n` +
395
+ `Use --ephemeral to create an ephemeral agent, or add a staff agent with "prlt agent add ${assignedAgent}"`);
396
+ }
397
+ // Check for running execution on this ticket (warning only, allows parallel work)
398
+ const runningExecution = executionStorage.getRunningExecution(ticketId);
399
+ if (runningExecution) {
400
+ this.log(styles.warning(`⚠️ Ticket "${ticketId}" already has work in progress: ${runningExecution.id}`));
401
+ this.log(styles.muted(` Starting parallel execution. Note: status updates may conflict.`));
402
+ }
403
+ // Check if agent is already working on something else
404
+ // Skip for ephemeral agents - they're created fresh for each spawn
405
+ if (!isEphemeralAgent) {
406
+ const agentRunningExecutions = executionStorage.getAgentRunningExecutions(assignedAgent);
407
+ if (agentRunningExecutions.length > 0 && !flags.force) {
408
+ const execInfo = agentRunningExecutions.map(e => ` ${e.id}: ${e.ticketId}`).join('\n');
409
+ db.close();
410
+ this.error(`Agent "${assignedAgent}" is already working on other tickets:\n${execInfo}\n\n` +
411
+ `Use --force to start anyway, or stop existing work first.`);
412
+ }
413
+ }
414
+ // Determine worktree path
415
+ // Agent directory structure varies:
416
+ // - Ephemeral: agents/temp/{agent}/ (created on-demand)
417
+ // - Staff HQ: agents/staff/{agent}/{repoName}/ (git worktree per repo)
418
+ // - Workspace-only: {agentsPath}/{agent}/{repoName}/ (git worktree)
419
+ // - HQ without repos: {agentsPath}/{agent}/ (placeholder, use cwd)
420
+ // For ephemeral agents, use the worktree path from creation
421
+ // For existing agents, derive from agentsPath
422
+ let agentDir;
423
+ if (isEphemeralAgent && agentWorktreePath) {
424
+ agentDir = agentWorktreePath;
425
+ }
426
+ else if (agentInfo?.worktree_path) {
427
+ // Agent has a worktree_path in the database
428
+ agentDir = path.join(workspaceInfo.path, agentInfo.worktree_path);
429
+ }
430
+ else {
431
+ // Fall back to default path calculation
432
+ agentDir = path.join(workspaceInfo.agentsPath, assignedAgent);
433
+ }
434
+ if (!fs.existsSync(agentDir)) {
435
+ db.close();
436
+ this.error(`Agent directory not found at ${agentDir}.\n` +
437
+ `Use --ephemeral to create an ephemeral agent, or create a staff agent with "prlt agent add ${assignedAgent}"`);
438
+ }
439
+ // For staff agents, check for uncommitted/unpushed work before starting
440
+ if (!isEphemeralAgent) {
441
+ const { getAgentGitStatus, pushAgentWork } = await import('../../lib/agents/commands.js');
442
+ const gitStatus = getAgentGitStatus(workspaceInfo, assignedAgent);
443
+ if (gitStatus.hasUnsavedWork) {
444
+ this.log(styles.warning(`\n⚠️ Agent "${assignedAgent}" has unsaved work:`));
445
+ for (const wt of gitStatus.worktrees) {
446
+ if (wt.hasUncommittedChanges) {
447
+ this.log(styles.muted(` ${wt.repoName}: ${wt.uncommittedFiles.length} uncommitted file(s)`));
448
+ }
449
+ if (wt.hasUnpushedCommits) {
450
+ this.log(styles.muted(` ${wt.repoName}: ${wt.unpushedCount} unpushed commit(s) on ${wt.branch}`));
451
+ }
452
+ }
453
+ this.log('');
454
+ const { action } = await inquirer.prompt([
455
+ {
456
+ type: 'list',
457
+ name: 'action',
458
+ message: 'How would you like to proceed?',
459
+ choices: [
460
+ { name: 'Push existing work and continue', value: 'push' },
461
+ { name: 'Continue anyway (existing work may conflict)', value: 'continue' },
462
+ { name: 'Cancel', value: 'cancel' },
463
+ ],
464
+ },
465
+ ]);
466
+ if (action === 'cancel') {
467
+ db.close();
468
+ this.log(styles.muted('Cancelled.'));
469
+ return;
470
+ }
471
+ if (action === 'push') {
472
+ const pushed = pushAgentWork(workspaceInfo, assignedAgent, (msg) => this.log(styles.muted(` ${msg}`)));
473
+ if (!pushed) {
474
+ this.log(styles.warning('Some work could not be pushed. Please resolve manually.'));
475
+ }
476
+ }
477
+ }
478
+ }
479
+ // Find worktree path for agent
480
+ // Agent directory may contain multiple repo worktrees - use the agent dir itself
481
+ // so Claude can work across all repos (frontend, backend, etc.)
482
+ let worktreePath = agentDir;
483
+ // Check if agent has repository worktrees (subdirectories with .git)
484
+ const agentContents = fs.readdirSync(agentDir);
485
+ const repoWorktrees = agentContents.filter(item => {
486
+ const itemPath = path.join(agentDir, item);
487
+ const gitPath = path.join(itemPath, '.git');
488
+ return fs.statSync(itemPath).isDirectory() && fs.existsSync(gitPath);
489
+ });
490
+ if (repoWorktrees.length === 1) {
491
+ // Single repo - open directly in the repo worktree
492
+ worktreePath = path.join(agentDir, repoWorktrees[0]);
493
+ }
494
+ else if (repoWorktrees.length > 1) {
495
+ // Multiple repos - open in agent directory, Claude can navigate between them
496
+ worktreePath = agentDir;
497
+ this.log(styles.muted(` Repos: ${repoWorktrees.join(', ')}`));
498
+ }
499
+ else {
500
+ // No git worktrees found - agent is a placeholder
501
+ // Fall back to the current working directory
502
+ this.log(styles.muted(` No git worktree found for agent, using current directory`));
503
+ worktreePath = process.cwd();
504
+ }
505
+ // Get coder name for branch naming (prompts on first use)
506
+ const coderName = await getOrPromptCoderName(db);
507
+ // Use ticket's existing branch or generate a new one
508
+ const branch = ticket.branch || generateBranchName(ticket.id, ticket.title, coderName, assignedAgent, ticket.category);
509
+ const isExistingBranch = !!ticket.branch;
510
+ // Get epic info if linked
511
+ let epicTitle;
512
+ if (ticket.epicId) {
513
+ const epic = await this.storage.getEpic(ticket.epicId);
514
+ epicTitle = epic?.title;
515
+ }
516
+ // Get spec info if linked
517
+ let specId;
518
+ let specTitle;
519
+ let specProblem;
520
+ let specSolution;
521
+ if (ticket.specId) {
522
+ const spec = await this.storage.getSpec(ticket.specId);
523
+ if (spec) {
524
+ specId = spec.id;
525
+ specTitle = spec.title;
526
+ specProblem = spec.problem;
527
+ specSolution = spec.solution;
528
+ }
529
+ }
530
+ // Determine action for this work session
531
+ let selectedAction = null;
532
+ let customPrompt;
533
+ if (flags.prompt) {
534
+ // Custom prompt overrides everything
535
+ customPrompt = flags.prompt;
536
+ }
537
+ else if (flags.action) {
538
+ // Specific action requested
539
+ selectedAction = await this.storage.getAction(flags.action);
540
+ if (!selectedAction) {
541
+ db.close();
542
+ this.error(`Action not found: ${flags.action}. Use "prlt action list" to see available actions.`);
543
+ }
544
+ }
545
+ else {
546
+ // Interactive action selection
547
+ // Get ticket's current status to determine suggested action
548
+ const ticketStatus = await this.storage.getStatus(ticket.statusId || '');
549
+ const currentCategory = ticketStatus?.category || 'unstarted';
550
+ // Get suggested action for this category
551
+ const suggestedAction = await this.storage.getSuggestedAction(currentCategory);
552
+ // Get all actions for selection
553
+ const allActions = await this.storage.listActions();
554
+ // Build choices with suggested action at top
555
+ const actionChoices = [];
556
+ if (suggestedAction) {
557
+ actionChoices.push({
558
+ name: `${suggestedAction.name} - ${suggestedAction.description || 'Suggested for ' + currentCategory} (Recommended)`,
559
+ value: suggestedAction.id,
560
+ });
561
+ actionChoices.push(new inquirer.Separator('── Other Actions ──'));
562
+ }
563
+ for (const action of allActions) {
564
+ if (suggestedAction && action.id === suggestedAction.id)
565
+ continue;
566
+ actionChoices.push({
567
+ name: `${action.name}${action.description ? ' - ' + action.description : ''}`,
568
+ value: action.id,
569
+ });
570
+ }
571
+ actionChoices.push(new inquirer.Separator('── Custom ──'));
572
+ actionChoices.push({ name: 'Custom prompt...', value: '__custom__' });
573
+ const { selectedActionId } = await inquirer.prompt([
574
+ {
575
+ type: 'list',
576
+ name: 'selectedActionId',
577
+ message: `What should the agent do with ${ticket.id}?`,
578
+ choices: actionChoices,
579
+ },
580
+ ]);
581
+ if (selectedActionId === '__custom__') {
582
+ const { customInput } = await inquirer.prompt([
583
+ {
584
+ type: 'input',
585
+ name: 'customInput',
586
+ message: 'Enter custom prompt:',
587
+ validate: (input) => input.trim() ? true : 'Prompt cannot be empty',
588
+ },
589
+ ]);
590
+ customPrompt = customInput.trim();
591
+ }
592
+ else {
593
+ selectedAction = await this.storage.getAction(selectedActionId);
594
+ }
595
+ }
596
+ // Build execution context with full ticket details
597
+ // HQ path comes from workspaceInfo (not derived from pmoPath since pmo can be nested in repos)
598
+ const hqPath = workspaceInfo.path;
599
+ const context = {
600
+ ticketId: ticket.id,
601
+ ticketTitle: ticket.title,
602
+ ticketDescription: ticket.description,
603
+ ticketSubtasks: ticket.subtasks?.map(s => ({ title: s.title, done: s.done })),
604
+ ticketPriority: ticket.priority,
605
+ ticketCategory: ticket.category,
606
+ epicTitle,
607
+ specId,
608
+ specTitle,
609
+ specProblem,
610
+ specSolution,
611
+ agentName: assignedAgent,
612
+ agentDir, // Agent directory (contains .devcontainer)
613
+ worktreePath, // Worktree path (may be subdirectory of agentDir)
614
+ branch,
615
+ hqPath,
616
+ pmoPath: this.pmoPath, // PMO path for container mounting
617
+ // Action context
618
+ actionId: selectedAction?.id,
619
+ actionName: selectedAction?.name || (customPrompt ? 'Custom' : undefined),
620
+ actionPrompt: customPrompt || selectedAction?.prompt,
621
+ actionEndPrompt: customPrompt ? undefined : selectedAction?.endPrompt,
622
+ modifiesCode: customPrompt ? true : selectedAction?.modifiesCode ?? true,
623
+ };
624
+ // Check if agent has devcontainer config
625
+ const hasDevcontainer = hasDevcontainerConfig(agentDir);
626
+ // Use devcontainer by default if available, unless --run-on-host is set
627
+ const useDevcontainer = hasDevcontainer && !flags['run-on-host'];
628
+ // Determine execution environment and display mode
629
+ let environment = 'host';
630
+ let displayMode = 'terminal';
631
+ let sandboxed = false; // Whether --dangerously-skip-permissions is NOT used
632
+ if (hasDevcontainer && !flags.mode && !flags['run-on-host']) {
633
+ // Agent has devcontainer - prompt for environment choice
634
+ // Loop to allow re-selection if Docker isn't running
635
+ let environmentSelected = false;
636
+ while (!environmentSelected) {
637
+ const { selectedEnvironment } = await inquirer.prompt([
638
+ {
639
+ type: 'list',
640
+ name: 'selectedEnvironment',
641
+ message: 'Where should the agent run?',
642
+ choices: [
643
+ { name: '🐳 devcontainer (sandboxed, recommended)', value: 'devcontainer' },
644
+ { name: '💻 host (runs directly on your machine)', value: 'host' },
645
+ { name: '✗ cancel', value: 'cancel' },
646
+ ],
647
+ default: 'devcontainer',
648
+ },
649
+ ]);
650
+ if (selectedEnvironment === 'cancel') {
651
+ db.close();
652
+ this.log(styles.muted('Cancelled.'));
653
+ return;
654
+ }
655
+ if (selectedEnvironment === 'devcontainer') {
656
+ // Check Docker is running before proceeding with devcontainer
657
+ if (!isDockerRunning()) {
658
+ this.log('');
659
+ this.warn('Docker is not running.\n' +
660
+ 'Docker is required for devcontainer execution.\n' +
661
+ 'Please start Docker Desktop or select "host" to run directly on your machine.');
662
+ this.log('');
663
+ continue; // Re-prompt for environment selection
664
+ }
665
+ environment = 'devcontainer';
666
+ // Pick display mode for devcontainer
667
+ const { selectedDisplay } = await inquirer.prompt([
668
+ {
669
+ type: 'list',
670
+ name: 'selectedDisplay',
671
+ message: 'How should the agent output be displayed?',
672
+ choices: [
673
+ { name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
674
+ { name: '📦 Background - Runs detached, reattach with: prlt session attach', value: 'background' },
675
+ ],
676
+ default: 'terminal',
677
+ },
678
+ ]);
679
+ displayMode = selectedDisplay;
680
+ environment = 'devcontainer';
681
+ environmentSelected = true;
682
+ }
683
+ else {
684
+ // User chose host
685
+ environment = 'host';
686
+ const { selectedDisplay } = await inquirer.prompt([
687
+ {
688
+ type: 'list',
689
+ name: 'selectedDisplay',
690
+ message: 'How should the agent output be displayed?',
691
+ choices: [
692
+ { name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
693
+ { name: '📦 Background - Runs detached, reattach with: prlt session attach', value: 'background' },
694
+ ],
695
+ default: 'terminal',
696
+ },
697
+ ]);
698
+ displayMode = selectedDisplay;
699
+ environmentSelected = true;
700
+ }
701
+ }
702
+ }
703
+ else if (useDevcontainer) {
704
+ // Devcontainer with explicit mode flag
705
+ environment = 'devcontainer';
706
+ // Use --display flag if provided, otherwise fall back to --mode or default to 'terminal'
707
+ if (flags.display) {
708
+ displayMode = flags.display;
709
+ }
710
+ else if (flags.mode && ['terminal', 'background'].includes(flags.mode)) {
711
+ displayMode = flags.mode;
712
+ }
713
+ else {
714
+ // Default to terminal for devcontainer (opens new tab instead of blocking current terminal)
715
+ displayMode = 'terminal';
716
+ }
717
+ }
718
+ else {
719
+ // No devcontainer or --run-on-host - host mode selection
720
+ if (flags.mode) {
721
+ const flagMode = flags.mode;
722
+ // Set environment based on mode flag
723
+ if (flagMode === 'docker') {
724
+ environment = 'docker';
725
+ displayMode = 'terminal';
726
+ }
727
+ else if (flagMode === 'vm') {
728
+ environment = 'vm';
729
+ displayMode = 'terminal';
730
+ }
731
+ else {
732
+ // Host environment: terminal/background are display modes
733
+ environment = 'host';
734
+ displayMode = flagMode;
735
+ }
736
+ }
737
+ else {
738
+ const warningMsg = flags['run-on-host']
739
+ ? 'Select execution mode (--run-on-host: bypassing devcontainer):'
740
+ : 'Select execution mode (no devcontainer - running on host):';
741
+ const { selectedMode } = await inquirer.prompt([
742
+ {
743
+ type: 'list',
744
+ name: 'selectedMode',
745
+ message: warningMsg,
746
+ choices: [
747
+ { name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
748
+ { name: '📦 Background - Runs detached, reattach with: prlt session attach', value: 'background' },
749
+ new inquirer.Separator('── Sandboxed (requires setup) ──'),
750
+ { name: '🐳 Docker - Container with worktree mounted', value: 'docker' },
751
+ new inquirer.Separator('── Remote ──'),
752
+ { name: '☁️ VM - Remote VM via SSH', value: 'vm' },
753
+ ],
754
+ default: 'terminal',
755
+ },
756
+ ]);
757
+ // Set environment based on selection
758
+ if (selectedMode === 'docker') {
759
+ environment = 'docker';
760
+ displayMode = 'terminal';
761
+ }
762
+ else if (selectedMode === 'vm') {
763
+ environment = 'vm';
764
+ displayMode = 'terminal';
765
+ }
766
+ else {
767
+ // Host environment: terminal/background are display modes
768
+ environment = 'host';
769
+ displayMode = selectedMode;
770
+ }
771
+ }
772
+ }
773
+ const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
774
+ // Default to interactive output mode (streaming UI)
775
+ // Can be overridden via --output flag if needed
776
+ let outputMode = flags.output || DEFAULT_EXECUTION_CONFIG.outputMode;
777
+ // Prompt for permissions mode (all environments)
778
+ // Skip prompt if --skip-permissions flag is set
779
+ if (flags['skip-permissions']) {
780
+ sandboxed = false;
781
+ }
782
+ else {
783
+ const containerNote = (environment === 'devcontainer' || environment === 'docker')
784
+ ? ' (container provides additional isolation)'
785
+ : '';
786
+ const { permissionMode } = await inquirer.prompt([
787
+ {
788
+ type: 'list',
789
+ name: 'permissionMode',
790
+ message: `Permission mode for Claude Code${containerNote}:`,
791
+ choices: [
792
+ { name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger' },
793
+ { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
794
+ ],
795
+ default: 'danger',
796
+ },
797
+ ]);
798
+ sandboxed = permissionMode === 'safe';
799
+ }
800
+ // Prompt for PR creation when work is complete
801
+ // Only show if gh CLI is available and authenticated
802
+ let createPR = false;
803
+ const ghAvailable = isGHInstalled() && isGHAuthenticated();
804
+ // Use flags if provided, otherwise prompt
805
+ if (flags['create-pr']) {
806
+ createPR = true;
807
+ }
808
+ else if (flags['no-pr']) {
809
+ createPR = false;
810
+ }
811
+ else if (ghAvailable) {
812
+ const { prChoice } = await inquirer.prompt([
813
+ {
814
+ type: 'list',
815
+ name: 'prChoice',
816
+ message: 'Create a pull request when work is ready?',
817
+ choices: [
818
+ { name: '✓ Yes - Create PR when running `prlt work ready`', value: 'yes' },
819
+ { name: '✗ No - Just move ticket to review (can create PR later)', value: 'no' },
820
+ ],
821
+ default: 'yes',
822
+ },
823
+ ]);
824
+ createPR = prChoice === 'yes';
825
+ }
826
+ // Show execution info
827
+ this.log('');
828
+ this.log(styles.header(`🚀 Starting work: ${ticket.id}: ${ticket.title}`));
829
+ this.log(styles.muted(` Agent: ${assignedAgent}`));
830
+ this.log(styles.muted(` Action: ${context.actionName || 'None'}`));
831
+ this.log(styles.muted(` Executor: ${executor}`));
832
+ // Environment info
833
+ const envIcon = environment === 'devcontainer' ? '🐳' : (environment === 'docker' ? '📦' : '💻');
834
+ this.log(styles.muted(` Environment: ${envIcon} ${environment}`));
835
+ this.log(styles.muted(` Display: ${displayMode}`));
836
+ // Permissions info
837
+ if (sandboxed) {
838
+ this.log(styles.success(` Permissions: 🔒 safe`));
839
+ }
840
+ else {
841
+ this.log(styles.warning(` Permissions: ⚠️ danger (--dangerously-skip-permissions)`));
842
+ }
843
+ this.log(styles.muted(` Output: ${outputMode === 'interactive' ? 'streaming (watch Claude work)' : 'print (final result only)'}`));
844
+ if (ghAvailable) {
845
+ this.log(styles.muted(` Create PR: ${createPR ? 'yes (when work is ready)' : 'no'}`));
846
+ }
847
+ this.log(styles.muted(` Worktree: ${worktreePath}`));
848
+ this.log(styles.muted(` Branch: ${branch}`));
849
+ this.log('');
850
+ // Add createPR to context
851
+ context.createPR = createPR;
852
+ // Handle git operations
853
+ let finalBranch = branch;
854
+ // Set up repo paths (needed for all action types)
855
+ const gitRepos = repoWorktrees.length > 0
856
+ ? repoWorktrees.map(r => path.join(agentDir, r))
857
+ : [worktreePath];
858
+ const primaryRepo = gitRepos[0];
859
+ // Always fetch latest from origin (regardless of action type)
860
+ // This ensures groom and other non-code-modifying actions see current code
861
+ for (const repoPath of gitRepos) {
862
+ if (isGitRepo(repoPath)) {
863
+ tryGitCommand('git fetch origin', repoPath);
864
+ }
865
+ }
866
+ // Branch handling - only if action modifies code
867
+ if (context.modifiesCode !== false) {
868
+ if (isExistingBranch) {
869
+ // Ticket already has a branch linked - just use it
870
+ this.log(styles.muted(`Using existing branch: ${branch}`));
871
+ }
872
+ else if (flags.action || flags.force) {
873
+ // Non-interactive mode (spawned from batch command) - auto-create branch
874
+ finalBranch = branch;
875
+ this.log(styles.muted(`Branch: ${finalBranch}`));
876
+ }
877
+ else {
878
+ // No branch in DB - ask user if one already exists
879
+ const { branchChoice } = await inquirer.prompt([
880
+ {
881
+ type: 'list',
882
+ name: 'branchChoice',
883
+ message: `Does a branch already exist for ${ticket.id}?`,
884
+ choices: [
885
+ { name: 'No, create new branch (Recommended)', value: 'create' },
886
+ { name: 'Yes, I\'ll enter the branch name', value: 'enter' },
887
+ { name: 'Search for matching branches', value: 'search' },
888
+ ],
889
+ },
890
+ ]);
891
+ if (branchChoice === 'enter') {
892
+ // User enters existing branch name
893
+ const { enteredBranch } = await inquirer.prompt([
894
+ {
895
+ type: 'input',
896
+ name: 'enteredBranch',
897
+ message: 'Enter branch name:',
898
+ validate: (input) => input.trim() ? true : 'Branch name required',
899
+ },
900
+ ]);
901
+ finalBranch = enteredBranch.trim();
902
+ // Validate branch exists (locally or in origin)
903
+ try {
904
+ execSync(`git rev-parse --verify ${finalBranch}`, { cwd: primaryRepo, stdio: 'pipe' });
905
+ this.log(styles.muted(` Found local branch: ${finalBranch}`));
906
+ }
907
+ catch {
908
+ // Try fetching from origin
909
+ try {
910
+ execSync(`git fetch origin ${finalBranch}:${finalBranch}`, { cwd: primaryRepo, stdio: 'pipe' });
911
+ this.log(styles.muted(` Fetched from origin: ${finalBranch}`));
912
+ }
913
+ catch {
914
+ this.warn(`Branch "${finalBranch}" not found locally or in origin. Will create it.`);
915
+ }
916
+ }
917
+ }
918
+ else if (branchChoice === 'search') {
919
+ // Search for matching branches
920
+ let remoteBranches = [];
921
+ try {
922
+ execSync('git fetch --prune', { cwd: primaryRepo, stdio: 'pipe' });
923
+ const branchOutput = execSync(`git branch -r`, { cwd: primaryRepo, encoding: 'utf-8' });
924
+ remoteBranches = branchOutput
925
+ .split('\n')
926
+ .map(b => b.trim())
927
+ .filter(b => b && !b.includes('HEAD') && b.toLowerCase().includes(ticket.id.toLowerCase()));
928
+ }
929
+ catch {
930
+ // Ignore fetch errors
931
+ }
932
+ if (remoteBranches.length > 0) {
933
+ const branchChoices = [
934
+ ...remoteBranches.map(b => ({ name: b, value: b.replace('origin/', '') })),
935
+ new inquirer.Separator(),
936
+ { name: 'None of these, create new branch', value: '__create__' },
937
+ ];
938
+ const { selectedBranch } = await inquirer.prompt([
939
+ {
940
+ type: 'list',
941
+ name: 'selectedBranch',
942
+ message: `Found ${remoteBranches.length} matching branch(es):`,
943
+ choices: branchChoices,
944
+ },
945
+ ]);
946
+ if (selectedBranch !== '__create__') {
947
+ finalBranch = selectedBranch;
948
+ // Fetch and checkout the selected branch
949
+ try {
950
+ execSync(`git fetch origin ${finalBranch}:${finalBranch}`, { cwd: primaryRepo, stdio: 'pipe' });
951
+ this.log(styles.muted(` Fetched: ${finalBranch}`));
952
+ }
953
+ catch {
954
+ // Branch might already exist locally
955
+ }
956
+ }
957
+ }
958
+ else {
959
+ this.log(styles.muted(` No matching branches found for "${ticket.id}". Creating new.`));
960
+ }
961
+ }
962
+ // branchChoice === 'create' uses the generated branch name (default)
963
+ this.log(styles.muted(`Branch: ${finalBranch}`));
964
+ }
965
+ // Handle branch in each repo
966
+ for (const repoPath of gitRepos) {
967
+ const repoName = path.basename(repoPath);
968
+ if (!isGitRepo(repoPath)) {
969
+ continue;
970
+ }
971
+ // Note: fetch already happened above (unconditionally for all action types)
972
+ try {
973
+ // Check if branch exists and checkout
974
+ if (tryGitCommand(`git rev-parse --verify ${finalBranch}`, repoPath)) {
975
+ execSync(`git checkout ${finalBranch}`, { cwd: repoPath, stdio: 'pipe' });
976
+ this.log(styles.muted(` ${repoName}: checked out branch`));
977
+ }
978
+ else {
979
+ // Branch doesn't exist - create from best available base
980
+ const baseBranch = findBaseBranch(repoPath);
981
+ execSync(`git checkout -b ${finalBranch} ${baseBranch}`, { cwd: repoPath, stdio: 'pipe' });
982
+ this.log(styles.muted(` ${repoName}: created new branch from ${baseBranch}`));
983
+ }
984
+ }
985
+ catch (error) {
986
+ this.warn(`Could not handle branch in ${repoName}: ${error instanceof Error ? error.message : error}`);
987
+ }
988
+ }
989
+ // Save branch to ticket
990
+ if (!isExistingBranch || finalBranch !== branch) {
991
+ await this.storage.updateTicket(ticket.id, { branch: finalBranch });
992
+ }
993
+ // Update context with final branch
994
+ context.branch = finalBranch;
995
+ }
996
+ else {
997
+ // Non-code-modifying action (e.g., groom) - checkout main/latest to see current code
998
+ this.log(styles.muted('Skipping branch creation (action does not modify code)'));
999
+ for (const repoPath of gitRepos) {
1000
+ const repoName = path.basename(repoPath);
1001
+ if (!isGitRepo(repoPath)) {
1002
+ continue;
1003
+ }
1004
+ try {
1005
+ // Checkout the latest main/master branch
1006
+ const baseBranch = findBaseBranch(repoPath);
1007
+ // Extract local branch name from origin/main -> main
1008
+ const localBranch = baseBranch.replace('origin/', '');
1009
+ execSync(`git checkout ${localBranch}`, { cwd: repoPath, stdio: 'pipe' });
1010
+ // Pull latest changes
1011
+ tryGitCommand(`git pull origin ${localBranch}`, repoPath);
1012
+ this.log(styles.muted(` ${repoName}: checked out ${localBranch} (latest)`));
1013
+ }
1014
+ catch (error) {
1015
+ this.warn(`Could not checkout main in ${repoName}: ${error instanceof Error ? error.message : error}`);
1016
+ }
1017
+ }
1018
+ }
1019
+ // Create execution record
1020
+ const execution = executionStorage.createExecution({
1021
+ ticketId: ticket.id,
1022
+ agentName: assignedAgent,
1023
+ executor,
1024
+ environment,
1025
+ displayMode,
1026
+ sandboxed,
1027
+ branch,
1028
+ });
1029
+ this.log(styles.muted(` Work ID: ${execution.id}`));
1030
+ this.log('');
1031
+ // Note: Ticket status update moved to after successful spawn (see below)
1032
+ // Load execution config from database
1033
+ const executionConfig = loadExecutionConfig(db);
1034
+ // If terminal display mode, ensure terminal and shell preferences are set (prompts on first use)
1035
+ // Also re-prompt if --reconfigure flag is set
1036
+ const needsTerminalConfig = displayMode === 'terminal';
1037
+ if (needsTerminalConfig) {
1038
+ const needsTerminal = !hasTerminalPreference(db);
1039
+ const needsShell = !hasShellPreference(db);
1040
+ // First-time setup: prompt for both together
1041
+ if ((needsTerminal || needsShell) && !flags.reconfigure) {
1042
+ this.log(styles.header('First-time execution setup'));
1043
+ this.log('');
1044
+ }
1045
+ let terminalApp;
1046
+ let shell;
1047
+ if (flags.reconfigure) {
1048
+ terminalApp = await promptTerminalPreference(db);
1049
+ shell = await promptShellPreference(db);
1050
+ this.log(styles.success(` Terminal: ${terminalApp}`));
1051
+ this.log(styles.success(` Shell: ${shell}`));
1052
+ }
1053
+ else {
1054
+ terminalApp = await getTerminalApp(db);
1055
+ shell = await getShell(db);
1056
+ this.log(styles.muted(` Terminal: ${terminalApp}`));
1057
+ this.log(styles.muted(` Shell: ${shell}`));
1058
+ }
1059
+ executionConfig.terminal.app = terminalApp;
1060
+ executionConfig.shell = shell;
1061
+ }
1062
+ // Set output mode from user selection
1063
+ executionConfig.outputMode = outputMode;
1064
+ // Set sandboxed mode (determines whether --dangerously-skip-permissions is used)
1065
+ executionConfig.sandboxed = sandboxed;
1066
+ // Run execution
1067
+ this.log(styles.muted('Starting agent...'));
1068
+ const sessionManager = (flags.session || 'tmux');
1069
+ const result = await runExecution(environment, context, executor, executionConfig, {
1070
+ host: flags['vm-host'],
1071
+ displayMode,
1072
+ sessionManager: environment === 'devcontainer' ? sessionManager : undefined,
1073
+ });
1074
+ if (result.success) {
1075
+ // Update execution record with process info
1076
+ executionStorage.updateStatus(execution.id, 'running');
1077
+ executionStorage.updateProcessInfo(execution.id, {
1078
+ pid: result.pid,
1079
+ containerId: result.containerId,
1080
+ sessionId: result.sessionId,
1081
+ logPath: result.logPath,
1082
+ });
1083
+ // Track container in containers table (for devcontainer environment)
1084
+ if (environment === 'devcontainer' && result.containerId) {
1085
+ const containerStorage = new ContainerStorage(db);
1086
+ containerStorage.upsertContainer({
1087
+ agentName: context.agentName,
1088
+ dockerId: result.containerId,
1089
+ status: 'running',
1090
+ currentExecutionId: execution.id,
1091
+ });
1092
+ }
1093
+ // Update ticket assignee ONLY after successful spawn
1094
+ if (!ticket.assignee || ticket.assignee !== assignedAgent) {
1095
+ await this.storage.updateTicket(ticket.id, { assignee: assignedAgent });
1096
+ this.log(styles.muted(` Assigned to: ${assignedAgent}`));
1097
+ }
1098
+ // Move ticket to target column based on action's defaultMoveToCategory
1099
+ // If action has a target category, find the matching column; otherwise use "started" default
1100
+ const targetCategory = selectedAction?.defaultMoveToCategory || 'started';
1101
+ const board = await this.storage.getBoard(ticket.projectId);
1102
+ const columnNames = board.columns.map(col => col.name);
1103
+ // Map category to column type for lookup
1104
+ const columnType = targetCategory === 'started' ? 'in_progress' :
1105
+ targetCategory === 'unstarted' ? 'planned' :
1106
+ targetCategory === 'completed' ? 'done' : 'in_progress';
1107
+ // Get the configured column name for this type (e.g., "In Progress" for in_progress)
1108
+ const workColumnName = getWorkColumnSetting(db, columnType);
1109
+ // Find the actual column on the board (case-insensitive, partial match)
1110
+ const targetColumnName = findColumnByName(columnNames, workColumnName);
1111
+ if (targetColumnName && ticket.statusName !== targetColumnName) {
1112
+ try {
1113
+ await this.storage.moveTicket(ticket.projectId, ticket.id, targetColumnName);
1114
+ this.log(styles.muted(` Moved to: ${targetColumnName}`));
1115
+ }
1116
+ catch (moveError) {
1117
+ // Non-fatal - work can proceed even if column move fails
1118
+ this.warn(`Could not move ticket to "${targetColumnName}": ${moveError instanceof Error ? moveError.message : moveError}`);
1119
+ }
1120
+ }
1121
+ await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
1122
+ this.log('');
1123
+ this.log(styles.success(`✓ Work started (${execution.id})`));
1124
+ this.log('');
1125
+ this.log(styles.muted('Commands:'));
1126
+ this.log(styles.muted(` prlt work status View work status`));
1127
+ this.log(styles.muted(` prlt work ready ${ticketId} Mark ready for review`));
1128
+ this.log(styles.muted(` prlt work stop ${execution.id} Stop work`));
1129
+ }
1130
+ else {
1131
+ executionStorage.updateStatus(execution.id, 'failed');
1132
+ this.error(`Failed to start work: ${result.error}`);
1133
+ }
1134
+ db.close();
1135
+ }
1136
+ catch (error) {
1137
+ db.close();
1138
+ throw error;
1139
+ }
1140
+ }
1141
+ /**
1142
+ * Run batch mode: spawn work for all unassigned backlog tickets
1143
+ */
1144
+ async runBatchMode(workspaceInfo, db, executionStorage, flags) {
1145
+ // Get all tickets and filter to backlog/unstarted (not in progress)
1146
+ // Note: In batch mode, we use undefined to get all tickets across all projects
1147
+ const allTickets = await this.storage.listTickets(undefined);
1148
+ const backlogTickets = allTickets.filter(t => t.statusCategory === 'backlog' || t.statusCategory === 'unstarted' || !t.statusCategory);
1149
+ if (backlogTickets.length === 0) {
1150
+ db.close();
1151
+ this.log(styles.muted('No backlog tickets to start.'));
1152
+ return;
1153
+ }
1154
+ this.log('');
1155
+ this.log(styles.header(`🚀 Batch Start: ${backlogTickets.length} backlog tickets`));
1156
+ this.log('');
1157
+ // Get staff agents that exist on disk (warns about missing directories)
1158
+ const activeStaffAgents = getActiveStaffAgents(workspaceInfo, (msg) => this.log(msg));
1159
+ const busyAgentNames = new Set();
1160
+ for (const agent of activeStaffAgents) {
1161
+ const runningExecutions = executionStorage.getAgentRunningExecutions(agent.name);
1162
+ if (runningExecutions.length > 0) {
1163
+ busyAgentNames.add(agent.name);
1164
+ }
1165
+ }
1166
+ const availableAgents = activeStaffAgents.filter(a => !busyAgentNames.has(a.name));
1167
+ if (availableAgents.length === 0) {
1168
+ db.close();
1169
+ this.error('No available agents. All agents are busy with other work.');
1170
+ }
1171
+ this.log(styles.muted(`Available agents: ${availableAgents.map(a => a.name).join(', ')}`));
1172
+ this.log(styles.muted(`Tickets to spawn: ${backlogTickets.map(t => t.id).join(', ')}`));
1173
+ this.log('');
1174
+ // Confirm before batch spawning
1175
+ const { confirm } = await inquirer.prompt([
1176
+ {
1177
+ type: 'list',
1178
+ name: 'confirm',
1179
+ message: `Start work on ${backlogTickets.length} tickets using ${availableAgents.length} available agents?`,
1180
+ choices: [
1181
+ { name: 'Yes', value: true },
1182
+ { name: 'No', value: false },
1183
+ ],
1184
+ },
1185
+ ]);
1186
+ if (!confirm) {
1187
+ db.close();
1188
+ this.log(styles.muted('Cancelled.'));
1189
+ return;
1190
+ }
1191
+ // Assign tickets to agents (round-robin)
1192
+ const assignments = [];
1193
+ for (let i = 0; i < backlogTickets.length; i++) {
1194
+ const agent = availableAgents[i % availableAgents.length];
1195
+ assignments.push({ ticket: backlogTickets[i], agent });
1196
+ }
1197
+ // Spawn each ticket
1198
+ let successCount = 0;
1199
+ let failCount = 0;
1200
+ for (const { ticket, agent } of assignments) {
1201
+ try {
1202
+ this.log(styles.muted(`Starting ${ticket.id} with ${agent.name}...`));
1203
+ // Use the work:start command for each ticket
1204
+ // Pass --project from ticket to avoid re-prompting for project selection
1205
+ await this.config.runCommand('work:start', [
1206
+ ticket.id,
1207
+ ...(ticket.projectId ? ['--project', ticket.projectId] : []),
1208
+ '--mode', flags.mode || 'background',
1209
+ ...(flags.executor ? ['--executor', flags.executor] : []),
1210
+ ...(flags['run-on-host'] ? ['--run-on-host'] : []),
1211
+ ...(flags.force ? ['--force'] : []),
1212
+ ]);
1213
+ successCount++;
1214
+ }
1215
+ catch (error) {
1216
+ failCount++;
1217
+ this.log(styles.error(`Failed to start ${ticket.id}: ${error instanceof Error ? error.message : error}`));
1218
+ }
1219
+ }
1220
+ db.close();
1221
+ this.log('');
1222
+ this.log(styles.success(`✓ Batch complete: ${successCount} started, ${failCount} failed`));
1223
+ const remaining = backlogTickets.length - assignments.length;
1224
+ if (remaining > 0) {
1225
+ this.log(styles.muted(` ${remaining} ticket(s) remain in backlog (no available agents)`));
1226
+ }
1227
+ }
1228
+ /**
1229
+ * Spawn work on a single ticket with non-interactive defaults.
1230
+ */
1231
+ async spawnSingleTicket(ticket, agent, workspaceInfo, executionStorage, db, flags) {
1232
+ const agentName = agent.name;
1233
+ // Note: Ticket assignee update moved to after successful spawn
1234
+ // Find agent directory and worktree
1235
+ const agentDir = path.join(workspaceInfo.agentsPath, agentName);
1236
+ if (!fs.existsSync(agentDir)) {
1237
+ throw new Error(`Agent directory not found: ${agentDir}`);
1238
+ }
1239
+ // Find worktree path
1240
+ let worktreePath = agentDir;
1241
+ const agentContents = fs.readdirSync(agentDir);
1242
+ const repoWorktrees = agentContents.filter(item => {
1243
+ const itemPath = path.join(agentDir, item);
1244
+ const gitPath = path.join(itemPath, '.git');
1245
+ return fs.statSync(itemPath).isDirectory() && fs.existsSync(gitPath);
1246
+ });
1247
+ if (repoWorktrees.length === 1) {
1248
+ worktreePath = path.join(agentDir, repoWorktrees[0]);
1249
+ }
1250
+ // Get coder name for branch naming (prompts on first use)
1251
+ const coderName = await getOrPromptCoderName(db);
1252
+ // Use ticket's existing branch or generate a new one
1253
+ const branch = ticket.branch || generateBranchName(ticket.id, ticket.title, coderName, agentName, ticket.category);
1254
+ const isExistingBranch = !!ticket.branch;
1255
+ // Get epic and spec info
1256
+ let epicTitle;
1257
+ let specId;
1258
+ let specTitle;
1259
+ let specProblem;
1260
+ let specSolution;
1261
+ if (ticket.epicId) {
1262
+ const epic = await this.storage.getEpic(ticket.epicId);
1263
+ epicTitle = epic?.title;
1264
+ }
1265
+ if (ticket.specId) {
1266
+ const spec = await this.storage.getSpec(ticket.specId);
1267
+ if (spec) {
1268
+ specId = spec.id;
1269
+ specTitle = spec.title;
1270
+ specProblem = spec.problem;
1271
+ specSolution = spec.solution;
1272
+ }
1273
+ }
1274
+ // Get default action for batch mode (use 'implement')
1275
+ const defaultAction = await this.storage.getAction('implement');
1276
+ // Build context
1277
+ const context = {
1278
+ ticketId: ticket.id,
1279
+ ticketTitle: ticket.title,
1280
+ ticketDescription: ticket.description,
1281
+ ticketSubtasks: ticket.subtasks?.map(s => ({ title: s.title, done: s.done })),
1282
+ ticketPriority: ticket.priority,
1283
+ ticketCategory: ticket.category,
1284
+ epicTitle,
1285
+ specId,
1286
+ specTitle,
1287
+ specProblem,
1288
+ specSolution,
1289
+ agentName,
1290
+ agentDir,
1291
+ worktreePath,
1292
+ branch,
1293
+ hqPath: workspaceInfo.path,
1294
+ pmoPath: this.pmoPath,
1295
+ createPR: flags['create-pr'] || false,
1296
+ // Use 'implement' action for batch mode
1297
+ actionId: defaultAction?.id,
1298
+ actionName: defaultAction?.name,
1299
+ actionPrompt: defaultAction?.prompt,
1300
+ actionEndPrompt: defaultAction?.endPrompt,
1301
+ modifiesCode: defaultAction?.modifiesCode ?? true,
1302
+ };
1303
+ // Use devcontainer by default if available
1304
+ const hasDevcontainer = hasDevcontainerConfig(agentDir);
1305
+ const useDevcontainer = hasDevcontainer && !flags['run-on-host'];
1306
+ // Non-interactive defaults
1307
+ const environment = useDevcontainer ? 'devcontainer' : 'host';
1308
+ const displayMode = 'terminal';
1309
+ const sandboxed = !flags['skip-permissions'];
1310
+ const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
1311
+ const outputMode = 'interactive';
1312
+ // Handle git branch - only if action modifies code
1313
+ if (context.modifiesCode !== false) {
1314
+ const gitRepos = repoWorktrees.length > 0
1315
+ ? repoWorktrees.map(r => path.join(agentDir, r))
1316
+ : [worktreePath];
1317
+ for (const repoPath of gitRepos) {
1318
+ if (!isGitRepo(repoPath)) {
1319
+ continue;
1320
+ }
1321
+ // Fetch latest from origin (best-effort, may fail if offline)
1322
+ tryGitCommand('git fetch origin', repoPath);
1323
+ try {
1324
+ // Check if branch exists and checkout
1325
+ if (tryGitCommand(`git rev-parse --verify ${branch}`, repoPath)) {
1326
+ execSync(`git checkout ${branch}`, { cwd: repoPath, stdio: 'pipe' });
1327
+ }
1328
+ else {
1329
+ // Branch doesn't exist - create from best available base
1330
+ const baseBranch = findBaseBranch(repoPath);
1331
+ execSync(`git checkout -b ${branch} ${baseBranch}`, { cwd: repoPath, stdio: 'pipe' });
1332
+ }
1333
+ }
1334
+ catch {
1335
+ // Ignore branch errors in batch mode - continue with other repos
1336
+ }
1337
+ }
1338
+ // Save branch to ticket if newly created
1339
+ if (!isExistingBranch) {
1340
+ await this.storage.updateTicket(ticket.id, { branch });
1341
+ }
1342
+ }
1343
+ // Create execution record
1344
+ const execution = executionStorage.createExecution({
1345
+ ticketId: ticket.id,
1346
+ agentName,
1347
+ executor,
1348
+ environment,
1349
+ displayMode,
1350
+ sandboxed,
1351
+ branch,
1352
+ });
1353
+ // Note: Ticket status update moved to after successful spawn
1354
+ // Load execution config
1355
+ const executionConfig = loadExecutionConfig(db);
1356
+ executionConfig.outputMode = outputMode;
1357
+ executionConfig.sandboxed = sandboxed;
1358
+ // Run execution
1359
+ this.log(styles.muted(` Starting ${ticket.id} → ${agentName}...`));
1360
+ const batchSessionManager = (flags.session || 'tmux');
1361
+ const result = await runExecution(environment, context, executor, executionConfig, {
1362
+ displayMode,
1363
+ sessionManager: environment === 'devcontainer' ? batchSessionManager : undefined,
1364
+ });
1365
+ if (result.success) {
1366
+ executionStorage.updateStatus(execution.id, 'running');
1367
+ executionStorage.updateProcessInfo(execution.id, {
1368
+ pid: result.pid,
1369
+ containerId: result.containerId,
1370
+ sessionId: result.sessionId,
1371
+ logPath: result.logPath,
1372
+ });
1373
+ // Update ticket assignee ONLY after successful spawn
1374
+ if (!ticket.assignee || ticket.assignee !== agentName) {
1375
+ await this.storage.updateTicket(ticket.id, { assignee: agentName });
1376
+ }
1377
+ // Move ticket to In Progress column ONLY after successful spawn
1378
+ const targetColumnName = getWorkColumnSetting(db, 'in_progress');
1379
+ const board = await this.storage.getBoard(ticket.projectId);
1380
+ const columnNames = board.columns.map(col => col.name);
1381
+ const inProgressColumn = findColumnByName(columnNames, targetColumnName);
1382
+ if (inProgressColumn && ticket.status !== inProgressColumn) {
1383
+ await this.storage.moveTicket(ticket.projectId, ticket.id, inProgressColumn);
1384
+ }
1385
+ await autoExportToBoard(this.pmoPath, this.storage, () => { });
1386
+ this.log(styles.success(` ✓ ${ticket.id} started (${execution.id})`));
1387
+ }
1388
+ else {
1389
+ executionStorage.updateStatus(execution.id, 'failed');
1390
+ throw new Error(result.error || 'Unknown error');
1391
+ }
1392
+ }
1393
+ }