erk 0.4.5__py3-none-any.whl

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 (331) hide show
  1. erk/__init__.py +12 -0
  2. erk/__main__.py +6 -0
  3. erk/agent_docs/__init__.py +5 -0
  4. erk/agent_docs/models.py +123 -0
  5. erk/agent_docs/operations.py +666 -0
  6. erk/artifacts/__init__.py +5 -0
  7. erk/artifacts/artifact_health.py +623 -0
  8. erk/artifacts/detection.py +16 -0
  9. erk/artifacts/discovery.py +343 -0
  10. erk/artifacts/models.py +63 -0
  11. erk/artifacts/staleness.py +56 -0
  12. erk/artifacts/state.py +100 -0
  13. erk/artifacts/sync.py +624 -0
  14. erk/cli/__init__.py +0 -0
  15. erk/cli/activation.py +132 -0
  16. erk/cli/alias.py +53 -0
  17. erk/cli/cli.py +221 -0
  18. erk/cli/commands/__init__.py +0 -0
  19. erk/cli/commands/admin.py +153 -0
  20. erk/cli/commands/artifact/__init__.py +1 -0
  21. erk/cli/commands/artifact/check.py +260 -0
  22. erk/cli/commands/artifact/group.py +31 -0
  23. erk/cli/commands/artifact/list_cmd.py +89 -0
  24. erk/cli/commands/artifact/show.py +62 -0
  25. erk/cli/commands/artifact/sync_cmd.py +39 -0
  26. erk/cli/commands/branch/__init__.py +26 -0
  27. erk/cli/commands/branch/assign_cmd.py +152 -0
  28. erk/cli/commands/branch/checkout_cmd.py +357 -0
  29. erk/cli/commands/branch/create_cmd.py +161 -0
  30. erk/cli/commands/branch/list_cmd.py +82 -0
  31. erk/cli/commands/branch/unassign_cmd.py +197 -0
  32. erk/cli/commands/cc/__init__.py +15 -0
  33. erk/cli/commands/cc/jsonl_cmd.py +20 -0
  34. erk/cli/commands/cc/session/AGENTS.md +30 -0
  35. erk/cli/commands/cc/session/CLAUDE.md +1 -0
  36. erk/cli/commands/cc/session/__init__.py +15 -0
  37. erk/cli/commands/cc/session/list_cmd.py +167 -0
  38. erk/cli/commands/cc/session/show_cmd.py +175 -0
  39. erk/cli/commands/completion.py +89 -0
  40. erk/cli/commands/completions.py +165 -0
  41. erk/cli/commands/config.py +327 -0
  42. erk/cli/commands/docs/__init__.py +1 -0
  43. erk/cli/commands/docs/group.py +16 -0
  44. erk/cli/commands/docs/sync.py +121 -0
  45. erk/cli/commands/docs/validate.py +102 -0
  46. erk/cli/commands/doctor.py +243 -0
  47. erk/cli/commands/down.py +171 -0
  48. erk/cli/commands/exec/__init__.py +1 -0
  49. erk/cli/commands/exec/group.py +164 -0
  50. erk/cli/commands/exec/scripts/AGENTS.md +79 -0
  51. erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
  52. erk/cli/commands/exec/scripts/__init__.py +5 -0
  53. erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
  54. erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
  55. erk/cli/commands/exec/scripts/check_impl.py +152 -0
  56. erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
  57. erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
  58. erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
  59. erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
  60. erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
  61. erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
  62. erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
  63. erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
  64. erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
  65. erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
  66. erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
  67. erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
  68. erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
  69. erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
  70. erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
  71. erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
  72. erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
  73. erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
  74. erk/cli/commands/exec/scripts/impl_init.py +158 -0
  75. erk/cli/commands/exec/scripts/impl_signal.py +375 -0
  76. erk/cli/commands/exec/scripts/impl_verify.py +49 -0
  77. erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
  78. erk/cli/commands/exec/scripts/list_sessions.py +296 -0
  79. erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
  80. erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
  81. erk/cli/commands/exec/scripts/marker.py +163 -0
  82. erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
  83. erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
  84. erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
  85. erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
  86. erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
  87. erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
  88. erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
  89. erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
  90. erk/cli/commands/exec/scripts/quick_submit.py +32 -0
  91. erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
  92. erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
  93. erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
  94. erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
  95. erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
  96. erk/cli/commands/exec/scripts/slot_objective.py +102 -0
  97. erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
  98. erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
  99. erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
  100. erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
  101. erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
  102. erk/cli/commands/implement.py +695 -0
  103. erk/cli/commands/implement_shared.py +649 -0
  104. erk/cli/commands/info/__init__.py +14 -0
  105. erk/cli/commands/info/release_notes_cmd.py +128 -0
  106. erk/cli/commands/init.py +801 -0
  107. erk/cli/commands/land_cmd.py +690 -0
  108. erk/cli/commands/log_cmd.py +137 -0
  109. erk/cli/commands/md/__init__.py +5 -0
  110. erk/cli/commands/md/check.py +118 -0
  111. erk/cli/commands/md/group.py +14 -0
  112. erk/cli/commands/navigation_helpers.py +430 -0
  113. erk/cli/commands/objective/__init__.py +16 -0
  114. erk/cli/commands/objective/list_cmd.py +47 -0
  115. erk/cli/commands/objective_helpers.py +132 -0
  116. erk/cli/commands/plan/__init__.py +32 -0
  117. erk/cli/commands/plan/check_cmd.py +174 -0
  118. erk/cli/commands/plan/close_cmd.py +69 -0
  119. erk/cli/commands/plan/create_cmd.py +120 -0
  120. erk/cli/commands/plan/docs/__init__.py +18 -0
  121. erk/cli/commands/plan/docs/extract_cmd.py +53 -0
  122. erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
  123. erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
  124. erk/cli/commands/plan/extraction/__init__.py +16 -0
  125. erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
  126. erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
  127. erk/cli/commands/plan/get.py +71 -0
  128. erk/cli/commands/plan/list_cmd.py +754 -0
  129. erk/cli/commands/plan/log_cmd.py +440 -0
  130. erk/cli/commands/plan/start_cmd.py +459 -0
  131. erk/cli/commands/planner/__init__.py +40 -0
  132. erk/cli/commands/planner/configure_cmd.py +73 -0
  133. erk/cli/commands/planner/connect_cmd.py +96 -0
  134. erk/cli/commands/planner/create_cmd.py +148 -0
  135. erk/cli/commands/planner/list_cmd.py +51 -0
  136. erk/cli/commands/planner/register_cmd.py +105 -0
  137. erk/cli/commands/planner/set_default_cmd.py +23 -0
  138. erk/cli/commands/planner/unregister_cmd.py +43 -0
  139. erk/cli/commands/pr/__init__.py +23 -0
  140. erk/cli/commands/pr/check_cmd.py +112 -0
  141. erk/cli/commands/pr/checkout_cmd.py +165 -0
  142. erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
  143. erk/cli/commands/pr/parse_pr_reference.py +10 -0
  144. erk/cli/commands/pr/submit_cmd.py +360 -0
  145. erk/cli/commands/pr/sync_cmd.py +181 -0
  146. erk/cli/commands/prepare_cwd_recovery.py +60 -0
  147. erk/cli/commands/project/__init__.py +16 -0
  148. erk/cli/commands/project/init_cmd.py +91 -0
  149. erk/cli/commands/run/__init__.py +17 -0
  150. erk/cli/commands/run/list_cmd.py +189 -0
  151. erk/cli/commands/run/logs_cmd.py +54 -0
  152. erk/cli/commands/run/shared.py +19 -0
  153. erk/cli/commands/shell_integration.py +29 -0
  154. erk/cli/commands/slot/__init__.py +23 -0
  155. erk/cli/commands/slot/check_cmd.py +277 -0
  156. erk/cli/commands/slot/common.py +314 -0
  157. erk/cli/commands/slot/init_pool_cmd.py +157 -0
  158. erk/cli/commands/slot/list_cmd.py +228 -0
  159. erk/cli/commands/slot/repair_cmd.py +190 -0
  160. erk/cli/commands/stack/__init__.py +23 -0
  161. erk/cli/commands/stack/consolidate_cmd.py +470 -0
  162. erk/cli/commands/stack/list_cmd.py +79 -0
  163. erk/cli/commands/stack/move_cmd.py +309 -0
  164. erk/cli/commands/stack/split_old/README.md +64 -0
  165. erk/cli/commands/stack/split_old/__init__.py +5 -0
  166. erk/cli/commands/stack/split_old/command.py +233 -0
  167. erk/cli/commands/stack/split_old/display.py +116 -0
  168. erk/cli/commands/stack/split_old/plan.py +216 -0
  169. erk/cli/commands/status.py +58 -0
  170. erk/cli/commands/submit.py +768 -0
  171. erk/cli/commands/up.py +154 -0
  172. erk/cli/commands/upgrade.py +82 -0
  173. erk/cli/commands/wt/__init__.py +29 -0
  174. erk/cli/commands/wt/checkout_cmd.py +110 -0
  175. erk/cli/commands/wt/create_cmd.py +998 -0
  176. erk/cli/commands/wt/current_cmd.py +35 -0
  177. erk/cli/commands/wt/delete_cmd.py +573 -0
  178. erk/cli/commands/wt/list_cmd.py +332 -0
  179. erk/cli/commands/wt/rename_cmd.py +66 -0
  180. erk/cli/config.py +242 -0
  181. erk/cli/constants.py +29 -0
  182. erk/cli/core.py +65 -0
  183. erk/cli/debug.py +9 -0
  184. erk/cli/ensure-conversion-tasks.md +288 -0
  185. erk/cli/ensure.py +628 -0
  186. erk/cli/github_parsing.py +96 -0
  187. erk/cli/graphite.py +81 -0
  188. erk/cli/graphite_command.py +80 -0
  189. erk/cli/help_formatter.py +345 -0
  190. erk/cli/output.py +361 -0
  191. erk/cli/presets/dagster.toml +12 -0
  192. erk/cli/presets/generic.toml +12 -0
  193. erk/cli/prompt_hooks_templates/README.md +68 -0
  194. erk/cli/script_output.py +32 -0
  195. erk/cli/shell_integration/bash_wrapper.sh +32 -0
  196. erk/cli/shell_integration/fish_wrapper.fish +39 -0
  197. erk/cli/shell_integration/handler.py +338 -0
  198. erk/cli/shell_integration/zsh_wrapper.sh +32 -0
  199. erk/cli/shell_utils.py +171 -0
  200. erk/cli/subprocess_utils.py +92 -0
  201. erk/cli/uvx_detection.py +59 -0
  202. erk/core/__init__.py +0 -0
  203. erk/core/claude_executor.py +511 -0
  204. erk/core/claude_settings.py +317 -0
  205. erk/core/command_log.py +406 -0
  206. erk/core/commit_message_generator.py +234 -0
  207. erk/core/completion.py +10 -0
  208. erk/core/consolidation_utils.py +177 -0
  209. erk/core/context.py +570 -0
  210. erk/core/display/__init__.py +4 -0
  211. erk/core/display/abc.py +24 -0
  212. erk/core/display/real.py +30 -0
  213. erk/core/display_utils.py +526 -0
  214. erk/core/file_utils.py +87 -0
  215. erk/core/health_checks.py +1315 -0
  216. erk/core/health_checks_dogfooder/__init__.py +85 -0
  217. erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
  218. erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
  219. erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
  220. erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
  221. erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
  222. erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
  223. erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
  224. erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
  225. erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
  226. erk/core/implementation_queue/__init__.py +1 -0
  227. erk/core/implementation_queue/github/__init__.py +8 -0
  228. erk/core/implementation_queue/github/abc.py +7 -0
  229. erk/core/implementation_queue/github/noop.py +38 -0
  230. erk/core/implementation_queue/github/printing.py +43 -0
  231. erk/core/implementation_queue/github/real.py +119 -0
  232. erk/core/init_utils.py +227 -0
  233. erk/core/output_filter.py +338 -0
  234. erk/core/plan_store/__init__.py +6 -0
  235. erk/core/planner/__init__.py +1 -0
  236. erk/core/planner/registry_abc.py +8 -0
  237. erk/core/planner/registry_fake.py +129 -0
  238. erk/core/planner/registry_real.py +195 -0
  239. erk/core/planner/types.py +7 -0
  240. erk/core/pr_utils.py +30 -0
  241. erk/core/release_notes.py +263 -0
  242. erk/core/repo_discovery.py +126 -0
  243. erk/core/script_writer.py +41 -0
  244. erk/core/services/__init__.py +1 -0
  245. erk/core/services/plan_list_service.py +94 -0
  246. erk/core/shell.py +51 -0
  247. erk/core/user_feedback.py +11 -0
  248. erk/core/version_check.py +55 -0
  249. erk/core/workflow_display.py +75 -0
  250. erk/core/worktree_pool.py +190 -0
  251. erk/core/worktree_utils.py +300 -0
  252. erk/data/CHANGELOG.md +438 -0
  253. erk/data/__init__.py +1 -0
  254. erk/data/claude/agents/devrun.md +180 -0
  255. erk/data/claude/commands/erk/__init__.py +0 -0
  256. erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
  257. erk/data/claude/commands/erk/fix-conflicts.md +25 -0
  258. erk/data/claude/commands/erk/git-pr-push.md +345 -0
  259. erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
  260. erk/data/claude/commands/erk/land.md +193 -0
  261. erk/data/claude/commands/erk/objective-create.md +370 -0
  262. erk/data/claude/commands/erk/objective-list.md +34 -0
  263. erk/data/claude/commands/erk/objective-next-plan.md +220 -0
  264. erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
  265. erk/data/claude/commands/erk/plan-implement.md +202 -0
  266. erk/data/claude/commands/erk/plan-save.md +45 -0
  267. erk/data/claude/commands/erk/plan-submit.md +39 -0
  268. erk/data/claude/commands/erk/pr-address.md +367 -0
  269. erk/data/claude/commands/erk/pr-submit.md +58 -0
  270. erk/data/claude/skills/dignified-python/SKILL.md +48 -0
  271. erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
  272. erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
  273. erk/data/claude/skills/dignified-python/subprocess.md +99 -0
  274. erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
  275. erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
  276. erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
  277. erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
  278. erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
  279. erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
  280. erk/data/claude/skills/learned-docs/SKILL.md +362 -0
  281. erk/data/github/actions/setup-claude-erk/action.yml +11 -0
  282. erk/data/github/prompts/dignified-python-review.md +125 -0
  283. erk/data/github/workflows/dignified-python-review.yml +61 -0
  284. erk/data/github/workflows/erk-impl.yml +251 -0
  285. erk/hooks/__init__.py +1 -0
  286. erk/hooks/decorators.py +319 -0
  287. erk/status/__init__.py +8 -0
  288. erk/status/collectors/__init__.py +9 -0
  289. erk/status/collectors/base.py +52 -0
  290. erk/status/collectors/git.py +76 -0
  291. erk/status/collectors/github.py +81 -0
  292. erk/status/collectors/graphite.py +80 -0
  293. erk/status/collectors/impl.py +145 -0
  294. erk/status/models/__init__.py +4 -0
  295. erk/status/models/status_data.py +404 -0
  296. erk/status/orchestrator.py +169 -0
  297. erk/status/renderers/__init__.py +5 -0
  298. erk/status/renderers/simple.py +322 -0
  299. erk/tui/AGENTS.md +193 -0
  300. erk/tui/CLAUDE.md +1 -0
  301. erk/tui/__init__.py +1 -0
  302. erk/tui/app.py +1404 -0
  303. erk/tui/commands/__init__.py +1 -0
  304. erk/tui/commands/executor.py +66 -0
  305. erk/tui/commands/provider.py +165 -0
  306. erk/tui/commands/real_executor.py +63 -0
  307. erk/tui/commands/registry.py +121 -0
  308. erk/tui/commands/types.py +36 -0
  309. erk/tui/data/__init__.py +1 -0
  310. erk/tui/data/provider.py +492 -0
  311. erk/tui/data/types.py +104 -0
  312. erk/tui/filtering/__init__.py +1 -0
  313. erk/tui/filtering/logic.py +43 -0
  314. erk/tui/filtering/types.py +55 -0
  315. erk/tui/jsonl_viewer/__init__.py +1 -0
  316. erk/tui/jsonl_viewer/app.py +61 -0
  317. erk/tui/jsonl_viewer/models.py +208 -0
  318. erk/tui/jsonl_viewer/widgets.py +204 -0
  319. erk/tui/sorting/__init__.py +6 -0
  320. erk/tui/sorting/logic.py +55 -0
  321. erk/tui/sorting/types.py +68 -0
  322. erk/tui/styles/dash.tcss +95 -0
  323. erk/tui/widgets/__init__.py +1 -0
  324. erk/tui/widgets/command_output.py +112 -0
  325. erk/tui/widgets/plan_table.py +276 -0
  326. erk/tui/widgets/status_bar.py +116 -0
  327. erk-0.4.5.dist-info/METADATA +376 -0
  328. erk-0.4.5.dist-info/RECORD +331 -0
  329. erk-0.4.5.dist-info/WHEEL +4 -0
  330. erk-0.4.5.dist-info/entry_points.txt +2 -0
  331. erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
