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,309 @@
1
+ """Move branches between worktrees with explicit source specification."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.commands.completions import complete_worktree_names
8
+ from erk.cli.core import discover_repo_context, worktree_path_for
9
+ from erk.cli.ensure import Ensure
10
+ from erk.core.context import ErkContext
11
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
12
+ from erk.core.worktree_utils import (
13
+ MoveOperationType,
14
+ determine_move_operation,
15
+ find_worktree_containing_path,
16
+ find_worktree_with_branch,
17
+ get_worktree_branch,
18
+ )
19
+ from erk_shared.output.output import user_confirm, user_output
20
+
21
+
22
+ def _resolve_current_worktree(ctx: ErkContext, repo_root: Path) -> Path:
23
+ """Find worktree containing current directory.
24
+
25
+ Raises SystemExit if not in a git repository or not in any worktree.
26
+ """
27
+ Ensure.not_none(ctx.git.get_git_common_dir(ctx.cwd), "Not in a git repository")
28
+
29
+ cwd = ctx.cwd.resolve()
30
+ worktrees = ctx.git.list_worktrees(repo_root)
31
+ wt_path = find_worktree_containing_path(worktrees, cwd)
32
+ if wt_path is None:
33
+ user_output(
34
+ f"Error: Current directory ({cwd}) is not in any worktree.\n"
35
+ f"Either run this from within a worktree, or use --worktree or "
36
+ f"--branch to specify the source."
37
+ )
38
+ raise SystemExit(1)
39
+ return wt_path
40
+
41
+
42
+ def resolve_source_worktree(
43
+ ctx: ErkContext,
44
+ repo_root: Path,
45
+ *,
46
+ current: bool,
47
+ branch: str | None,
48
+ worktree: str | None,
49
+ worktrees_dir: Path,
50
+ ) -> Path:
51
+ """Determine source worktree from flags.
52
+
53
+ Defaults to current worktree if no flags provided.
54
+ Raises SystemExit if multiple flags specified or if source cannot be resolved.
55
+ """
56
+ # Count how many source flags are specified
57
+ flag_count = sum([current, branch is not None, worktree is not None])
58
+
59
+ if flag_count > 1:
60
+ user_output("Error: Only one of --current, --branch, or --worktree can be specified")
61
+ raise SystemExit(1)
62
+
63
+ if flag_count == 0 or current:
64
+ # Default to current worktree (either no flags or --current explicitly set)
65
+ return _resolve_current_worktree(ctx, repo_root)
66
+
67
+ if branch:
68
+ # Find worktree containing this branch
69
+ worktrees = ctx.git.list_worktrees(repo_root)
70
+ wt = Ensure.not_none(
71
+ find_worktree_with_branch(worktrees, branch),
72
+ f"Branch '{branch}' not found in any worktree",
73
+ )
74
+ return wt
75
+
76
+ if worktree:
77
+ # Resolve worktree name to path
78
+ wt_path = worktree_path_for(worktrees_dir, worktree)
79
+ # Validate that the worktree exists
80
+ Ensure.path_exists(ctx, wt_path, f"Worktree '{worktree}' does not exist")
81
+ return wt_path
82
+
83
+ user_output("Error: Invalid state - no source specified")
84
+ raise SystemExit(1)
85
+
86
+
87
+ def detect_operation_type(
88
+ source_wt: Path, target_wt: Path, ctx: ErkContext, repo_root: Path
89
+ ) -> MoveOperationType:
90
+ """Determine whether to move, swap, or create based on target existence.
91
+
92
+ Returns MoveOperationType enum value.
93
+ """
94
+ worktrees = ctx.git.list_worktrees(repo_root)
95
+ operation = determine_move_operation(worktrees, source_wt, target_wt)
96
+ return operation.operation_type
97
+
98
+
99
+ def execute_move(
100
+ ctx: ErkContext,
101
+ repo_root: Path,
102
+ source_wt: Path,
103
+ target_wt: Path,
104
+ fallback_ref: str,
105
+ *,
106
+ force: bool,
107
+ ) -> None:
108
+ """Execute move operation (target doesn't exist or is in detached HEAD).
109
+
110
+ Moves the branch from source to target, then switches source to fallback_ref.
111
+ """
112
+ # Validate source has a branch
113
+ worktrees = ctx.git.list_worktrees(repo_root)
114
+ source_branch = Ensure.not_none(
115
+ get_worktree_branch(worktrees, source_wt), "Source worktree is in detached HEAD state"
116
+ )
117
+
118
+ # Check for uncommitted changes in source
119
+ if ctx.git.has_uncommitted_changes(source_wt) and not force:
120
+ user_output(
121
+ f"Error: Uncommitted changes in source worktree '{source_wt.name}'.\n"
122
+ f"Commit, stash, or use --force to override."
123
+ )
124
+ raise SystemExit(1)
125
+
126
+ target_exists = ctx.git.path_exists(target_wt)
127
+
128
+ # To move branch from source to target, we need to avoid having the same branch
129
+ # checked out in two places simultaneously. Strategy:
130
+ # 1. Detach HEAD in source worktree (frees up source_branch)
131
+ # 2. Create/checkout source_branch in target worktree
132
+ # 3. Checkout fallback_ref in source worktree
133
+ user_output(f"Moving '{source_branch}' from '{source_wt.name}' to '{target_wt.name}'")
134
+ ctx.git.checkout_detached(source_wt, source_branch)
135
+
136
+ if target_exists:
137
+ # Target exists - check for uncommitted changes
138
+ if ctx.git.has_uncommitted_changes(target_wt) and not force:
139
+ user_output(
140
+ f"Error: Uncommitted changes in target worktree '{target_wt.name}'.\n"
141
+ f"Commit, stash, or use --force to override."
142
+ )
143
+ raise SystemExit(1)
144
+
145
+ # Checkout branch in existing target
146
+ ctx.git.checkout_branch(target_wt, source_branch)
147
+ else:
148
+ # Create new worktree with branch
149
+ ctx.git.add_worktree(
150
+ repo_root, target_wt, branch=source_branch, ref=None, create_branch=False
151
+ )
152
+
153
+ # Check if fallback_ref is already checked out elsewhere, and detach it if needed
154
+ fallback_wt = ctx.git.is_branch_checked_out(repo_root, fallback_ref)
155
+ if fallback_wt is not None and fallback_wt.resolve() != source_wt.resolve():
156
+ # Fallback branch is checked out in another worktree, detach it first
157
+ ctx.git.checkout_detached(fallback_wt, fallback_ref)
158
+
159
+ # Switch source to fallback branch
160
+ ctx.git.checkout_branch(source_wt, fallback_ref)
161
+
162
+ user_output(f"✓ Moved '{source_branch}' from '{source_wt.name}' to '{target_wt.name}'")
163
+
164
+
165
+ def execute_swap(
166
+ ctx: ErkContext,
167
+ repo_root: Path,
168
+ source_wt: Path,
169
+ target_wt: Path,
170
+ *,
171
+ force: bool,
172
+ ) -> None:
173
+ """Execute swap operation (both worktrees exist with branches).
174
+
175
+ Swaps the branches between source and target worktrees.
176
+ """
177
+ worktrees = ctx.git.list_worktrees(repo_root)
178
+ source_branch = get_worktree_branch(worktrees, source_wt)
179
+ target_branch = get_worktree_branch(worktrees, target_wt)
180
+
181
+ if source_branch is None or target_branch is None:
182
+ user_output("Error: Both worktrees must have branches checked out for swap")
183
+ raise SystemExit(1)
184
+
185
+ # Check for uncommitted changes
186
+ if ctx.git.has_uncommitted_changes(source_wt) or ctx.git.has_uncommitted_changes(target_wt):
187
+ if not force:
188
+ user_output(
189
+ "Error: Uncommitted changes detected in one or more worktrees.\n"
190
+ "Commit, stash, or use --force to override."
191
+ )
192
+ raise SystemExit(1)
193
+
194
+ # Confirm swap unless --force
195
+ if not force:
196
+ user_output("This will swap branches between worktrees:")
197
+ user_output(f" '{source_wt.name}': '{source_branch}' → '{target_branch}'")
198
+ user_output(f" '{target_wt.name}': '{target_branch}' → '{source_branch}'")
199
+ if not user_confirm("Continue?", default=False):
200
+ user_output("Swap cancelled")
201
+ raise SystemExit(0)
202
+
203
+ user_output(f"Swapping branches between '{source_wt.name}' and '{target_wt.name}'")
204
+
205
+ # To swap branches between worktrees, we need to avoid having the same branch
206
+ # checked out in two places simultaneously. Strategy:
207
+ # 1. Detach HEAD in source worktree (frees up source_branch)
208
+ # 2. Checkout source_branch in target worktree
209
+ # 3. Checkout target_branch in source worktree
210
+ ctx.git.checkout_detached(source_wt, source_branch)
211
+ ctx.git.checkout_branch(target_wt, source_branch)
212
+ ctx.git.checkout_branch(source_wt, target_branch)
213
+
214
+ user_output(f"✓ Swapped '{source_branch}' ↔ '{target_branch}'")
215
+
216
+
217
+ @click.command("move")
218
+ @click.option("--current", is_flag=True, help="Use current worktree as source")
219
+ @click.option("--branch", help="Auto-detect worktree containing this branch")
220
+ @click.option("--worktree", help="Use specific worktree as source")
221
+ @click.option("--ref", default="main", help="Fallback branch for source after move (default: main)")
222
+ @click.option("-f", "--force", is_flag=True, help="Skip confirmation prompts")
223
+ @click.argument("target", required=True, shell_complete=complete_worktree_names)
224
+ @click.pass_obj
225
+ def move_stack(
226
+ ctx: ErkContext,
227
+ current: bool,
228
+ branch: str | None,
229
+ worktree: str | None,
230
+ ref: str,
231
+ force: bool,
232
+ target: str,
233
+ ) -> None:
234
+ """Move branches between worktrees with explicit source specification.
235
+
236
+ Examples:
237
+
238
+ \b
239
+ # Move current branch back to repository root
240
+ erk move root
241
+
242
+ \b
243
+ # Move from current worktree to new worktree
244
+ erk move target-wt
245
+
246
+ \b
247
+ # Move from current worktree (explicit)
248
+ erk move --current target-wt
249
+
250
+ \b
251
+ # Auto-detect source from branch name
252
+ erk move --branch feature-x new-wt
253
+
254
+ \b
255
+ # Move from specific source to target
256
+ erk move --worktree old-wt new-wt
257
+
258
+ \b
259
+ # Swap branches between current and another worktree
260
+ erk move --current existing-wt
261
+
262
+ \b
263
+ # Force operation without prompts (for scripts)
264
+ erk move --current target-wt --force
265
+
266
+ \b
267
+ # Specify custom fallback branch
268
+ erk move --current new-wt --ref develop
269
+ """
270
+ # Discover repository context
271
+ repo = discover_repo_context(ctx, ctx.cwd)
272
+ ensure_erk_metadata_dir(repo)
273
+
274
+ # Resolve source worktree
275
+ source_wt = resolve_source_worktree(
276
+ ctx,
277
+ repo.root,
278
+ current=current,
279
+ branch=branch,
280
+ worktree=worktree,
281
+ worktrees_dir=repo.worktrees_dir,
282
+ )
283
+
284
+ # Resolve target worktree path
285
+ # Special case: "root" refers to the main repository root (not current worktree)
286
+ if target == "root":
287
+ # main_repo_root is always set by RepoContext.__post_init__, but ty doesn't know
288
+ target_wt = repo.main_repo_root if repo.main_repo_root else repo.root
289
+ else:
290
+ target_wt = worktree_path_for(repo.worktrees_dir, target)
291
+
292
+ # Validate source and target are different
293
+ if source_wt.resolve() == target_wt.resolve():
294
+ user_output("Error: Source and target worktrees are the same")
295
+ raise SystemExit(1)
296
+
297
+ # Detect operation type
298
+ operation_type = detect_operation_type(source_wt, target_wt, ctx, repo.root)
299
+
300
+ # Execute operation
301
+ if operation_type == MoveOperationType.SWAP:
302
+ execute_swap(ctx, repo.root, source_wt, target_wt, force=force)
303
+ else:
304
+ # Auto-detect default branch if using 'main' default and it doesn't exist
305
+ if ref == "main":
306
+ detected_default = ctx.git.detect_trunk_branch(repo.root)
307
+ ref = detected_default
308
+
309
+ execute_move(ctx, repo.root, source_wt, target_wt, ref, force=force)
@@ -0,0 +1,64 @@
1
+ # Split Command
2
+
3
+ The split command creates individual worktrees for each branch in a Graphite stack, implementing the ephemeral worktree pattern. It's the inverse of the consolidate command.
4
+
5
+ ## Overview
6
+
7
+ Split takes a consolidated worktree (where multiple branches exist in one worktree via `gt stack checkout`) and creates individual worktrees for each branch. This allows parallel development across multiple branches in a stack.
8
+
9
+ ## Module Structure
10
+
11
+ - `command.py` - CLI entry point, validation, and orchestration
12
+ - `plan.py` - Models, planning logic, and worktree creation
13
+ - `display.py` - Output formatting and user interaction
14
+
15
+ ## Key Concepts
16
+
17
+ ### Exclusions
18
+
19
+ The split command automatically excludes:
20
+
21
+ - **Trunk branch** (main/master) - stays in the root worktree
22
+ - **Current branch** - already checked out, can't have duplicate worktrees
23
+ - **Existing worktrees** - idempotent operation, preserves existing worktrees
24
+
25
+ ### Stack Filtering
26
+
27
+ - `--up` - Split only upstack (current branch to leaf)
28
+ - `--down` - Split only downstack (trunk to current branch)
29
+ - Default - Split entire stack (trunk to leaf)
30
+
31
+ ## Workflow Phases
32
+
33
+ 1. **Validation** - Check flags, trunk availability, uncommitted changes
34
+ 2. **Discovery** - Get Graphite stack branches
35
+ 3. **Filtering** - Apply --up/--down filters
36
+ 4. **Planning** - Identify branches needing worktrees
37
+ 5. **Preview** - Show what will be created
38
+ 6. **Confirmation** - User approval (unless --force or --dry-run)
39
+ 7. **Execution** - Create worktrees
40
+ 8. **Results** - Display what was created
41
+
42
+ ## Usage Examples
43
+
44
+ ```bash
45
+ # Split full stack into worktrees
46
+ erk split
47
+
48
+ # Split only upstack branches
49
+ erk split --up
50
+
51
+ # Preview without creating
52
+ erk split --dry-run
53
+
54
+ # Skip confirmation
55
+ erk split --force
56
+ ```
57
+
58
+ ## Integration
59
+
60
+ The split command integrates with:
61
+
62
+ - **Graphite** - Uses gt to determine stack structure
63
+ - **Git worktrees** - Creates standard git worktrees
64
+ - **Erks directory** - Places worktrees in `.erks/`
@@ -0,0 +1,5 @@
1
+ """Split command subpackage - split stacks into individual worktrees.
2
+
3
+ Import from command submodule:
4
+ - command: split_cmd
5
+ """
@@ -0,0 +1,233 @@
1
+ """Split command - CLI entry point, validation, and orchestration."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.commands.stack.split_old.display import (
8
+ confirm_split,
9
+ display_creation_preview,
10
+ display_results,
11
+ display_stack_preview,
12
+ )
13
+ from erk.cli.commands.stack.split_old.plan import (
14
+ create_split_plan,
15
+ execute_split_plan,
16
+ get_stack_branches,
17
+ )
18
+ from erk.cli.core import discover_repo_context
19
+ from erk.cli.graphite_command import GraphiteCommand
20
+ from erk.core.context import ErkContext
21
+ from erk_shared.naming import sanitize_worktree_name
22
+ from erk_shared.output.output import user_output
23
+
24
+ # Validation functions
25
+
26
+
27
+ def validate_flags(up: bool, down: bool) -> None:
28
+ """Validate that --up and --down are not used together.
29
+
30
+ Raises:
31
+ SystemExit: If both flags are set
32
+ """
33
+ if up and down:
34
+ user_output(click.style("❌ Error: Cannot use --up and --down together", fg="red"))
35
+ user_output(
36
+ "Use either --up (split upstack) or --down (split downstack) or neither (full stack)"
37
+ )
38
+ raise SystemExit(1)
39
+
40
+
41
+ def validate_trunk_branch(trunk_branch: str | None) -> None:
42
+ """Validate trunk branch is available.
43
+
44
+ Raises:
45
+ SystemExit: If trunk branch cannot be determined
46
+ """
47
+ if not trunk_branch:
48
+ user_output(click.style("❌ Error: Cannot determine trunk branch", fg="red"))
49
+ user_output("Initialize repository or configure trunk branch")
50
+ raise SystemExit(1)
51
+
52
+
53
+ def check_uncommitted_changes(
54
+ ctx: ErkContext,
55
+ current_worktree: Path,
56
+ force: bool,
57
+ dry_run: bool,
58
+ ) -> None:
59
+ """Check for uncommitted changes unless --force or --dry-run.
60
+
61
+ Raises:
62
+ SystemExit: If uncommitted changes detected
63
+ """
64
+ if not force and not dry_run:
65
+ if ctx.git.has_uncommitted_changes(current_worktree):
66
+ user_output(click.style("❌ Error: Uncommitted changes detected", fg="red", bold=True))
67
+ user_output("\nCommit or stash changes before running split")
68
+ raise SystemExit(1)
69
+
70
+
71
+ # Stack filtering
72
+
73
+
74
+ def apply_stack_filter(
75
+ stack_branches: list[str],
76
+ current_branch: str | None,
77
+ up: bool,
78
+ down: bool,
79
+ ) -> list[str]:
80
+ """Apply --up or --down filters to determine which branches to split.
81
+
82
+ Args:
83
+ stack_branches: Full stack from trunk to leaf
84
+ current_branch: Currently checked out branch (None if detached)
85
+ up: If True, only split upstack (current to leaf)
86
+ down: If True, only split downstack (trunk to current)
87
+
88
+ Returns:
89
+ Filtered list of branches to split
90
+
91
+ Notes:
92
+ - If both up and down are False, returns full stack
93
+ - If current_branch is None, filters have no effect
94
+ - If current_branch is not in stack, returns empty list
95
+ """
96
+ if up and current_branch is not None:
97
+ # Only split upstack (from current to leaf)
98
+ if current_branch in stack_branches:
99
+ current_index = stack_branches.index(current_branch)
100
+ return stack_branches[current_index:]
101
+ else:
102
+ # Current branch not in stack, split nothing
103
+ return []
104
+ elif down and current_branch is not None:
105
+ # Only split downstack (from trunk to current)
106
+ if current_branch in stack_branches:
107
+ current_index = stack_branches.index(current_branch)
108
+ return stack_branches[: current_index + 1]
109
+ else:
110
+ # Current branch not in stack, split nothing
111
+ return []
112
+ else:
113
+ # Split entire stack
114
+ return stack_branches
115
+
116
+
117
+ # Main CLI command
118
+
119
+
120
+ @click.command("split", cls=GraphiteCommand)
121
+ @click.option("-f", "--force", is_flag=True, help="Skip confirmation prompt")
122
+ @click.option(
123
+ "--dry-run",
124
+ is_flag=True,
125
+ default=False,
126
+ help="Show what worktrees would be created without executing",
127
+ )
128
+ @click.option(
129
+ "--up",
130
+ is_flag=True,
131
+ help="Only split upstack (current branch to leaf). Default is entire stack.",
132
+ )
133
+ @click.option(
134
+ "--down",
135
+ is_flag=True,
136
+ help="Only split downstack (trunk to current branch). Default is entire stack.",
137
+ )
138
+ @click.pass_obj
139
+ def split_cmd(
140
+ ctx: ErkContext,
141
+ force: bool,
142
+ dry_run: bool,
143
+ up: bool,
144
+ down: bool,
145
+ ) -> None:
146
+ """Split a stack into individual worktrees per branch.
147
+
148
+ This is the inverse of consolidate - it creates individual worktrees for each
149
+ branch in the stack (except trunk and the current branch).
150
+
151
+ By default, splits the full stack (trunk to leaf). With --up or --down, splits
152
+ only a portion of the stack.
153
+
154
+ This command is useful after consolidating branches for operations like
155
+ 'gt restack', allowing you to return to the ephemeral worktree pattern.
156
+
157
+ \b
158
+ Examples:
159
+ # Split full stack into individual worktrees (default)
160
+ $ erk split
161
+
162
+ # Split only upstack (current to leaf)
163
+ $ erk split --up
164
+
165
+ # Split only downstack (trunk to current)
166
+ $ erk split --down
167
+
168
+ # Preview changes without executing
169
+ $ erk split --dry-run
170
+
171
+ # Skip confirmation prompt
172
+ $ erk split --force
173
+
174
+ Notes:
175
+ - Trunk branch (main/master) stays in root worktree
176
+ - Current branch cannot get its own worktree (already checked out)
177
+ - Existing worktrees are preserved (idempotent operation)
178
+ - Creates worktrees in the .erks directory
179
+ """
180
+ # 1. Validate input flags
181
+ validate_flags(up, down)
182
+
183
+ # 2. Gather repository context
184
+ current_worktree = ctx.cwd
185
+ current_branch = ctx.git.get_current_branch(current_worktree)
186
+ repo = discover_repo_context(ctx, current_worktree)
187
+ trunk_branch = ctx.trunk_branch
188
+ validate_trunk_branch(trunk_branch)
189
+ # After validation, trunk_branch is guaranteed to be non-None
190
+ assert trunk_branch is not None # Type narrowing for mypy/ty
191
+
192
+ # 3. Get stack branches
193
+ stack_branches = get_stack_branches(ctx, repo.root, current_branch, trunk_branch)
194
+
195
+ # 4. Apply stack filters
196
+ stack_to_split = apply_stack_filter(stack_branches, current_branch, up, down)
197
+
198
+ # 5. Safety checks
199
+ check_uncommitted_changes(ctx, current_worktree, force, dry_run)
200
+
201
+ # 6. Create split plan
202
+ all_worktrees = ctx.git.list_worktrees(repo.root)
203
+ plan = create_split_plan(
204
+ stack_branches=stack_to_split,
205
+ trunk_branch=trunk_branch,
206
+ current_branch=current_branch,
207
+ all_worktrees=all_worktrees,
208
+ worktrees_dir=repo.worktrees_dir,
209
+ sanitize_worktree_name=sanitize_worktree_name,
210
+ source_worktree_path=current_worktree,
211
+ repo_root=repo.root,
212
+ )
213
+
214
+ # 7. Display preview
215
+ display_stack_preview(stack_to_split, trunk_branch, current_branch, plan)
216
+ display_creation_preview(plan, dry_run)
217
+
218
+ # Early exit if nothing to do
219
+ if not plan.branches_to_split:
220
+ return
221
+
222
+ # 8. Get user confirmation
223
+ confirm_split(force, dry_run)
224
+
225
+ # 9. Execute or simulate
226
+ user_output("")
227
+ if dry_run:
228
+ results = [(branch, plan.target_paths[branch]) for branch in plan.branches_to_split]
229
+ else:
230
+ results = execute_split_plan(plan, ctx.git)
231
+
232
+ # 10. Display results
233
+ display_results(results, dry_run)