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,116 @@
1
+ """Split command display functions - output formatting and user interaction."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.commands.stack.split_old.plan import SplitPlan
8
+ from erk_shared.output.output import user_confirm, user_output
9
+
10
+
11
+ def display_stack_preview(
12
+ stack_to_split: list[str],
13
+ trunk_branch: str,
14
+ current_branch: str | None,
15
+ plan: SplitPlan,
16
+ ) -> None:
17
+ """Display which branches will be split and their status.
18
+
19
+ Shows visual indicators for:
20
+ - Trunk branch (stays in root)
21
+ - Current branch (already checked out)
22
+ - Branches with existing worktrees
23
+ - Branches that will get new worktrees
24
+ """
25
+ user_output("\n" + click.style("Stack to split:", bold=True))
26
+ for b in stack_to_split:
27
+ if b == trunk_branch:
28
+ marker = f" {click.style('←', fg='cyan')} trunk (stays in root)"
29
+ branch_display = click.style(b, fg="cyan")
30
+ elif b == current_branch:
31
+ marker = f" {click.style('←', fg='bright_green')} current (already checked out)"
32
+ branch_display = click.style(b, fg="bright_green", bold=True)
33
+ elif b in plan.existing_worktrees:
34
+ marker = f" {click.style('✓', fg='green')} already has worktree"
35
+ branch_display = click.style(b, fg="green")
36
+ elif b in plan.branches_to_split:
37
+ marker = f" {click.style('→', fg='yellow')} will create worktree"
38
+ branch_display = click.style(b, fg="yellow")
39
+ else:
40
+ marker = ""
41
+ branch_display = click.style(b, fg="white", dim=True)
42
+
43
+ user_output(f" {branch_display}{marker}")
44
+
45
+
46
+ def display_creation_preview(
47
+ plan: SplitPlan,
48
+ dry_run: bool,
49
+ ) -> None:
50
+ """Display which worktrees will be created.
51
+
52
+ Shows paths for each branch that needs a worktree.
53
+ Returns early if no worktrees need to be created.
54
+
55
+ Args:
56
+ plan: The split plan containing branches to split
57
+ dry_run: Whether this is a dry run
58
+ """
59
+ if plan.branches_to_split:
60
+ if dry_run:
61
+ user_output(f"\n{click.style('[DRY RUN] Would create:', fg='yellow', bold=True)}")
62
+ else:
63
+ user_output(f"\n{click.style('Will create:', bold=True)}")
64
+
65
+ for branch in plan.branches_to_split:
66
+ worktree_path = plan.target_paths[branch]
67
+ path_text = click.style(str(worktree_path), fg="cyan")
68
+ branch_text = click.style(branch, fg="yellow")
69
+ user_output(f" - {branch_text} at {path_text}")
70
+ else:
71
+ user_output("\n✅ All branches already have worktrees or are excluded")
72
+
73
+
74
+ def confirm_split(force: bool, dry_run: bool) -> None:
75
+ """Prompt user for confirmation unless --force or --dry-run.
76
+
77
+ Args:
78
+ force: Whether to skip confirmation
79
+ dry_run: Whether this is a dry run
80
+
81
+ Raises:
82
+ SystemExit: If user declines
83
+ """
84
+ if not force and not dry_run:
85
+ user_output("")
86
+ if not user_confirm("Proceed with creating worktrees?", default=False):
87
+ user_output(click.style("⭕ Aborted", fg="yellow"))
88
+ raise SystemExit(1)
89
+
90
+
91
+ def display_results(
92
+ results: list[tuple[str, Path]],
93
+ dry_run: bool,
94
+ ) -> None:
95
+ """Display results of split operation.
96
+
97
+ Shows created worktrees or dry-run simulation results.
98
+
99
+ Args:
100
+ results: List of (branch, worktree_path) tuples
101
+ dry_run: Whether this is a dry run
102
+ """
103
+ if results:
104
+ for branch, worktree_path in results:
105
+ path_text = click.style(str(worktree_path), fg="green")
106
+ branch_text = click.style(branch, fg="yellow")
107
+ if dry_run:
108
+ user_output(f"[DRY RUN] Would create worktree for {branch_text} at {path_text}")
109
+ else:
110
+ user_output(f"✅ Created worktree for {branch_text} at {path_text}")
111
+
112
+ # Summary message
113
+ if dry_run:
114
+ user_output(f"\n{click.style('[DRY RUN] No changes made', fg='yellow')}")
115
+ else:
116
+ user_output(f"\n✅ Split complete: created {len(results)} worktree(s)")
@@ -0,0 +1,216 @@
1
+ """Split command planning logic - models, branch identification, plan creation, and execution."""
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from erk.core.context import ErkContext
8
+ from erk_shared.git.abc import WorktreeInfo
9
+ from erk_shared.output.output import user_output
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SplitPlan:
14
+ """Plan for splitting a stack into individual worktrees.
15
+
16
+ Attributes:
17
+ stack_branches: Full list of branches in the stack (trunk to leaf)
18
+ branches_to_split: Subset of branches needing worktrees
19
+ existing_worktrees: Branches that already have worktrees (informational)
20
+ target_paths: Mapping of branch names to their target worktree paths
21
+ source_worktree_path: Path to the source worktree where we're splitting from
22
+ repo_root: Path to the repository root
23
+ skipped_current: True if current branch was skipped (already checked out)
24
+ skipped_trunk: True if trunk branch was skipped (stays in root worktree)
25
+ """
26
+
27
+ stack_branches: list[str]
28
+ branches_to_split: list[str]
29
+ existing_worktrees: list[str]
30
+ target_paths: dict[str, Path]
31
+ source_worktree_path: Path
32
+ repo_root: Path
33
+ skipped_current: bool
34
+ skipped_trunk: bool
35
+
36
+
37
+ def get_stack_branches(
38
+ ctx: ErkContext,
39
+ repo_root: Path,
40
+ current_branch: str | None,
41
+ trunk_branch: str,
42
+ ) -> list[str]:
43
+ """Get the worktree stack for the current or trunk branch.
44
+
45
+ Handles detached HEAD state by falling back to trunk branch stack.
46
+
47
+ Returns:
48
+ List of branches in the stack (trunk to leaf)
49
+
50
+ Raises:
51
+ SystemExit: If branch is not tracked by Graphite
52
+ """
53
+ if current_branch is None:
54
+ # In detached HEAD state, get the full stack from trunk
55
+ stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo_root, trunk_branch)
56
+ if stack_branches is None:
57
+ user_output(f"Error: Trunk branch '{trunk_branch}' is not tracked by Graphite")
58
+ raise SystemExit(1)
59
+ else:
60
+ # Get current branch's stack
61
+ stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo_root, current_branch)
62
+ if stack_branches is None:
63
+ user_output(f"Error: Branch '{current_branch}' is not tracked by Graphite")
64
+ user_output(
65
+ "Run 'gt repo init' to initialize Graphite, or use 'gt track' to track this branch"
66
+ )
67
+ raise SystemExit(1)
68
+
69
+ return stack_branches
70
+
71
+
72
+ def identify_splittable_branches(
73
+ stack_branches: list[str],
74
+ trunk_branch: str,
75
+ current_branch: str | None,
76
+ all_worktrees: list[WorktreeInfo],
77
+ ) -> tuple[list[str], list[str], bool, bool]:
78
+ """Identify which branches need new worktrees.
79
+
80
+ A branch needs a worktree if:
81
+ 1. It's not the trunk branch (trunk stays in root worktree)
82
+ 2. It's not the currently checked out branch (git prevents duplicate checkouts)
83
+ 3. It doesn't already have a worktree
84
+
85
+ Args:
86
+ stack_branches: Full stack from trunk to leaf
87
+ trunk_branch: The trunk branch name (main or master)
88
+ current_branch: Currently checked out branch (None if detached)
89
+ all_worktrees: All existing worktrees in the repository
90
+
91
+ Returns:
92
+ Tuple of (branches_to_split, existing_worktrees, skipped_current, skipped_trunk)
93
+ - branches_to_split: Branches that need new worktrees
94
+ - existing_worktrees: Branches that already have worktrees
95
+ - skipped_current: True if current branch was skipped
96
+ - skipped_trunk: True if trunk branch was skipped
97
+ """
98
+ branches_to_split = []
99
+ existing_worktrees = []
100
+ skipped_current = False
101
+ skipped_trunk = False
102
+
103
+ # Build set of branches that already have worktrees
104
+ branches_with_worktrees = {wt.branch for wt in all_worktrees if wt.branch is not None}
105
+
106
+ for branch in stack_branches:
107
+ # Track if this branch is trunk and/or current (can be both)
108
+ is_trunk = branch == trunk_branch
109
+ is_current = branch == current_branch
110
+
111
+ # Skip trunk branch - it stays in root worktree
112
+ if is_trunk:
113
+ skipped_trunk = True
114
+
115
+ # Skip current branch - can't create worktree for checked out branch
116
+ if is_current:
117
+ skipped_current = True
118
+
119
+ # Skip this branch if it's trunk or current
120
+ if is_trunk or is_current:
121
+ continue
122
+
123
+ # Check if branch already has a worktree
124
+ if branch in branches_with_worktrees:
125
+ existing_worktrees.append(branch)
126
+ else:
127
+ branches_to_split.append(branch)
128
+
129
+ return branches_to_split, existing_worktrees, skipped_current, skipped_trunk
130
+
131
+
132
+ def create_split_plan(
133
+ stack_branches: list[str],
134
+ trunk_branch: str,
135
+ current_branch: str | None,
136
+ all_worktrees: list[WorktreeInfo],
137
+ worktrees_dir: Path,
138
+ sanitize_worktree_name: Callable[[str], str],
139
+ source_worktree_path: Path,
140
+ repo_root: Path,
141
+ ) -> SplitPlan:
142
+ """Create a complete split plan.
143
+
144
+ Args:
145
+ stack_branches: Full stack from trunk to leaf
146
+ trunk_branch: The trunk branch name (main or master)
147
+ current_branch: Currently checked out branch (None if detached)
148
+ all_worktrees: All existing worktrees in the repository
149
+ worktrees_dir: Directory containing worktrees
150
+ sanitize_worktree_name: Function to convert branch name to valid worktree name
151
+ source_worktree_path: Path to the current worktree we're splitting from
152
+ repo_root: Path to the repository root
153
+
154
+ Returns:
155
+ Complete split plan with all information needed for execution
156
+ """
157
+ result = identify_splittable_branches(
158
+ stack_branches, trunk_branch, current_branch, all_worktrees
159
+ )
160
+ branches_to_split, existing_worktrees, skipped_current, skipped_trunk = result
161
+
162
+ # Build target paths for branches needing worktrees
163
+ target_paths = {}
164
+ for branch in branches_to_split:
165
+ worktree_name = sanitize_worktree_name(branch)
166
+ target_paths[branch] = worktrees_dir / worktree_name
167
+
168
+ return SplitPlan(
169
+ stack_branches=stack_branches,
170
+ branches_to_split=branches_to_split,
171
+ existing_worktrees=existing_worktrees,
172
+ target_paths=target_paths,
173
+ source_worktree_path=source_worktree_path,
174
+ repo_root=repo_root,
175
+ skipped_current=skipped_current,
176
+ skipped_trunk=skipped_trunk,
177
+ )
178
+
179
+
180
+ def execute_split_plan(
181
+ plan: SplitPlan,
182
+ git_ops, # Type annotation omitted to avoid circular import
183
+ ) -> list[tuple[str, Path]]:
184
+ """Execute a split plan by creating worktrees.
185
+
186
+ The actual execution depends on the Git implementation passed in:
187
+ - RealGit: Actually creates the worktrees
188
+ - DryRunGit: No-op execution for dry-run mode
189
+ - PrintingGit: Prints operations (wraps either Real or Noop)
190
+
191
+ Args:
192
+ plan: The split plan to execute
193
+ git_ops: Git instance for performing git operations
194
+
195
+ Returns:
196
+ List of (branch, worktree_path) tuples indicating what was processed
197
+ """
198
+ results = []
199
+
200
+ for branch in plan.branches_to_split:
201
+ target_path = plan.target_paths[branch]
202
+
203
+ # Create worktree for existing branch
204
+ # Using create_branch=False since branch already exists
205
+ # The actual behavior depends on the injected Git implementation
206
+ git_ops.add_worktree(
207
+ plan.repo_root,
208
+ target_path,
209
+ branch=branch,
210
+ ref=None,
211
+ create_branch=False,
212
+ )
213
+
214
+ results.append((branch, target_path))
215
+
216
+ return results
@@ -0,0 +1,58 @@
1
+ """Status command implementation."""
2
+
3
+ import click
4
+
5
+ from erk.cli.core import discover_repo_context
6
+ from erk.cli.ensure import Ensure
7
+ from erk.core.context import ErkContext
8
+ from erk.status.collectors.git import GitStatusCollector
9
+ from erk.status.collectors.github import GitHubPRCollector
10
+ from erk.status.collectors.graphite import GraphiteStackCollector
11
+ from erk.status.collectors.impl import PlanFileCollector
12
+ from erk.status.orchestrator import StatusOrchestrator
13
+ from erk.status.renderers.simple import SimpleRenderer
14
+ from erk_shared.gateway.parallel.real import RealParallelTaskRunner
15
+
16
+
17
+ @click.command("status")
18
+ @click.pass_obj
19
+ def status_cmd(ctx: ErkContext) -> None:
20
+ """Show comprehensive status of current worktree."""
21
+ # Discover repository context
22
+ repo = discover_repo_context(ctx, ctx.cwd)
23
+ current_dir = ctx.cwd.resolve()
24
+
25
+ # Find which worktree we're in
26
+ worktrees = ctx.git.list_worktrees(repo.root)
27
+ current_worktree_path = None
28
+
29
+ for wt in worktrees:
30
+ # Check path exists before resolution/comparison to avoid OSError
31
+ if wt.path.exists():
32
+ wt_path_resolved = wt.path.resolve()
33
+ # Use is_relative_to only after confirming path exists
34
+ if current_dir == wt_path_resolved or current_dir.is_relative_to(wt_path_resolved):
35
+ current_worktree_path = wt_path_resolved
36
+ break
37
+
38
+ Ensure.in_git_worktree(ctx, current_worktree_path)
39
+ # After Ensure check, we know current_worktree_path is not None
40
+ assert current_worktree_path is not None
41
+
42
+ # Create collectors
43
+ collectors = [
44
+ GitStatusCollector(),
45
+ GraphiteStackCollector(),
46
+ GitHubPRCollector(),
47
+ PlanFileCollector(),
48
+ ]
49
+
50
+ # Create orchestrator
51
+ orchestrator = StatusOrchestrator(collectors, runner=RealParallelTaskRunner())
52
+
53
+ # Collect status
54
+ status = orchestrator.collect_status(ctx, current_worktree_path, repo.root)
55
+
56
+ # Render status
57
+ renderer = SimpleRenderer()
58
+ renderer.render(status)