@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.
- package/README.md +510 -255
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +23 -0
- package/dist/commands/action/create.d.ts +21 -0
- package/dist/commands/action/create.js +126 -0
- package/dist/commands/action/delete.d.ts +17 -0
- package/dist/commands/action/delete.js +78 -0
- package/dist/commands/action/index.d.ts +15 -0
- package/dist/commands/action/index.js +107 -0
- package/dist/commands/action/list.d.ts +14 -0
- package/dist/commands/action/list.js +89 -0
- package/dist/commands/action/run.d.ts +19 -0
- package/dist/commands/action/run.js +179 -0
- package/dist/commands/action/show.d.ts +15 -0
- package/dist/commands/action/show.js +47 -0
- package/dist/commands/action/update.d.ts +22 -0
- package/dist/commands/action/update.js +168 -0
- package/dist/commands/agent/index.d.ts +13 -0
- package/dist/commands/agent/index.js +131 -0
- package/dist/commands/agent/list.d.ts +7 -0
- package/dist/commands/agent/list.js +126 -0
- package/dist/commands/agent/login.d.ts +16 -0
- package/dist/commands/agent/login.js +146 -0
- package/dist/commands/agent/rebuild.d.ts +18 -0
- package/dist/commands/agent/rebuild.js +133 -0
- package/dist/commands/agent/restart.d.ts +17 -0
- package/dist/commands/agent/restart.js +116 -0
- package/dist/commands/agent/shell.d.ts +23 -0
- package/dist/commands/agent/shell.js +378 -0
- package/dist/commands/agent/staff/add.d.ts +15 -0
- package/dist/commands/agent/staff/add.js +281 -0
- package/dist/commands/agent/staff/index.d.ts +14 -0
- package/dist/commands/agent/staff/index.js +90 -0
- package/dist/commands/agent/staff/list.d.ts +7 -0
- package/dist/commands/agent/staff/list.js +90 -0
- package/dist/commands/agent/staff/remove.d.ts +16 -0
- package/dist/commands/agent/staff/remove.js +137 -0
- package/dist/commands/agent/status.d.ts +17 -0
- package/dist/commands/agent/status.js +139 -0
- package/dist/commands/agent/temp/cleanup.d.ts +23 -0
- package/dist/commands/agent/temp/cleanup.js +388 -0
- package/dist/commands/agent/temp/index.d.ts +14 -0
- package/dist/commands/agent/temp/index.js +82 -0
- package/dist/commands/agent/temp/list.d.ts +7 -0
- package/dist/commands/agent/temp/list.js +108 -0
- package/dist/commands/agent/themes/add-names.d.ts +10 -0
- package/dist/commands/agent/themes/add-names.js +67 -0
- package/dist/commands/agent/themes/create.d.ts +13 -0
- package/dist/commands/agent/themes/create.js +66 -0
- package/dist/commands/agent/themes/index.d.ts +9 -0
- package/dist/commands/agent/themes/index.js +194 -0
- package/dist/commands/agent/themes/list.d.ts +6 -0
- package/dist/commands/agent/themes/list.js +41 -0
- package/dist/commands/agent/themes/set.d.ts +12 -0
- package/dist/commands/agent/themes/set.js +77 -0
- package/dist/commands/agent/visit.d.ts +16 -0
- package/dist/commands/agent/visit.js +88 -0
- package/dist/commands/autocomplete/setup.d.ts +14 -0
- package/dist/commands/autocomplete/setup.js +154 -0
- package/dist/commands/board/index.d.ts +17 -0
- package/dist/commands/board/index.js +255 -0
- package/dist/commands/board/watch.d.ts +13 -0
- package/dist/commands/board/watch.js +52 -0
- package/dist/commands/branch/create.d.ts +50 -0
- package/dist/commands/branch/create.js +624 -0
- package/dist/commands/branch/index.d.ts +13 -0
- package/dist/commands/branch/index.js +50 -0
- package/dist/commands/branch/list.d.ts +17 -0
- package/dist/commands/branch/list.js +120 -0
- package/dist/commands/branch/validate.d.ts +15 -0
- package/dist/commands/branch/validate.js +73 -0
- package/dist/commands/commit.d.ts +71 -0
- package/dist/commands/commit.js +499 -0
- package/dist/commands/docker/clean.d.ts +13 -0
- package/dist/commands/docker/clean.js +224 -0
- package/dist/commands/docker/index.d.ts +19 -0
- package/dist/commands/docker/index.js +274 -0
- package/dist/commands/docker/list.d.ts +16 -0
- package/dist/commands/docker/list.js +200 -0
- package/dist/commands/docker/logs.d.ts +14 -0
- package/dist/commands/docker/logs.js +118 -0
- package/dist/commands/docker/prune.d.ts +14 -0
- package/dist/commands/docker/prune.js +211 -0
- package/dist/commands/docker/restart.d.ts +14 -0
- package/dist/commands/docker/restart.js +129 -0
- package/dist/commands/docker/shell.d.ts +14 -0
- package/dist/commands/docker/shell.js +103 -0
- package/dist/commands/docker/start.d.ts +12 -0
- package/dist/commands/docker/start.js +92 -0
- package/dist/commands/docker/status.d.ts +7 -0
- package/dist/commands/docker/status.js +40 -0
- package/dist/commands/docker/stop.d.ts +14 -0
- package/dist/commands/docker/stop.js +134 -0
- package/dist/commands/docker/sync.d.ts +15 -0
- package/dist/commands/docker/sync.js +112 -0
- package/dist/commands/epic/activate.d.ts +13 -0
- package/dist/commands/epic/activate.js +118 -0
- package/dist/commands/epic/archive.d.ts +14 -0
- package/dist/commands/epic/archive.js +132 -0
- package/dist/commands/epic/create.d.ts +15 -0
- package/dist/commands/epic/create.js +137 -0
- package/dist/commands/epic/index.d.ts +13 -0
- package/dist/commands/epic/index.js +88 -0
- package/dist/commands/epic/link/block.d.ts +14 -0
- package/dist/commands/epic/link/block.js +79 -0
- package/dist/commands/epic/link/duplicates.d.ts +14 -0
- package/dist/commands/epic/link/duplicates.js +66 -0
- package/dist/commands/epic/link/index.d.ts +19 -0
- package/dist/commands/epic/link/index.js +242 -0
- package/dist/commands/epic/link/relates.d.ts +14 -0
- package/dist/commands/epic/link/relates.js +66 -0
- package/dist/commands/epic/link/remove.d.ts +16 -0
- package/dist/commands/epic/link/remove.js +89 -0
- package/dist/commands/epic/list.d.ts +11 -0
- package/dist/commands/epic/list.js +87 -0
- package/dist/commands/epic/move.d.ts +15 -0
- package/dist/commands/epic/move.js +184 -0
- package/dist/commands/epic/progress.d.ts +16 -0
- package/dist/commands/epic/progress.js +166 -0
- package/dist/commands/epic/project.d.ts +15 -0
- package/dist/commands/epic/project.js +219 -0
- package/dist/commands/epic/reorder.d.ts +21 -0
- package/dist/commands/epic/reorder.js +160 -0
- package/dist/commands/epic/spec.d.ts +15 -0
- package/dist/commands/epic/spec.js +191 -0
- package/dist/commands/epic/ticket.d.ts +18 -0
- package/dist/commands/epic/ticket.js +291 -0
- package/dist/commands/epic/view.d.ts +13 -0
- package/dist/commands/epic/view.js +117 -0
- package/dist/commands/execution/index.d.ts +13 -0
- package/dist/commands/execution/index.js +70 -0
- package/dist/commands/execution/list.d.ts +15 -0
- package/dist/commands/execution/list.js +144 -0
- package/dist/commands/execution/logs.d.ts +18 -0
- package/dist/commands/execution/logs.js +161 -0
- package/dist/commands/execution/stop.d.ts +22 -0
- package/dist/commands/execution/stop.js +248 -0
- package/dist/commands/gh/index.d.ts +9 -0
- package/dist/commands/gh/index.js +53 -0
- package/dist/commands/gh/login.d.ts +6 -0
- package/dist/commands/gh/login.js +57 -0
- package/dist/commands/gh/status.d.ts +6 -0
- package/dist/commands/gh/status.js +48 -0
- package/dist/commands/gh/token.d.ts +6 -0
- package/dist/commands/gh/token.js +59 -0
- package/dist/commands/init.d.ts +26 -0
- package/dist/commands/init.js +200 -0
- package/dist/commands/phase/create.d.ts +22 -0
- package/dist/commands/phase/create.js +123 -0
- package/dist/commands/phase/delete.d.ts +17 -0
- package/dist/commands/phase/delete.js +73 -0
- package/dist/commands/phase/list.d.ts +12 -0
- package/dist/commands/phase/list.js +76 -0
- package/dist/commands/phase/move.d.ts +17 -0
- package/dist/commands/phase/move.js +115 -0
- package/dist/commands/phase/template/apply.d.ts +17 -0
- package/dist/commands/phase/template/apply.js +106 -0
- package/dist/commands/phase/template/create.d.ts +16 -0
- package/dist/commands/phase/template/create.js +58 -0
- package/dist/commands/phase/template/delete.d.ts +17 -0
- package/dist/commands/phase/template/delete.js +98 -0
- package/dist/commands/phase/template/index.d.ts +15 -0
- package/dist/commands/phase/template/index.js +128 -0
- package/dist/commands/phase/template/list.d.ts +16 -0
- package/dist/commands/phase/template/list.js +95 -0
- package/dist/commands/phase/template/update.d.ts +17 -0
- package/dist/commands/phase/template/update.js +89 -0
- package/dist/commands/phase/update.d.ts +23 -0
- package/dist/commands/phase/update.js +174 -0
- package/dist/commands/pmo/init.d.ts +25 -0
- package/dist/commands/pmo/init.js +341 -0
- package/dist/commands/pr/create.d.ts +17 -0
- package/dist/commands/pr/create.js +242 -0
- package/dist/commands/pr/index.d.ts +9 -0
- package/dist/commands/pr/index.js +68 -0
- package/dist/commands/pr/link.d.ts +14 -0
- package/dist/commands/pr/link.js +212 -0
- package/dist/commands/pr/status.d.ts +12 -0
- package/dist/commands/pr/status.js +161 -0
- package/dist/commands/project/archive.d.ts +17 -0
- package/dist/commands/project/archive.js +83 -0
- package/dist/commands/project/create.d.ts +22 -0
- package/dist/commands/project/create.js +143 -0
- package/dist/commands/project/delete.d.ts +17 -0
- package/dist/commands/project/delete.js +128 -0
- package/dist/commands/project/index.d.ts +13 -0
- package/dist/commands/project/index.js +64 -0
- package/dist/commands/project/list.d.ts +14 -0
- package/dist/commands/project/list.js +96 -0
- package/dist/commands/project/spec.d.ts +18 -0
- package/dist/commands/project/spec.js +216 -0
- package/dist/commands/project/unarchive.d.ts +15 -0
- package/dist/commands/project/unarchive.js +35 -0
- package/dist/commands/project/view.d.ts +16 -0
- package/dist/commands/project/view.js +94 -0
- package/dist/commands/repo/add.d.ts +21 -0
- package/dist/commands/repo/add.js +118 -0
- package/dist/commands/repo/index.d.ts +13 -0
- package/dist/commands/repo/index.js +114 -0
- package/dist/commands/repo/list.d.ts +13 -0
- package/dist/commands/repo/list.js +96 -0
- package/dist/commands/repo/remove.d.ts +23 -0
- package/dist/commands/repo/remove.js +217 -0
- package/dist/commands/repo/view.d.ts +15 -0
- package/dist/commands/repo/view.js +99 -0
- package/dist/commands/session/attach.d.ts +40 -0
- package/dist/commands/session/attach.js +307 -0
- package/dist/commands/session/index.d.ts +13 -0
- package/dist/commands/session/index.js +64 -0
- package/dist/commands/session/list.d.ts +21 -0
- package/dist/commands/session/list.js +181 -0
- package/dist/commands/spec/create.d.ts +19 -0
- package/dist/commands/spec/create.js +130 -0
- package/dist/commands/spec/index.d.ts +13 -0
- package/dist/commands/spec/index.js +68 -0
- package/dist/commands/spec/link/depends.d.ts +14 -0
- package/dist/commands/spec/link/depends.js +64 -0
- package/dist/commands/spec/link/duplicates.d.ts +14 -0
- package/dist/commands/spec/link/duplicates.js +63 -0
- package/dist/commands/spec/link/index.d.ts +19 -0
- package/dist/commands/spec/link/index.js +200 -0
- package/dist/commands/spec/link/relates.d.ts +14 -0
- package/dist/commands/spec/link/relates.js +63 -0
- package/dist/commands/spec/link/remove.d.ts +16 -0
- package/dist/commands/spec/link/remove.js +94 -0
- package/dist/commands/spec/list.d.ts +12 -0
- package/dist/commands/spec/list.js +75 -0
- package/dist/commands/spec/plan.d.ts +15 -0
- package/dist/commands/spec/plan.js +108 -0
- package/dist/commands/spec/ticket.d.ts +18 -0
- package/dist/commands/spec/ticket.js +160 -0
- package/dist/commands/spec/view.d.ts +15 -0
- package/dist/commands/spec/view.js +163 -0
- package/dist/commands/status/create.d.ts +21 -0
- package/dist/commands/status/create.js +140 -0
- package/dist/commands/status/delete.d.ts +13 -0
- package/dist/commands/status/delete.js +77 -0
- package/dist/commands/status/index.d.ts +14 -0
- package/dist/commands/status/index.js +91 -0
- package/dist/commands/status/list.d.ts +12 -0
- package/dist/commands/status/list.js +93 -0
- package/dist/commands/status/move.d.ts +14 -0
- package/dist/commands/status/move.js +120 -0
- package/dist/commands/status/update.d.ts +20 -0
- package/dist/commands/status/update.js +180 -0
- package/dist/commands/template/delete.d.ts +15 -0
- package/dist/commands/template/delete.js +142 -0
- package/dist/commands/template/index.d.ts +10 -0
- package/dist/commands/template/index.js +64 -0
- package/dist/commands/template/list.d.ts +18 -0
- package/dist/commands/template/list.js +157 -0
- package/dist/commands/template/phase/apply.d.ts +14 -0
- package/dist/commands/template/phase/apply.js +41 -0
- package/dist/commands/template/phase/create.d.ts +12 -0
- package/dist/commands/template/phase/create.js +29 -0
- package/dist/commands/template/phase/delete.d.ts +13 -0
- package/dist/commands/template/phase/delete.js +34 -0
- package/dist/commands/template/phase/index.d.ts +10 -0
- package/dist/commands/template/phase/index.js +62 -0
- package/dist/commands/template/phase/list.d.ts +11 -0
- package/dist/commands/template/phase/list.js +34 -0
- package/dist/commands/template/phase/update.d.ts +13 -0
- package/dist/commands/template/phase/update.js +35 -0
- package/dist/commands/template/ticket/apply.d.ts +17 -0
- package/dist/commands/template/ticket/apply.js +58 -0
- package/dist/commands/template/ticket/delete.d.ts +13 -0
- package/dist/commands/template/ticket/delete.js +34 -0
- package/dist/commands/template/ticket/index.d.ts +10 -0
- package/dist/commands/template/ticket/index.js +62 -0
- package/dist/commands/template/ticket/list.d.ts +11 -0
- package/dist/commands/template/ticket/list.js +34 -0
- package/dist/commands/template/ticket/save.d.ts +13 -0
- package/dist/commands/template/ticket/save.js +35 -0
- package/dist/commands/ticket/bulk.d.ts +13 -0
- package/dist/commands/ticket/bulk.js +145 -0
- package/dist/commands/ticket/complete.d.ts +16 -0
- package/dist/commands/ticket/complete.js +170 -0
- package/dist/commands/ticket/create.d.ts +22 -0
- package/dist/commands/ticket/create.js +390 -0
- package/dist/commands/ticket/delete.d.ts +16 -0
- package/dist/commands/ticket/delete.js +178 -0
- package/dist/commands/ticket/edit.d.ts +27 -0
- package/dist/commands/ticket/edit.js +322 -0
- package/dist/commands/ticket/epic.d.ts +20 -0
- package/dist/commands/ticket/epic.js +333 -0
- package/dist/commands/ticket/index.d.ts +13 -0
- package/dist/commands/ticket/index.js +103 -0
- package/dist/commands/ticket/link/block.d.ts +14 -0
- package/dist/commands/ticket/link/block.js +94 -0
- package/dist/commands/ticket/link/duplicates.d.ts +14 -0
- package/dist/commands/ticket/link/duplicates.js +93 -0
- package/dist/commands/ticket/link/index.d.ts +19 -0
- package/dist/commands/ticket/link/index.js +239 -0
- package/dist/commands/ticket/link/relates.d.ts +14 -0
- package/dist/commands/ticket/link/relates.js +93 -0
- package/dist/commands/ticket/link/remove.d.ts +16 -0
- package/dist/commands/ticket/link/remove.js +128 -0
- package/dist/commands/ticket/list.d.ts +24 -0
- package/dist/commands/ticket/list.js +431 -0
- package/dist/commands/ticket/move.d.ts +18 -0
- package/dist/commands/ticket/move.js +212 -0
- package/dist/commands/ticket/project.d.ts +18 -0
- package/dist/commands/ticket/project.js +254 -0
- package/dist/commands/ticket/reassign.d.ts +19 -0
- package/dist/commands/ticket/reassign.js +279 -0
- package/dist/commands/ticket/spec.d.ts +18 -0
- package/dist/commands/ticket/spec.js +259 -0
- package/dist/commands/ticket/status.d.ts +13 -0
- package/dist/commands/ticket/status.js +87 -0
- package/dist/commands/ticket/template/apply.d.ts +25 -0
- package/dist/commands/ticket/template/apply.js +249 -0
- package/dist/commands/ticket/template/create.d.ts +19 -0
- package/dist/commands/ticket/template/create.js +210 -0
- package/dist/commands/ticket/template/delete.d.ts +17 -0
- package/dist/commands/ticket/template/delete.js +92 -0
- package/dist/commands/ticket/template/index.d.ts +15 -0
- package/dist/commands/ticket/template/index.js +118 -0
- package/dist/commands/ticket/template/list.d.ts +16 -0
- package/dist/commands/ticket/template/list.js +110 -0
- package/dist/commands/ticket/template/save.d.ts +14 -0
- package/dist/commands/ticket/template/save.js +110 -0
- package/dist/commands/ticket/update.d.ts +18 -0
- package/dist/commands/ticket/update.js +325 -0
- package/dist/commands/ticket/view.d.ts +13 -0
- package/dist/commands/ticket/view.js +80 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +103 -0
- package/dist/commands/work/complete.d.ts +13 -0
- package/dist/commands/work/complete.js +121 -0
- package/dist/commands/work/index.d.ts +13 -0
- package/dist/commands/work/index.js +70 -0
- package/dist/commands/work/ready.d.ts +24 -0
- package/dist/commands/work/ready.js +290 -0
- package/dist/commands/work/revise.d.ts +19 -0
- package/dist/commands/work/revise.js +377 -0
- package/dist/commands/work/spawn-all.d.ts +17 -0
- package/dist/commands/work/spawn-all.js +58 -0
- package/dist/commands/work/spawn.d.ts +29 -0
- package/dist/commands/work/spawn.js +728 -0
- package/dist/commands/work/start.d.ts +39 -0
- package/dist/commands/work/start.js +1393 -0
- package/dist/commands/work/watch.d.ts +31 -0
- package/dist/commands/work/watch.js +359 -0
- package/dist/commands/workflow/create.d.ts +18 -0
- package/dist/commands/workflow/create.js +119 -0
- package/dist/commands/workflow/delete.d.ts +17 -0
- package/dist/commands/workflow/delete.js +119 -0
- package/dist/commands/workflow/index.d.ts +15 -0
- package/dist/commands/workflow/index.js +75 -0
- package/dist/commands/workflow/list.d.ts +15 -0
- package/dist/commands/workflow/list.js +75 -0
- package/dist/commands/workflow/switch.d.ts +13 -0
- package/dist/commands/workflow/switch.js +117 -0
- package/dist/commands/workflow/view.d.ts +16 -0
- package/dist/commands/workflow/view.js +114 -0
- package/dist/commands/workspace/add.d.ts +12 -0
- package/dist/commands/workspace/add.js +74 -0
- package/dist/commands/workspace/list.d.ts +9 -0
- package/dist/commands/workspace/list.js +153 -0
- package/dist/commands/workspace/remove.d.ts +13 -0
- package/dist/commands/workspace/remove.js +98 -0
- package/dist/commands/workspace/use.d.ts +12 -0
- package/dist/commands/workspace/use.js +111 -0
- package/dist/hooks/init.d.ts +11 -0
- package/dist/hooks/init.js +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/agents/commands.d.ts +189 -0
- package/dist/lib/agents/commands.js +893 -0
- package/dist/lib/agents/index.d.ts +54 -0
- package/dist/lib/agents/index.js +382 -0
- package/dist/lib/branch/index.d.ts +120 -0
- package/dist/lib/branch/index.js +334 -0
- package/dist/lib/colors.d.ts +94 -0
- package/dist/lib/colors.js +68 -0
- package/dist/lib/commands/docker-command.d.ts +21 -0
- package/dist/lib/commands/docker-command.js +27 -0
- package/dist/lib/database/index.d.ts +176 -0
- package/dist/lib/database/index.js +581 -0
- package/dist/lib/docker/resolve.d.ts +38 -0
- package/dist/lib/docker/resolve.js +175 -0
- package/dist/lib/execution/config.d.ts +150 -0
- package/dist/lib/execution/config.js +541 -0
- package/dist/lib/execution/devcontainer.d.ts +85 -0
- package/dist/lib/execution/devcontainer.js +594 -0
- package/dist/lib/execution/index.d.ts +10 -0
- package/dist/lib/execution/index.js +10 -0
- package/dist/lib/execution/runners.d.ts +53 -0
- package/dist/lib/execution/runners.js +1182 -0
- package/dist/lib/execution/spawner.d.ts +85 -0
- package/dist/lib/execution/spawner.js +548 -0
- package/dist/lib/execution/storage.d.ts +159 -0
- package/dist/lib/execution/storage.js +425 -0
- package/dist/lib/execution/types.d.ts +145 -0
- package/dist/lib/execution/types.js +157 -0
- package/dist/lib/init/index.d.ts +75 -0
- package/dist/lib/init/index.js +355 -0
- package/dist/lib/machine-config.d.ts +170 -0
- package/dist/lib/machine-config.js +386 -0
- package/dist/lib/pmo/base-command.d.ts +195 -0
- package/dist/lib/pmo/base-command.js +319 -0
- package/dist/lib/pmo/create-spec-folders.d.ts +43 -0
- package/dist/lib/pmo/create-spec-folders.js +64 -0
- package/dist/lib/pmo/epic-files.d.ts +56 -0
- package/dist/lib/pmo/epic-files.js +195 -0
- package/dist/lib/pmo/find-pmo.d.ts +14 -0
- package/dist/lib/pmo/find-pmo.js +172 -0
- package/dist/lib/pmo/index.d.ts +109 -0
- package/dist/lib/pmo/index.js +501 -0
- package/dist/lib/pmo/markdown.d.ts +31 -0
- package/dist/lib/pmo/markdown.js +245 -0
- package/dist/lib/pmo/pmo-context.d.ts +27 -0
- package/dist/lib/pmo/pmo-context.js +44 -0
- package/dist/lib/pmo/schema.d.ts +82 -0
- package/dist/lib/pmo/schema.js +531 -0
- package/dist/lib/pmo/spec-parser.d.ts +25 -0
- package/dist/lib/pmo/spec-parser.js +205 -0
- package/dist/lib/pmo/spec-types.d.ts +43 -0
- package/dist/lib/pmo/spec-types.js +7 -0
- package/dist/lib/pmo/storage/actions.d.ts +34 -0
- package/dist/lib/pmo/storage/actions.js +177 -0
- package/dist/lib/pmo/storage/base.d.ts +47 -0
- package/dist/lib/pmo/storage/base.js +858 -0
- package/dist/lib/pmo/storage/dependencies.d.ts +61 -0
- package/dist/lib/pmo/storage/dependencies.js +267 -0
- package/dist/lib/pmo/storage/epics.d.ts +46 -0
- package/dist/lib/pmo/storage/epics.js +243 -0
- package/dist/lib/pmo/storage/helpers.d.ts +33 -0
- package/dist/lib/pmo/storage/helpers.js +148 -0
- package/dist/lib/pmo/storage/index.d.ts +186 -0
- package/dist/lib/pmo/storage/index.js +689 -0
- package/dist/lib/pmo/storage/phases.d.ts +65 -0
- package/dist/lib/pmo/storage/phases.js +392 -0
- package/dist/lib/pmo/storage/projects.d.ts +79 -0
- package/dist/lib/pmo/storage/projects.js +303 -0
- package/dist/lib/pmo/storage/specs.d.ts +77 -0
- package/dist/lib/pmo/storage/specs.js +389 -0
- package/dist/lib/pmo/storage/statuses.d.ts +63 -0
- package/dist/lib/pmo/storage/statuses.js +404 -0
- package/dist/lib/pmo/storage/subtasks.d.ts +37 -0
- package/dist/lib/pmo/storage/subtasks.js +184 -0
- package/dist/lib/pmo/storage/templates.d.ts +40 -0
- package/dist/lib/pmo/storage/templates.js +210 -0
- package/dist/lib/pmo/storage/tickets.d.ts +57 -0
- package/dist/lib/pmo/storage/tickets.js +453 -0
- package/dist/lib/pmo/storage/types.d.ts +200 -0
- package/dist/lib/pmo/storage/types.js +5 -0
- package/dist/lib/pmo/storage/views.d.ts +44 -0
- package/dist/lib/pmo/storage/views.js +355 -0
- package/dist/lib/pmo/storage-sqlite.d.ts +7 -0
- package/dist/lib/pmo/storage-sqlite.js +7 -0
- package/dist/lib/pmo/sync-manager.d.ts +92 -0
- package/dist/lib/pmo/sync-manager.js +229 -0
- package/dist/lib/pmo/types.d.ts +710 -0
- package/dist/lib/pmo/types.js +108 -0
- package/dist/lib/pmo/utils.d.ts +122 -0
- package/dist/lib/pmo/utils.js +174 -0
- package/dist/lib/pmo/watcher.d.ts +43 -0
- package/dist/lib/pmo/watcher.js +208 -0
- package/dist/lib/pr/index.d.ts +150 -0
- package/dist/lib/pr/index.js +483 -0
- package/dist/lib/prompt-json.d.ts +231 -0
- package/dist/lib/prompt-json.js +213 -0
- package/dist/lib/repos/index.d.ts +81 -0
- package/dist/lib/repos/index.js +679 -0
- package/dist/lib/styles.d.ts +98 -0
- package/dist/lib/styles.js +195 -0
- package/dist/lib/themes.d.ts +128 -0
- package/dist/lib/themes.js +301 -0
- package/dist/lib/ui/BoardUI.d.ts +21 -0
- package/dist/lib/ui/BoardUI.js +85 -0
- package/dist/lib/ui/ClaimTicketUI.d.ts +17 -0
- package/dist/lib/ui/ClaimTicketUI.js +64 -0
- package/dist/lib/ui/CreateTicketUI.d.ts +13 -0
- package/dist/lib/ui/CreateTicketUI.js +101 -0
- package/dist/lib/workspace.d.ts +66 -0
- package/dist/lib/workspace.js +204 -0
- package/oclif.manifest.json +10593 -0
- package/package.json +104 -52
- package/LICENSE +0 -21
- package/dist/bin/prlt.d.ts +0 -11
- package/dist/bin/prlt.d.ts.map +0 -1
- package/dist/bin/prlt.js +0 -144
- package/dist/bin/prlt.js.map +0 -1
- package/dist/lib/config/index.d.ts +0 -14
- package/dist/lib/config/index.d.ts.map +0 -1
- package/dist/lib/config/index.js +0 -139
- package/dist/lib/config/index.js.map +0 -1
- package/dist/lib/config/upgrade.d.ts +0 -2
- package/dist/lib/config/upgrade.d.ts.map +0 -1
- package/dist/lib/config/upgrade.js +0 -173
- package/dist/lib/config/upgrade.js.map +0 -1
- package/dist/lib/themes/index.d.ts +0 -8
- package/dist/lib/themes/index.d.ts.map +0 -1
- package/dist/lib/themes/index.js +0 -80
- package/dist/lib/themes/index.js.map +0 -1
- package/dist/lib/utils/helpers.d.ts +0 -4
- package/dist/lib/utils/helpers.d.ts.map +0 -1
- package/dist/lib/utils/helpers.js +0 -39
- package/dist/lib/utils/helpers.js.map +0 -1
- package/dist/lib/utils/logger.d.ts +0 -4
- package/dist/lib/utils/logger.d.ts.map +0 -1
- package/dist/lib/utils/logger.js +0 -28
- package/dist/lib/utils/logger.js.map +0 -1
- package/dist/lib/workspace/index.d.ts +0 -13
- package/dist/lib/workspace/index.d.ts.map +0 -1
- package/dist/lib/workspace/index.js +0 -116
- package/dist/lib/workspace/index.js.map +0 -1
- package/dist/lib/worktree/index.d.ts +0 -7
- package/dist/lib/worktree/index.d.ts.map +0 -1
- package/dist/lib/worktree/index.js +0 -362
- package/dist/lib/worktree/index.js.map +0 -1
- package/dist/lib/worktree/migrate.d.ts +0 -2
- package/dist/lib/worktree/migrate.d.ts.map +0 -1
- package/dist/lib/worktree/migrate.js +0 -212
- package/dist/lib/worktree/migrate.js.map +0 -1
- package/dist/lib/worktree/repair.d.ts +0 -3
- package/dist/lib/worktree/repair.d.ts.map +0 -1
- package/dist/lib/worktree/repair.js +0 -140
- package/dist/lib/worktree/repair.js.map +0 -1
- package/dist/types/index.d.ts +0 -57
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- 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
|
+
}
|