@@ -0,0 +1,157 @@
1
+ """Slot init-pool command - proactively initialize pool slots."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.slot.common import (
6
+ generate_slot_name,
7
+ get_placeholder_branch_name,
8
+ get_pool_size,
9
+ is_slot_initialized,
10
+ )
11
+ from erk.cli.core import discover_repo_context
12
+ from erk.core.context import ErkContext, create_context
13
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
14
+ from erk.core.worktree_pool import (
15
+ PoolState,
16
+ SlotInfo,
17
+ load_pool_state,
18
+ save_pool_state,
19
+ )
20
+ from erk_shared.output.output import user_output
21
+
22
+
23
+ @click.command("init-pool")
24
+ @click.option(
25
+ "-n",
26
+ "--count",
27
+ type=int,
28
+ help="Number of slots to initialize. Defaults to pool_size from config.",
29
+ )
30
+ @click.option(
31
+ "--dry-run",
32
+ is_flag=True,
33
+ help="Print what would be done without executing destructive operations.",
34
+ )
35
+ @click.pass_obj
36
+ def slot_init_pool(ctx: ErkContext, count: int | None, *, dry_run: bool) -> None:
37
+ """Initialize pool slots with worktrees and placeholder branches.
38
+
39
+ Pre-creates worktrees with placeholder branches so they're ready for
40
+ immediate assignment. This makes `erk slot create` faster because it can
41
+ reuse existing worktrees instead of creating new ones.
42
+
43
+ By default, initializes slots up to the configured pool_size. Use -n to
44
+ specify a different count.
45
+
46
+ Examples:
47
+ erk slot init-pool # Initialize all slots up to pool_size
48
+ erk slot init-pool -n 2 # Initialize just 2 slots
49
+ erk slot init-pool --dry-run # Preview without executing
50
+ """
51
+ if dry_run:
52
+ ctx = create_context(dry_run=True)
53
+
54
+ repo = discover_repo_context(ctx, ctx.cwd)
55
+ ensure_erk_metadata_dir(repo)
56
+
57
+ # Get effective slot count
58
+ pool_size = get_pool_size(ctx)
59
+ slot_count = count if count is not None else pool_size
60
+
61
+ # Validate slot count
62
+ if slot_count < 1:
63
+ user_output("Error: Slot count must be at least 1")
64
+ raise SystemExit(1) from None
65
+
66
+ if slot_count > pool_size:
67
+ user_output(
68
+ f"Warning: Requested {slot_count} slots but pool_size is {pool_size}. "
69
+ f"Initializing {pool_size} slots."
70
+ )
71
+ slot_count = pool_size
72
+
73
+ # Load or create pool state
74
+ state = load_pool_state(repo.pool_json_path)
75
+ if state is None:
76
+ state = PoolState(
77
+ version="1.0",
78
+ pool_size=pool_size,
79
+ slots=(),
80
+ assignments=(),
81
+ )
82
+
83
+ # Get trunk branch for placeholder branches
84
+ trunk = ctx.git.detect_trunk_branch(repo.root)
85
+ local_branches = ctx.git.list_local_branches(repo.root)
86
+
87
+ initialized_count = 0
88
+ already_initialized_count = 0
89
+ new_slots: list[SlotInfo] = list(state.slots)
90
+
91
+ for slot_num in range(1, slot_count + 1):
92
+ slot_name = generate_slot_name(slot_num)
93
+ worktree_path = repo.worktrees_dir / slot_name
94
+
95
+ # Check if already initialized
96
+ if is_slot_initialized(state, slot_name):
97
+ already_initialized_count += 1
98
+ continue
99
+
100
+ # Get or create placeholder branch
101
+ placeholder_branch = get_placeholder_branch_name(slot_name)
102
+ if placeholder_branch is None:
103
+ user_output(f"Error: Could not generate placeholder branch for {slot_name}")
104
+ continue
105
+
106
+ if placeholder_branch not in local_branches:
107
+ ctx.git.create_branch(repo.root, placeholder_branch, trunk)
108
+
109
+ # Create worktree directory
110
+ if ctx.dry_run:
111
+ user_output(f"[DRY RUN] Would create directory: {worktree_path}")
112
+ else:
113
+ worktree_path.mkdir(parents=True, exist_ok=True)
114
+
115
+ # Create worktree with placeholder branch
116
+ if not ctx.git.path_exists(worktree_path / ".git"):
117
+ ctx.git.add_worktree(
118
+ repo.root,
119
+ worktree_path,
120
+ branch=placeholder_branch,
121
+ ref=None,
122
+ create_branch=False,
123
+ )
124
+
125
+ # Add to slots list
126
+ new_slots.append(SlotInfo(name=slot_name, last_objective_issue=None))
127
+ initialized_count += 1
128
+ if ctx.dry_run:
129
+ user_output(f"[DRY RUN] Would initialize {slot_name}")
130
+ else:
131
+ user_output(f" Initialized {slot_name}")
132
+
133
+ # Update and save state
134
+ new_state = PoolState(
135
+ version=state.version,
136
+ pool_size=state.pool_size,
137
+ slots=tuple(new_slots),
138
+ assignments=state.assignments,
139
+ )
140
+ if ctx.dry_run:
141
+ user_output("[DRY RUN] Would save pool state")
142
+ else:
143
+ save_pool_state(repo.pool_json_path, new_state)
144
+
145
+ # Report results
146
+ if initialized_count > 0:
147
+ if ctx.dry_run:
148
+ msg = f"[DRY RUN] Would initialize {initialized_count} slots"
149
+ else:
150
+ msg = click.style(f"✓ Initialized {initialized_count} slots", fg="green")
151
+ if already_initialized_count > 0:
152
+ msg += f" ({already_initialized_count} already existed)"
153
+ user_output(msg)
154
+ elif already_initialized_count > 0:
155
+ user_output(f"All {already_initialized_count} slots already initialized")
156
+ else:
157
+ user_output("No slots to initialize")
@@ -0,0 +1,228 @@
1
+ """Slot list command - display unified worktree pool status."""
2
+
3
+ from typing import Literal
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from erk.cli.alias import alias
10
+ from erk.cli.commands.slot.common import (
11
+ DEFAULT_POOL_SIZE,
12
+ generate_slot_name,
13
+ )
14
+ from erk.cli.core import discover_repo_context
15
+ from erk.core.context import ErkContext
16
+ from erk.core.display_utils import format_relative_time
17
+ from erk.core.worktree_pool import PoolState, load_pool_state
18
+
19
+ SlotStatus = Literal["available", "assigned", "error"]
20
+ SlotReason = Literal["worktree-missing", "branch-mismatch", "-"]
21
+
22
+
23
+ def _determine_slot_status(
24
+ slot_name: str,
25
+ assigned_slots: set[str],
26
+ reason: SlotReason,
27
+ ) -> SlotStatus:
28
+ """Determine the status of a worktree slot.
29
+
30
+ Args:
31
+ slot_name: The slot identifier (e.g., "erk-managed-wt-01")
32
+ assigned_slots: Set of slot names that have assignments in pool.json
33
+ reason: The reason code from _get_slot_reason (indicates any problems)
34
+
35
+ Returns:
36
+ Status: "assigned" (healthy assignment), "error" (assignment with problem),
37
+ "available" (can be used)
38
+ """
39
+ # If slot has an assignment in pool.json
40
+ if slot_name in assigned_slots:
41
+ # Check if there's a problem with the assignment
42
+ if reason != "-":
43
+ return "error"
44
+ return "assigned"
45
+
46
+ # No assignment - slot is available
47
+ return "available"
48
+
49
+
50
+ def _get_slot_reason(
51
+ assigned_branch: str | None,
52
+ actual_branch: str | None,
53
+ ) -> SlotReason:
54
+ """Determine the reason for slot state.
55
+
56
+ Returns a kebab-case reason explaining any issues with the slot:
57
+ - "worktree-missing": pool.json has assignment but worktree doesn't exist
58
+ - "branch-mismatch": worktree exists but has different branch than assignment
59
+ - "-": healthy state (no problem)
60
+
61
+ Args:
62
+ assigned_branch: Branch assigned in pool.json (if any)
63
+ actual_branch: Actual branch on filesystem (if any)
64
+
65
+ Returns:
66
+ Reason literal explaining any issues
67
+ """
68
+ if actual_branch is None:
69
+ # No worktree on filesystem
70
+ if assigned_branch is not None:
71
+ return "worktree-missing" # pool.json says assigned but no worktree
72
+ return "-" # Neither assigned nor exists - healthy available state
73
+
74
+ if assigned_branch is None:
75
+ # Worktree exists but not assigned - healthy available state
76
+ return "-"
77
+
78
+ # Both exist - check if they match
79
+ if actual_branch == assigned_branch:
80
+ return "-" # Healthy - branches match
81
+ return "branch-mismatch"
82
+
83
+
84
+ @alias("ls")
85
+ @click.command("list")
86
+ @click.pass_obj
87
+ def slot_list(ctx: ErkContext) -> None:
88
+ """List all pool slots with unified status view.
89
+
90
+ Shows a table combining pool.json state and filesystem state:
91
+ - Worktree: The pool worktree name
92
+ - Branch: Assigned branch or - (available)
93
+ - Assigned: When the assignment was made (relative time)
94
+ - Status: available, assigned (healthy), or error (has problem)
95
+ - Reason: worktree-missing, branch-mismatch, or - (healthy)
96
+ """
97
+ repo = discover_repo_context(ctx, ctx.cwd)
98
+
99
+ # Load pool state (or use defaults if no state exists)
100
+ state = load_pool_state(repo.pool_json_path)
101
+ if state is None:
102
+ state = PoolState(
103
+ version="1.0",
104
+ pool_size=DEFAULT_POOL_SIZE,
105
+ slots=(),
106
+ assignments=(),
107
+ )
108
+
109
+ # Build lookup set
110
+ assigned_slots = {a.slot_name for a in state.assignments}
111
+
112
+ # Build lookup of slot_name -> (branch_name, relative_time)
113
+ assignments_by_slot: dict[str, tuple[str, str]] = {}
114
+ for assignment in state.assignments:
115
+ relative_time = format_relative_time(assignment.assigned_at)
116
+ assignments_by_slot[assignment.slot_name] = (assignment.branch_name, relative_time)
117
+
118
+ # Build lookup of slot_name -> last_objective_issue
119
+ objectives_by_slot: dict[str, int] = {}
120
+ for slot in state.slots:
121
+ if slot.last_objective_issue is not None:
122
+ objectives_by_slot[slot.name] = slot.last_objective_issue
123
+
124
+ # Create Rich table
125
+ table = Table(show_header=True, header_style="bold", box=None)
126
+ table.add_column("Worktree", style="cyan", no_wrap=True)
127
+ table.add_column("Branch", style="yellow", no_wrap=True)
128
+ table.add_column("Objective", no_wrap=True)
129
+ table.add_column("Assigned", no_wrap=True)
130
+ table.add_column("Status", no_wrap=True)
131
+ table.add_column("Reason", no_wrap=True)
132
+ table.add_column("Changes", no_wrap=True)
133
+
134
+ # Track counts for summary
135
+ assigned_count = 0
136
+ error_count = 0
137
+
138
+ # Add rows for all slots
139
+ for slot_num in range(1, state.pool_size + 1):
140
+ slot_name = generate_slot_name(slot_num)
141
+ worktree_path = repo.worktrees_dir / slot_name
142
+
143
+ # Check if worktree exists and get current branch
144
+ worktree_exists = ctx.git.path_exists(worktree_path)
145
+
146
+ actual_branch: str | None = None
147
+ if worktree_exists:
148
+ actual_branch = ctx.git.get_current_branch(worktree_path)
149
+
150
+ # Get assigned branch info
151
+ assigned_branch: str | None = None
152
+ assigned_time = "-"
153
+ if slot_name in assignments_by_slot:
154
+ assigned_branch, assigned_time = assignments_by_slot[slot_name]
155
+
156
+ # Determine reason for any issues (needed before status)
157
+ reason = _get_slot_reason(assigned_branch, actual_branch)
158
+
159
+ # Determine status (depends on reason)
160
+ status = _determine_slot_status(slot_name, assigned_slots, reason)
161
+
162
+ # Format branch display
163
+ if status == "available":
164
+ branch_display = "[dim]-[/dim]"
165
+ assigned_time = "-"
166
+ else:
167
+ # Both "assigned" and "error" show the assigned branch
168
+ branch_display = assigned_branch if assigned_branch else "-"
169
+
170
+ # Format status with color
171
+ status_map: dict[SlotStatus, str] = {
172
+ "available": "[dim]available[/dim]",
173
+ "assigned": "[green]assigned[/green]",
174
+ "error": "[red]error[/red]",
175
+ }
176
+ status_display = status_map[status]
177
+
178
+ # Format reason with color (only shown for error states)
179
+ reason_map: dict[SlotReason, str] = {
180
+ "worktree-missing": "[red]worktree-missing[/red]",
181
+ "branch-mismatch": "[red]branch-mismatch[/red]",
182
+ "-": "[dim]-[/dim]",
183
+ }
184
+ reason_display = reason_map[reason]
185
+
186
+ # Format changes display
187
+ changes_display: str
188
+ if worktree_exists and ctx.git.has_uncommitted_changes(worktree_path):
189
+ changes_display = "[yellow]dirty[/yellow]"
190
+ else:
191
+ changes_display = "[dim]-[/dim]"
192
+
193
+ # Format objective display
194
+ objective_display: str
195
+ if slot_name in objectives_by_slot:
196
+ objective_display = f"#{objectives_by_slot[slot_name]}"
197
+ else:
198
+ objective_display = "[dim]-[/dim]"
199
+
200
+ table.add_row(
201
+ slot_name,
202
+ branch_display,
203
+ objective_display,
204
+ assigned_time,
205
+ status_display,
206
+ reason_display,
207
+ changes_display,
208
+ )
209
+
210
+ # Track counts
211
+ if status == "assigned":
212
+ assigned_count += 1
213
+ elif status == "error":
214
+ error_count += 1
215
+
216
+ # Output table to stderr (consistent with user_output convention)
217
+ # Use width=200 to ensure proper display without truncation
218
+ console = Console(stderr=True, width=200, force_terminal=True)
219
+ console.print(table)
220
+
221
+ # Print summary
222
+ available_count = state.pool_size - assigned_count - error_count
223
+ console.print(
224
+ f"\nPool: {state.pool_size} slots | "
225
+ f"{available_count} available | "
226
+ f"{assigned_count} assigned | "
227
+ f"{error_count} error"
228
+ )
@@ -0,0 +1,190 @@
1
+ """Slot repair command - remove stale assignments from pool state."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.commands.slot.check_cmd import SyncIssue, run_sync_diagnostics
8
+ from erk.cli.core import discover_repo_context
9
+ from erk.core.context import ErkContext
10
+ from erk.core.worktree_pool import (
11
+ PoolState,
12
+ SlotAssignment,
13
+ load_pool_state,
14
+ save_pool_state,
15
+ )
16
+ from erk_shared.output.output import user_confirm, user_output
17
+
18
+ # Issue codes that can be auto-repaired by removing the assignment
19
+ AUTO_REPAIRABLE_CODES = frozenset({"orphan-state"})
20
+
21
+
22
+ def _extract_slot_name(issue: SyncIssue) -> str:
23
+ """Extract slot name from issue message.
24
+
25
+ Issue messages have format: "Slot <slot-name>: <description>"
26
+ """
27
+ return issue.message.split(":")[0].replace("Slot ", "")
28
+
29
+
30
+ def _format_remediation(issue: SyncIssue, worktrees_dir: Path) -> list[str]:
31
+ """Format remediation suggestions for an issue.
32
+
33
+ Args:
34
+ issue: The sync issue
35
+ worktrees_dir: Path to worktrees directory (for path display)
36
+
37
+ Returns:
38
+ List of remediation command strings
39
+ """
40
+ slot_name = _extract_slot_name(issue)
41
+ worktree_path = worktrees_dir / slot_name
42
+
43
+ if issue.code == "branch-mismatch":
44
+ # Extract expected branch from message: "pool says 'X', git says 'Y'"
45
+ # Message format: "Slot <slot>: pool says '<expected>', git says '<actual>'"
46
+ parts = issue.message.split("pool says '")
47
+ expected_branch = parts[1].split("'")[0] if len(parts) > 1 else "<expected-branch>"
48
+ return [
49
+ f"erk slot unassign {slot_name}",
50
+ f"cd {worktree_path} && git checkout {expected_branch}",
51
+ ]
52
+ elif issue.code == "missing-branch":
53
+ return [f"erk slot unassign {slot_name}"]
54
+ elif issue.code == "git-registry-missing":
55
+ return [f"erk slot unassign {slot_name}"]
56
+ else:
57
+ return []
58
+
59
+
60
+ def find_stale_assignments(
61
+ state: PoolState,
62
+ issues: list[SyncIssue],
63
+ ) -> list[SlotAssignment]:
64
+ """Find assignments that can be auto-repaired.
65
+
66
+ Args:
67
+ state: Pool state to check
68
+ issues: List of sync issues from run_sync_diagnostics
69
+
70
+ Returns:
71
+ List of stale SlotAssignments (orphan-state issues)
72
+ """
73
+ # Collect the slot names that have auto-repairable issues
74
+ stale_slot_names = {
75
+ _extract_slot_name(issue) for issue in issues if issue.code in AUTO_REPAIRABLE_CODES
76
+ }
77
+
78
+ # Return the actual assignments that are stale
79
+ return [a for a in state.assignments if a.slot_name in stale_slot_names]
80
+
81
+
82
+ def execute_repair(
83
+ state: PoolState,
84
+ stale_assignments: list[SlotAssignment],
85
+ ) -> PoolState:
86
+ """Create new pool state with stale assignments removed.
87
+
88
+ Args:
89
+ state: Current pool state
90
+ stale_assignments: Assignments to remove
91
+
92
+ Returns:
93
+ New PoolState with stale assignments filtered out
94
+ """
95
+ stale_slot_names = {a.slot_name for a in stale_assignments}
96
+ new_assignments = tuple(a for a in state.assignments if a.slot_name not in stale_slot_names)
97
+
98
+ return PoolState(
99
+ version=state.version,
100
+ pool_size=state.pool_size,
101
+ slots=state.slots,
102
+ assignments=new_assignments,
103
+ )
104
+
105
+
106
+ def _display_informational_issues(
107
+ issues: list[SyncIssue],
108
+ worktrees_dir: Path,
109
+ ) -> None:
110
+ """Display informational issues that require manual intervention.
111
+
112
+ Args:
113
+ issues: List of non-auto-repairable issues
114
+ worktrees_dir: Path to worktrees directory (for path display)
115
+ """
116
+ informational = [i for i in issues if i.code not in AUTO_REPAIRABLE_CODES]
117
+ if not informational:
118
+ return
119
+
120
+ user_output("")
121
+ user_output(f"Found {len(informational)} issue(s) requiring manual intervention:")
122
+ for issue in informational:
123
+ user_output(f" [{click.style(issue.code, fg='yellow')}] {issue.message}")
124
+ remediation = _format_remediation(issue, worktrees_dir)
125
+ if remediation:
126
+ user_output(" Remediation:")
127
+ for cmd in remediation:
128
+ user_output(f" {click.style(cmd, fg='cyan')}")
129
+
130
+
131
+ @click.command("repair")
132
+ @click.option("-f", "--force", is_flag=True, help="Skip confirmation prompt")
133
+ @click.pass_obj
134
+ def slot_repair(ctx: ErkContext, force: bool) -> None:
135
+ """Remove stale assignments from pool state.
136
+
137
+ Finds assignments where the worktree directory no longer exists
138
+ and removes them from pool.json.
139
+
140
+ Also displays other issues (like branch-mismatch) that require
141
+ manual intervention with suggested remediation commands.
142
+
143
+ Use --force to skip the confirmation prompt.
144
+ """
145
+ repo = discover_repo_context(ctx, ctx.cwd)
146
+
147
+ # Load pool state
148
+ state = load_pool_state(repo.pool_json_path)
149
+ if state is None:
150
+ user_output("Error: No pool configured. Run `erk slot create` first.")
151
+ raise SystemExit(1) from None
152
+
153
+ # Run full diagnostics to get all issues
154
+ all_issues = run_sync_diagnostics(ctx, state, repo.root)
155
+
156
+ # Find stale (auto-repairable) assignments
157
+ stale_assignments = find_stale_assignments(state, all_issues)
158
+
159
+ # Display informational issues (non-auto-repairable)
160
+ _display_informational_issues(all_issues, repo.worktrees_dir)
161
+
162
+ if not stale_assignments:
163
+ if not any(i.code not in AUTO_REPAIRABLE_CODES for i in all_issues):
164
+ user_output(click.style("✓ No issues found", fg="green"))
165
+ return
166
+
167
+ # Show what will be repaired
168
+ user_output("")
169
+ user_output(f"Found {len(stale_assignments)} repairable issue(s):")
170
+ for assignment in stale_assignments:
171
+ user_output(
172
+ f" - {click.style(assignment.slot_name, fg='cyan')}: "
173
+ f"branch '{click.style(assignment.branch_name, fg='yellow')}' "
174
+ f"(worktree missing)"
175
+ )
176
+
177
+ # Prompt for confirmation unless --force
178
+ if not force:
179
+ if not user_confirm("\nRemove these stale assignments?", default=True):
180
+ user_output("Aborted.")
181
+ return
182
+
183
+ # Execute repair
184
+ new_state = execute_repair(state, stale_assignments)
185
+ save_pool_state(repo.pool_json_path, new_state)
186
+
187
+ user_output("")
188
+ user_output(
189
+ click.style("✓ ", fg="green") + f"Removed {len(stale_assignments)} stale assignment(s)"
190
+ )
@@ -0,0 +1,23 @@
1
+ """Stack operation commands for managing worktree stacks."""
2
+
3
+ import click
4
+
5
+ from erk.cli.alias import register_with_aliases
6
+ from erk.cli.commands.stack.consolidate_cmd import consolidate_stack
7
+ from erk.cli.commands.stack.list_cmd import list_stack
8
+ from erk.cli.commands.stack.move_cmd import move_stack
9
+ from erk.cli.commands.stack.split_old.command import split_cmd as split_stack
10
+ from erk.cli.graphite_command import GraphiteGroup
11
+
12
+
13
+ @click.group("stack", cls=GraphiteGroup)
14
+ def stack_group() -> None:
15
+ """Manage worktree stack operations."""
16
+ pass
17
+
18
+
19
+ # Register subcommands
20
+ stack_group.add_command(consolidate_stack, name="consolidate")
21
+ register_with_aliases(stack_group, list_stack, name="list")
22
+ stack_group.add_command(move_stack, name="move")
23
+ stack_group.add_command(split_stack, name="split")