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,357 @@
1
+ """Checkout command - find and switch to a worktree by branch name."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.activation import render_activation_script
8
+ from erk.cli.alias import alias
9
+ from erk.cli.commands.completions import complete_branch_names
10
+ from erk.cli.commands.wt.create_cmd import ensure_worktree_for_branch
11
+ from erk.cli.core import discover_repo_context
12
+ from erk.cli.graphite import find_worktrees_containing_branch
13
+ from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
14
+ from erk.core.context import ErkContext
15
+ from erk.core.repo_discovery import RepoContext, ensure_erk_metadata_dir
16
+ from erk.core.worktree_utils import compute_relative_path_in_worktree
17
+ from erk_shared.git.abc import WorktreeInfo
18
+ from erk_shared.output.output import user_confirm, user_output
19
+
20
+
21
+ def try_switch_root_worktree(ctx: ErkContext, repo: RepoContext, branch: str) -> Path | None:
22
+ """Try to switch root worktree to branch if it's trunk and root is clean.
23
+
24
+ This implements the "takeover" behavior where checking out trunk in a clean root
25
+ worktree switches the root to trunk instead of creating a new dated worktree.
26
+
27
+ Args:
28
+ ctx: Erk context with git operations
29
+ repo: Repository context
30
+ branch: Branch name to check
31
+
32
+ Returns:
33
+ Root worktree path if successful, None otherwise
34
+ """
35
+ # Check if branch is trunk
36
+ if branch != ctx.trunk_branch:
37
+ return None
38
+
39
+ # Find root worktree
40
+ worktrees = ctx.git.list_worktrees(repo.root)
41
+ root_worktree = None
42
+ for wt in worktrees:
43
+ if wt.is_root:
44
+ root_worktree = wt
45
+ break
46
+
47
+ if root_worktree is None:
48
+ return None
49
+
50
+ # Check if root is clean
51
+ if not ctx.git.is_worktree_clean(root_worktree.path):
52
+ return None
53
+
54
+ # Switch root to trunk branch
55
+ ctx.git.checkout_branch(root_worktree.path, branch)
56
+
57
+ return root_worktree.path
58
+
59
+
60
+ def _ensure_graphite_tracking(
61
+ ctx: ErkContext,
62
+ repo_root: Path,
63
+ target_path: Path,
64
+ branch: str,
65
+ script: bool,
66
+ ) -> None:
67
+ """Ensure branch is tracked by Graphite (idempotent), with user confirmation.
68
+
69
+ If the branch is not already tracked, prompts the user and tracks it with
70
+ trunk as parent if confirmed. This enables branches created without Graphite
71
+ (e.g., via erk-queue) to be managed with Graphite locally.
72
+
73
+ Args:
74
+ ctx: Erk context
75
+ repo_root: Repository root path
76
+ target_path: Worktree path where `gt track` should run
77
+ branch: Target branch name
78
+ script: Whether to output only the activation script
79
+ """
80
+ # Skip if Graphite is disabled
81
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
82
+ if not use_graphite:
83
+ return
84
+
85
+ trunk_branch = ctx.trunk_branch
86
+ # Skip if no trunk branch detected (shouldn't happen in checkout context)
87
+ if trunk_branch is None:
88
+ return
89
+
90
+ # Skip trunk branch - it's always implicitly tracked
91
+ if branch == trunk_branch:
92
+ return
93
+
94
+ # Check if already tracked (LBYL)
95
+ all_branches = ctx.graphite.get_all_branches(ctx.git, repo_root)
96
+ if branch in all_branches:
97
+ return # Already tracked, nothing to do
98
+
99
+ # In script mode, skip tracking (no interactive prompts allowed)
100
+ if script:
101
+ return
102
+
103
+ # Prompt user for confirmation
104
+ if not user_confirm(
105
+ f"Branch '{branch}' is not tracked by Graphite. Track it with parent '{trunk_branch}'?",
106
+ default=False,
107
+ ):
108
+ return
109
+
110
+ # Track the branch with trunk as parent
111
+ ctx.graphite.track_branch(target_path, branch, trunk_branch)
112
+ user_output(f"Tracked '{branch}' with Graphite (parent: {trunk_branch})")
113
+
114
+
115
+ def _format_worktree_info(wt: WorktreeInfo, repo_root: Path) -> str:
116
+ """Format worktree information for display.
117
+
118
+ Args:
119
+ wt: WorktreeInfo to format
120
+ repo_root: Path to repository root (used to identify root worktree)
121
+
122
+ Returns:
123
+ Formatted string like "root (currently on 'main')" or "wt-name (currently on 'feature')"
124
+ """
125
+ current = wt.branch or "(detached HEAD)"
126
+ if wt.path == repo_root:
127
+ return f" - root (currently on '{current}')"
128
+ else:
129
+ # Get worktree name from path
130
+ wt_name = wt.path.name
131
+ return f" - {wt_name} (currently on '{current}')"
132
+
133
+
134
+ def _perform_checkout(
135
+ ctx: ErkContext,
136
+ *,
137
+ repo_root: Path,
138
+ target_worktree: WorktreeInfo,
139
+ branch: str,
140
+ script: bool,
141
+ is_newly_created: bool = False,
142
+ worktrees: list[WorktreeInfo] | None = None,
143
+ ) -> None:
144
+ """Perform the actual checkout and switch to a worktree.
145
+
146
+ Args:
147
+ ctx: Erk context
148
+ repo_root: Repository root path
149
+ target_worktree: The worktree to switch to
150
+ branch: Target branch name
151
+ script: Whether to output only the activation script
152
+ is_newly_created: Whether the worktree was just created (default False)
153
+ worktrees: Optional list of worktrees (for relative path computation)
154
+ """
155
+ target_path = target_worktree.path
156
+ current_branch_in_worktree = target_worktree.branch
157
+ current_cwd = ctx.cwd
158
+
159
+ # Compute relative path to preserve directory position
160
+ relative_path = compute_relative_path_in_worktree(worktrees, ctx.cwd) if worktrees else None
161
+
162
+ # Check if branch is already checked out in the worktree
163
+ need_checkout = current_branch_in_worktree != branch
164
+
165
+ # If we need to checkout, do it before generating the activation script
166
+ if need_checkout:
167
+ # Checkout the branch in the target worktree
168
+ ctx.git.checkout_branch(target_path, branch)
169
+
170
+ # Ensure branch is tracked with Graphite (idempotent)
171
+ _ensure_graphite_tracking(ctx, repo_root, target_path, branch, script)
172
+
173
+ if need_checkout:
174
+ # Show stack context
175
+ if not script:
176
+ stack = ctx.graphite.get_branch_stack(ctx.git, repo_root, branch)
177
+ if stack:
178
+ user_output(f"Stack: {' -> '.join(stack)}")
179
+ user_output(f"Checked out '{branch}' in worktree")
180
+
181
+ # Generate activation script
182
+ if script:
183
+ # Script mode: always generate script (for shell integration or manual sourcing)
184
+ is_switching_location = current_cwd != target_path
185
+
186
+ # Determine worktree name from path
187
+ worktree_name = target_path.name
188
+
189
+ # Four-case message logic:
190
+ if is_newly_created:
191
+ # Case 4: Switched to newly created worktree
192
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
193
+ switch_message = f'echo "Switched to new worktree {styled_wt}"'
194
+ elif not is_switching_location:
195
+ # Case 1: Already on target branch in current worktree
196
+ styled_branch = click.style(branch, fg="yellow")
197
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
198
+ switch_message = f'echo "Already on branch {styled_branch} in worktree {styled_wt}"'
199
+ elif not need_checkout:
200
+ # Case 2: Switched to existing worktree with branch already checked out
201
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
202
+ if worktree_name == branch:
203
+ # Standard naming
204
+ switch_message = f'echo "Switched to worktree {styled_wt}"'
205
+ else:
206
+ # Edge case: non-standard naming
207
+ styled_branch = click.style(branch, fg="yellow")
208
+ switch_message = f'echo "Switched to worktree {styled_wt} (branch {styled_branch})"'
209
+ else:
210
+ # Case 3: Switched to existing worktree and checked out branch
211
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
212
+ styled_branch = click.style(branch, fg="yellow")
213
+ switch_message = (
214
+ f'echo "Switched to worktree {styled_wt} and checked out branch {styled_branch}"'
215
+ )
216
+
217
+ script_content = render_activation_script(
218
+ worktree_path=target_path,
219
+ target_subpath=relative_path,
220
+ post_cd_commands=None,
221
+ final_message=switch_message,
222
+ comment="work activate-script",
223
+ )
224
+
225
+ result = ctx.script_writer.write_activation_script(
226
+ script_content,
227
+ command_name="checkout",
228
+ comment=f"checkout {branch}",
229
+ )
230
+ result.output_for_shell_integration()
231
+ else:
232
+ # Non-script mode: Apply same four-case logic with user_output()
233
+ worktree_name = target_path.name
234
+
235
+ if is_newly_created:
236
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
237
+ user_output(f"Switched to new worktree {styled_wt}")
238
+ elif ctx.cwd == target_path:
239
+ styled_branch = click.style(branch, fg="yellow")
240
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
241
+ user_output(f"Already on branch {styled_branch} in worktree {styled_wt}")
242
+ elif current_branch_in_worktree == branch:
243
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
244
+ if worktree_name == branch:
245
+ user_output(f"Switched to worktree {styled_wt}")
246
+ else:
247
+ styled_branch = click.style(branch, fg="yellow")
248
+ user_output(f"Switched to worktree {styled_wt} (branch {styled_branch})")
249
+ else:
250
+ styled_wt = click.style(worktree_name, fg="cyan", bold=True)
251
+ styled_branch = click.style(branch, fg="yellow")
252
+ user_output(f"Switched to worktree {styled_wt} and checked out branch {styled_branch}")
253
+
254
+ # Show manual instructions
255
+ user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
256
+ user_output(f"Or use: source <(erk br co {branch} --script)")
257
+
258
+
259
+ @alias("co")
260
+ @click.command("checkout", cls=CommandWithHiddenOptions)
261
+ @click.argument("branch", metavar="BRANCH", shell_complete=complete_branch_names)
262
+ @script_option
263
+ @click.pass_obj
264
+ def branch_checkout(ctx: ErkContext, branch: str, script: bool) -> None:
265
+ """Checkout BRANCH by finding and switching to its worktree.
266
+
267
+ This command finds which worktree has the specified branch checked out
268
+ and switches to it. If the branch exists but isn't checked out anywhere,
269
+ a worktree is automatically created. If the branch exists on origin but
270
+ not locally, a tracking branch and worktree are created automatically.
271
+
272
+ Examples:
273
+
274
+ erk br co feature/user-auth # Checkout existing worktree
275
+
276
+ erk br co unchecked-branch # Auto-create worktree
277
+
278
+ erk br co origin-only-branch # Create tracking branch + worktree
279
+
280
+ If multiple worktrees contain the branch, all options are shown.
281
+ """
282
+ # Use existing repo from context if available (for tests), otherwise discover
283
+ if isinstance(ctx.repo, RepoContext):
284
+ repo = ctx.repo
285
+ else:
286
+ repo = discover_repo_context(ctx, ctx.cwd)
287
+ ensure_erk_metadata_dir(repo)
288
+
289
+ # Get all worktrees
290
+ worktrees = ctx.git.list_worktrees(repo.root)
291
+
292
+ # Find worktrees containing the target branch
293
+ matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
294
+
295
+ # Track whether we're creating a new worktree
296
+ is_newly_created = False
297
+
298
+ # Handle three cases: no match, one match, multiple matches
299
+ if len(matching_worktrees) == 0:
300
+ # No worktrees have this branch checked out
301
+ # First, try switching clean root worktree if checking out trunk
302
+ root_path = try_switch_root_worktree(ctx, repo, branch)
303
+ if root_path is not None:
304
+ # Successfully switched root to trunk - refresh and jump to it
305
+ worktrees = ctx.git.list_worktrees(repo.root)
306
+ matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
307
+ else:
308
+ # Root not available or not trunk - auto-create worktree
309
+ _worktree_path, is_newly_created = ensure_worktree_for_branch(
310
+ ctx, repo, branch, is_plan_derived=False
311
+ )
312
+
313
+ # Refresh worktree list to include the newly created worktree
314
+ worktrees = ctx.git.list_worktrees(repo.root)
315
+ matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
316
+
317
+ # Fall through to jump to the worktree
318
+
319
+ if len(matching_worktrees) == 1:
320
+ # Exactly one worktree contains this branch
321
+ target_worktree = matching_worktrees[0]
322
+ _perform_checkout(
323
+ ctx,
324
+ repo_root=repo.root,
325
+ target_worktree=target_worktree,
326
+ branch=branch,
327
+ script=script,
328
+ is_newly_created=is_newly_created,
329
+ worktrees=worktrees,
330
+ )
331
+
332
+ else:
333
+ # Multiple worktrees contain this branch
334
+ # Check if any worktree has the branch directly checked out
335
+ directly_checked_out = [wt for wt in matching_worktrees if wt.branch == branch]
336
+
337
+ if len(directly_checked_out) == 1:
338
+ # Exactly one worktree has the branch directly checked out - jump to it
339
+ target_worktree = directly_checked_out[0]
340
+ _perform_checkout(
341
+ ctx,
342
+ repo_root=repo.root,
343
+ target_worktree=target_worktree,
344
+ branch=branch,
345
+ script=script,
346
+ is_newly_created=is_newly_created,
347
+ worktrees=worktrees,
348
+ )
349
+ else:
350
+ # Zero or multiple worktrees have it directly checked out
351
+ # Show error message listing all options
352
+ user_output(f"Branch '{branch}' exists in multiple worktrees:")
353
+ for wt in matching_worktrees:
354
+ user_output(_format_worktree_info(wt, repo.root))
355
+
356
+ user_output("\nPlease specify which worktree to use.")
357
+ raise SystemExit(1)
@@ -0,0 +1,161 @@
1
+ """Branch create command - create a new branch with optional slot assignment."""
2
+
3
+ import sys
4
+ from datetime import UTC, datetime
5
+
6
+ import click
7
+
8
+ from erk.cli.commands.slot.common import (
9
+ cleanup_worktree_artifacts,
10
+ find_branch_assignment,
11
+ find_inactive_slot,
12
+ find_next_available_slot,
13
+ generate_slot_name,
14
+ get_pool_size,
15
+ handle_pool_full_interactive,
16
+ )
17
+ from erk.cli.core import discover_repo_context
18
+ from erk.core.context import ErkContext
19
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
20
+ from erk.core.worktree_pool import (
21
+ PoolState,
22
+ SlotAssignment,
23
+ load_pool_state,
24
+ save_pool_state,
25
+ )
26
+ from erk_shared.output.output import user_output
27
+
28
+
29
+ @click.command("create")
30
+ @click.argument("branch_name", metavar="BRANCH")
31
+ @click.option("--no-slot", is_flag=True, help="Create branch without slot assignment")
32
+ @click.option("-f", "--force", is_flag=True, help="Auto-unassign oldest branch if pool is full")
33
+ @click.pass_obj
34
+ def branch_create(ctx: ErkContext, branch_name: str, no_slot: bool, force: bool) -> None:
35
+ """Create a NEW branch and optionally assign it to a pool slot.
36
+
37
+ BRANCH is the name of the new git branch to create.
38
+
39
+ By default, the command will:
40
+ 1. Verify the branch does NOT already exist (fails if it does)
41
+ 2. Create the branch from trunk
42
+ 3. Find the next available slot in the pool
43
+ 4. Create a worktree for that slot
44
+ 5. Assign the branch to the slot
45
+
46
+ Use --no-slot to create a branch without assigning it to a slot.
47
+ Use `erk br assign` to assign an EXISTING branch to a slot.
48
+ """
49
+ repo = discover_repo_context(ctx, ctx.cwd)
50
+ ensure_erk_metadata_dir(repo)
51
+
52
+ # Check if branch already exists
53
+ local_branches = ctx.git.list_local_branches(repo.root)
54
+ if branch_name in local_branches:
55
+ user_output(
56
+ f"Error: Branch '{branch_name}' already exists.\n"
57
+ "Use `erk br assign` to assign an existing branch to a slot."
58
+ )
59
+ raise SystemExit(1) from None
60
+
61
+ # Create the new branch from trunk
62
+ trunk = ctx.git.detect_trunk_branch(repo.root)
63
+ ctx.git.create_branch(repo.root, branch_name, trunk)
64
+ ctx.graphite.track_branch(repo.root, branch_name, trunk)
65
+ user_output(f"Created branch: {branch_name}")
66
+
67
+ # If --no-slot is specified, we're done
68
+ if no_slot:
69
+ return
70
+
71
+ # Get pool size from config or default
72
+ pool_size = get_pool_size(ctx)
73
+
74
+ # Load or create pool state
75
+ state = load_pool_state(repo.pool_json_path)
76
+ if state is None:
77
+ state = PoolState(
78
+ version="1.0",
79
+ pool_size=pool_size,
80
+ slots=(),
81
+ assignments=(),
82
+ )
83
+
84
+ # Check if branch is already assigned (shouldn't happen since we just created it)
85
+ existing = find_branch_assignment(state, branch_name)
86
+ if existing is not None:
87
+ user_output(f"Error: Branch '{branch_name}' already assigned to {existing.slot_name}")
88
+ raise SystemExit(1) from None
89
+
90
+ # First, prefer reusing existing worktrees (fast path)
91
+ inactive_slot = find_inactive_slot(state, ctx.git, repo.root)
92
+ if inactive_slot is not None:
93
+ slot_name, worktree_path = inactive_slot
94
+
95
+ # Checkout the branch in the existing worktree
96
+ ctx.git.checkout_branch(worktree_path, branch_name)
97
+ else:
98
+ # Fall back to on-demand slot creation
99
+ slot_num = find_next_available_slot(state, repo.worktrees_dir)
100
+ if slot_num is None:
101
+ # Pool is full - handle interactively or with --force
102
+ to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
103
+ if to_unassign is None:
104
+ raise SystemExit(1) from None
105
+
106
+ # Remove the assignment from state
107
+ new_assignments = tuple(
108
+ a for a in state.assignments if a.slot_name != to_unassign.slot_name
109
+ )
110
+ state = PoolState(
111
+ version=state.version,
112
+ pool_size=state.pool_size,
113
+ slots=state.slots,
114
+ assignments=new_assignments,
115
+ )
116
+ save_pool_state(repo.pool_json_path, state)
117
+ user_output(
118
+ click.style("✓ ", fg="green")
119
+ + f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
120
+ + f"from {click.style(to_unassign.slot_name, fg='cyan')}"
121
+ )
122
+
123
+ # Reuse the unassigned slot - worktree exists, just checkout
124
+ slot_name = to_unassign.slot_name
125
+ worktree_path = to_unassign.worktree_path
126
+ cleanup_worktree_artifacts(worktree_path)
127
+ ctx.git.checkout_branch(worktree_path, branch_name)
128
+ else:
129
+ # Create new slot - no worktree exists yet
130
+ slot_name = generate_slot_name(slot_num)
131
+ worktree_path = repo.worktrees_dir / slot_name
132
+ worktree_path.mkdir(parents=True, exist_ok=True)
133
+ ctx.git.add_worktree(
134
+ repo.root,
135
+ worktree_path,
136
+ branch=branch_name,
137
+ ref=None,
138
+ create_branch=False,
139
+ )
140
+
141
+ # Create new assignment
142
+ now = datetime.now(UTC).isoformat()
143
+ new_assignment = SlotAssignment(
144
+ slot_name=slot_name,
145
+ branch_name=branch_name,
146
+ assigned_at=now,
147
+ worktree_path=worktree_path,
148
+ )
149
+
150
+ # Update state with new assignment
151
+ new_state = PoolState(
152
+ version=state.version,
153
+ pool_size=state.pool_size,
154
+ slots=state.slots,
155
+ assignments=(*state.assignments, new_assignment),
156
+ )
157
+
158
+ # Save state
159
+ save_pool_state(repo.pool_json_path, new_state)
160
+
161
+ user_output(click.style(f"✓ Assigned {branch_name} to {slot_name}", fg="green"))
@@ -0,0 +1,82 @@
1
+ """List active branches - those with worktrees or open PRs."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from erk.cli.alias import alias
8
+ from erk.cli.core import discover_repo_context
9
+ from erk.core.context import ErkContext
10
+ from erk.core.display_utils import format_relative_time, get_pr_status_emoji
11
+ from erk_shared.github.types import PullRequestInfo
12
+
13
+
14
+ @alias("ls")
15
+ @click.command("list")
16
+ @click.pass_obj
17
+ def branch_list(ctx: ErkContext) -> None:
18
+ """List active branches with their worktrees and PR status.
19
+
20
+ Active branches are those that have:
21
+ - A worktree checked out, OR
22
+ - An open pull request
23
+
24
+ Example:
25
+ erk br ls
26
+ """
27
+ repo = discover_repo_context(ctx, ctx.cwd)
28
+
29
+ # Detect trunk branch for last commit calculation
30
+ trunk = ctx.git.detect_trunk_branch(repo.root)
31
+
32
+ # Get worktrees and PR info
33
+ worktrees = ctx.git.list_worktrees(repo.root)
34
+ prs = ctx.graphite.get_prs_from_graphite(ctx.git, repo.root)
35
+
36
+ # Build active branches map: branch -> (worktree_name, pr_info)
37
+ active_branches: dict[str, tuple[str | None, PullRequestInfo | None]] = {}
38
+
39
+ # Add branches from worktrees
40
+ for wt in worktrees:
41
+ if wt.branch is not None and wt.branch != ctx.trunk_branch:
42
+ wt_name = "root" if wt.is_root else wt.path.name
43
+ pr = prs.get(wt.branch)
44
+ active_branches[wt.branch] = (wt_name, pr)
45
+
46
+ # Add branches with open PRs (not already in worktrees)
47
+ for branch, pr in prs.items():
48
+ if branch not in active_branches and pr.state == "OPEN":
49
+ active_branches[branch] = (None, pr)
50
+
51
+ # Display table
52
+ console = Console(stderr=True, force_terminal=True)
53
+ table = Table(show_header=True, header_style="bold", box=None)
54
+ table.add_column("branch", style="yellow")
55
+ table.add_column("worktree", style="cyan")
56
+ table.add_column("pr")
57
+ table.add_column("last", no_wrap=True)
58
+ table.add_column("state", style="dim")
59
+
60
+ for branch in sorted(active_branches.keys()):
61
+ wt_name, pr = active_branches[branch]
62
+
63
+ pr_cell = "-"
64
+ state_cell = "-"
65
+ if pr is not None:
66
+ emoji = get_pr_status_emoji(pr)
67
+ pr_cell = f"{emoji} #{pr.number}"
68
+ state_cell = pr.state
69
+
70
+ # Get last commit time for this branch
71
+ timestamp = ctx.git.get_branch_last_commit_time(repo.root, branch, trunk)
72
+ last_cell = format_relative_time(timestamp) if timestamp is not None else "-"
73
+
74
+ table.add_row(
75
+ branch,
76
+ wt_name or "-",
77
+ pr_cell,
78
+ last_cell,
79
+ state_cell,
80
+ )
81
+
82
+ console.print(table)