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,459 @@
1
+ """Command to start a planning session in an assigned pool slot.
2
+
3
+ This command assigns a pool slot and launches Claude for planning,
4
+ without requiring a pre-existing plan file or GitHub issue.
5
+ """
6
+
7
+ import sys
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+ from erk.cli.activation import render_activation_script
14
+ from erk.cli.commands.implement_shared import normalize_model_name
15
+ from erk.cli.commands.slot.common import (
16
+ find_branch_assignment,
17
+ find_inactive_slot,
18
+ find_next_available_slot,
19
+ generate_slot_name,
20
+ get_pool_size,
21
+ handle_pool_full_interactive,
22
+ )
23
+ from erk.cli.commands.wt.create_cmd import run_post_worktree_setup
24
+ from erk.cli.config import LoadedConfig
25
+ from erk.cli.core import discover_repo_context
26
+ from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
27
+ from erk.core.context import ErkContext
28
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
29
+ from erk.core.worktree_pool import (
30
+ PoolState,
31
+ SlotAssignment,
32
+ load_pool_state,
33
+ save_pool_state,
34
+ )
35
+ from erk.core.worktree_utils import compute_relative_path_in_worktree
36
+ from erk_shared.naming import sanitize_worktree_name
37
+ from erk_shared.output.output import user_output
38
+
39
+
40
+ def _generate_timestamp_name() -> str:
41
+ """Generate a timestamp-based branch name for planning.
42
+
43
+ Returns:
44
+ Branch name like "planning-01-04-0930"
45
+ """
46
+ now = datetime.now(tz=UTC)
47
+ return f"planning-{now.strftime('%m-%d-%H%M')}"
48
+
49
+
50
+ def _determine_base_branch(ctx: ErkContext, repo_root: Path) -> str:
51
+ """Determine the base branch for new worktree creation.
52
+
53
+ When Graphite is enabled and the user is on a non-trunk branch,
54
+ stack on the current branch. Otherwise, use trunk.
55
+
56
+ Args:
57
+ ctx: Erk context
58
+ repo_root: Repository root path
59
+
60
+ Returns:
61
+ Base branch name to use as ref for worktree creation
62
+ """
63
+ trunk_branch = ctx.git.detect_trunk_branch(repo_root)
64
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
65
+
66
+ if not use_graphite:
67
+ return trunk_branch
68
+
69
+ current_branch = ctx.git.get_current_branch(ctx.cwd)
70
+ if current_branch and current_branch != trunk_branch:
71
+ return current_branch
72
+
73
+ return trunk_branch
74
+
75
+
76
+ def _build_claude_command(dangerous: bool, model: str | None) -> str:
77
+ """Build a Claude CLI invocation without a slash command.
78
+
79
+ Args:
80
+ dangerous: Whether to skip permission prompts
81
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
82
+
83
+ Returns:
84
+ Complete Claude CLI command string
85
+ """
86
+ cmd = "claude --permission-mode acceptEdits"
87
+ if dangerous:
88
+ cmd += " --dangerously-skip-permissions"
89
+ if model is not None:
90
+ cmd += f" --model {model}"
91
+ return cmd
92
+
93
+
94
+ @click.command("start", cls=CommandWithHiddenOptions)
95
+ @click.option(
96
+ "-n",
97
+ "--name",
98
+ type=str,
99
+ help="Branch name for the planning session (auto-generated if not provided)",
100
+ )
101
+ @click.option(
102
+ "--dry-run",
103
+ is_flag=True,
104
+ help="Print what would be executed without doing it",
105
+ )
106
+ @click.option(
107
+ "--dangerous",
108
+ is_flag=True,
109
+ help="Skip permission prompts by passing --dangerously-skip-permissions to Claude",
110
+ )
111
+ @script_option
112
+ @click.option(
113
+ "-m",
114
+ "--model",
115
+ type=str,
116
+ help="Model to use for Claude (haiku/h, sonnet/s, opus/o)",
117
+ )
118
+ @click.option(
119
+ "-f",
120
+ "--force",
121
+ is_flag=True,
122
+ help="Auto-unassign oldest slot if pool is full (no interactive prompt).",
123
+ )
124
+ @click.pass_obj
125
+ def plan_start(
126
+ ctx: ErkContext,
127
+ name: str | None,
128
+ dry_run: bool,
129
+ dangerous: bool,
130
+ script: bool,
131
+ model: str | None,
132
+ force: bool,
133
+ ) -> None:
134
+ """Start a planning session in an assigned pool slot.
135
+
136
+ This command assigns a pool slot, creates a planning branch, and launches
137
+ Claude for planning. Use this when you want to explore a problem space
138
+ before committing to a specific plan.
139
+
140
+ If --name is provided, it will be used as the branch name (sanitized).
141
+ Otherwise, a timestamp-based name like "planning-01-04-0930" is generated.
142
+
143
+ Examples:
144
+
145
+ \b
146
+ # Start planning with auto-generated branch name
147
+ erk plan start
148
+
149
+ \b
150
+ # Start planning with a custom branch name
151
+ erk plan start --name my-feature
152
+
153
+ \b
154
+ # Skip permission prompts
155
+ erk plan start --dangerous
156
+
157
+ \b
158
+ # Shell integration
159
+ source <(erk plan start --script)
160
+
161
+ \b
162
+ # With specific model
163
+ erk plan start --model opus
164
+ """
165
+ # Normalize model name (validates and expands aliases)
166
+ model = normalize_model_name(model)
167
+
168
+ # Discover repository context
169
+ repo = discover_repo_context(ctx, ctx.cwd)
170
+ ensure_erk_metadata_dir(repo)
171
+ repo_root = repo.root
172
+
173
+ # Determine base branch (respects worktree stacking)
174
+ base_branch = _determine_base_branch(ctx, repo_root)
175
+
176
+ # Generate or sanitize branch name
177
+ if name is not None:
178
+ branch = sanitize_worktree_name(name)
179
+ else:
180
+ branch = _generate_timestamp_name()
181
+
182
+ ctx.feedback.info(f"Planning branch: {branch}")
183
+
184
+ # Get pool size from config
185
+ pool_size = get_pool_size(ctx)
186
+
187
+ # Load or create pool state
188
+ state = load_pool_state(repo.pool_json_path)
189
+ if state is None:
190
+ state = PoolState(
191
+ version="1.0",
192
+ pool_size=pool_size,
193
+ slots=(),
194
+ assignments=(),
195
+ )
196
+ elif state.pool_size != pool_size:
197
+ # Update pool_size from config if it changed
198
+ state = PoolState(
199
+ version=state.version,
200
+ pool_size=pool_size,
201
+ slots=state.slots,
202
+ assignments=state.assignments,
203
+ )
204
+
205
+ # Check if branch is already assigned to a slot
206
+ existing_assignment = find_branch_assignment(state, branch)
207
+ if existing_assignment is not None:
208
+ # Branch already has a slot - use it
209
+ slot_name = existing_assignment.slot_name
210
+ wt_path = existing_assignment.worktree_path
211
+ ctx.feedback.info(f"Branch '{branch}' already assigned to {slot_name}")
212
+
213
+ # Handle dry-run mode
214
+ if dry_run:
215
+ _show_dry_run_output(slot_name, branch, dangerous, model)
216
+ return
217
+
218
+ # Execute planning session
219
+ _execute_planning(ctx, repo_root, wt_path, branch, dangerous, model, script)
220
+ return
221
+
222
+ # Check if branch already exists locally
223
+ local_branches = ctx.git.list_local_branches(repo_root)
224
+ use_existing_branch = branch in local_branches
225
+
226
+ # Find available slot
227
+ inactive_slot = find_inactive_slot(state, ctx.git, repo_root)
228
+ if inactive_slot is not None:
229
+ # Fast path: reuse existing worktree
230
+ slot_name, wt_path = inactive_slot
231
+ else:
232
+ # Find next available slot number
233
+ slot_num = find_next_available_slot(state, repo.worktrees_dir)
234
+ if slot_num is None:
235
+ # Pool is full - handle interactively or with --force
236
+ to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
237
+ if to_unassign is None:
238
+ raise SystemExit(1) from None
239
+
240
+ # Remove the assignment from state
241
+ new_assignments = tuple(
242
+ a for a in state.assignments if a.slot_name != to_unassign.slot_name
243
+ )
244
+ state = PoolState(
245
+ version=state.version,
246
+ pool_size=state.pool_size,
247
+ slots=state.slots,
248
+ assignments=new_assignments,
249
+ )
250
+ save_pool_state(repo.pool_json_path, state)
251
+ user_output(
252
+ click.style("✓ ", fg="green")
253
+ + f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
254
+ + f"from {click.style(to_unassign.slot_name, fg='cyan')}"
255
+ )
256
+
257
+ # Use the slot we just unassigned (it has a worktree directory that can be reused)
258
+ slot_name = to_unassign.slot_name
259
+ wt_path = to_unassign.worktree_path
260
+ else:
261
+ slot_name = generate_slot_name(slot_num)
262
+ wt_path = repo.worktrees_dir / slot_name
263
+
264
+ # Handle dry-run mode
265
+ if dry_run:
266
+ _show_dry_run_output(slot_name, branch, dangerous, model)
267
+ return
268
+
269
+ # Create worktree at slot path
270
+ ctx.feedback.info(f"Assigning to slot '{slot_name}'...")
271
+
272
+ # Load local config
273
+ config = ctx.local_config if ctx.local_config is not None else LoadedConfig.test()
274
+
275
+ # Respect global use_graphite config
276
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
277
+
278
+ if inactive_slot is not None:
279
+ # Fast path: checkout branch in existing worktree
280
+ if use_existing_branch:
281
+ ctx.feedback.info(f"Checking out existing branch '{branch}'...")
282
+ ctx.git.checkout_branch(wt_path, branch)
283
+ else:
284
+ # Create branch and checkout
285
+ ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
286
+ ctx.git.create_branch(repo_root, branch, base_branch)
287
+ if use_graphite:
288
+ ctx.graphite.track_branch(repo_root, branch, base_branch)
289
+ ctx.git.checkout_branch(wt_path, branch)
290
+ else:
291
+ # On-demand slot creation
292
+ if not use_existing_branch:
293
+ # Create branch first
294
+ ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
295
+ ctx.git.create_branch(repo_root, branch, base_branch)
296
+ if use_graphite:
297
+ ctx.graphite.track_branch(repo_root, branch, base_branch)
298
+
299
+ # Check if worktree directory already exists (from pool initialization)
300
+ if wt_path.exists():
301
+ # Worktree already exists - check out the branch
302
+ ctx.git.checkout_branch(wt_path, branch)
303
+ else:
304
+ # Create directory for worktree
305
+ wt_path.mkdir(parents=True, exist_ok=True)
306
+
307
+ # Add worktree
308
+ ctx.git.add_worktree(
309
+ repo_root,
310
+ wt_path,
311
+ branch=branch,
312
+ ref=None,
313
+ create_branch=False,
314
+ )
315
+
316
+ ctx.feedback.success(f"✓ Assigned {branch} to {slot_name}")
317
+
318
+ # Create slot assignment
319
+ now = ctx.time.now().isoformat()
320
+ new_assignment = SlotAssignment(
321
+ slot_name=slot_name,
322
+ branch_name=branch,
323
+ assigned_at=now,
324
+ worktree_path=wt_path,
325
+ )
326
+
327
+ # Update state with new assignment
328
+ new_state = PoolState(
329
+ version=state.version,
330
+ pool_size=state.pool_size,
331
+ slots=state.slots,
332
+ assignments=(*state.assignments, new_assignment),
333
+ )
334
+
335
+ # Save state
336
+ save_pool_state(repo.pool_json_path, new_state)
337
+
338
+ # Run post-worktree setup
339
+ run_post_worktree_setup(ctx, config, wt_path, repo_root, slot_name)
340
+
341
+ # Execute planning session
342
+ _execute_planning(ctx, repo_root, wt_path, branch, dangerous, model, script)
343
+
344
+
345
+ def _show_dry_run_output(
346
+ slot_name: str,
347
+ branch: str,
348
+ dangerous: bool,
349
+ model: str | None,
350
+ ) -> None:
351
+ """Show dry-run output for slot assignment."""
352
+ dry_run_header = click.style("Dry-run mode:", fg="cyan", bold=True)
353
+ user_output(dry_run_header + " No changes will be made\n")
354
+
355
+ user_output(f"Would assign to slot '{slot_name}'")
356
+ user_output(f" Branch: {branch}")
357
+
358
+ user_output("\nWould launch Claude:")
359
+ claude_cmd = _build_claude_command(dangerous, model)
360
+ user_output(f" {claude_cmd}")
361
+
362
+
363
+ def _execute_planning(
364
+ ctx: ErkContext,
365
+ repo_root: Path,
366
+ wt_path: Path,
367
+ branch: str,
368
+ dangerous: bool,
369
+ model: str | None,
370
+ script: bool,
371
+ ) -> None:
372
+ """Execute planning session - script mode or interactive.
373
+
374
+ Args:
375
+ ctx: Erk context
376
+ repo_root: Repository root path
377
+ wt_path: Worktree path
378
+ branch: Branch name
379
+ dangerous: Whether to skip permission prompts
380
+ model: Optional model name
381
+ script: Whether to output shell script instead of launching Claude
382
+ """
383
+ if script:
384
+ # Script mode - output activation script
385
+ _output_activation_script(ctx, wt_path, branch, dangerous, model)
386
+ else:
387
+ # Interactive mode - launch Claude
388
+ _launch_claude_interactive(ctx, repo_root, wt_path, dangerous, model)
389
+
390
+
391
+ def _output_activation_script(
392
+ ctx: ErkContext,
393
+ wt_path: Path,
394
+ branch: str,
395
+ dangerous: bool,
396
+ model: str | None,
397
+ ) -> None:
398
+ """Output activation script for shell integration.
399
+
400
+ Args:
401
+ ctx: Erk context
402
+ wt_path: Worktree path
403
+ branch: Branch name (for comment)
404
+ dangerous: Whether to skip permission prompts
405
+ model: Optional model name
406
+ """
407
+ # Build Claude command (no slash command)
408
+ claude_cmd = _build_claude_command(dangerous, model)
409
+
410
+ # Get base activation script (cd + venv + env)
411
+ full_script = render_activation_script(
412
+ worktree_path=wt_path,
413
+ target_subpath=None,
414
+ post_cd_commands=None,
415
+ final_message=claude_cmd,
416
+ comment=f"plan start {branch}",
417
+ )
418
+
419
+ result = ctx.script_writer.write_activation_script(
420
+ full_script,
421
+ command_name="plan-start",
422
+ comment=f"activate {wt_path.name} and launch Claude for planning",
423
+ )
424
+
425
+ result.output_for_shell_integration()
426
+
427
+
428
+ def _launch_claude_interactive(
429
+ ctx: ErkContext,
430
+ repo_root: Path,
431
+ wt_path: Path,
432
+ dangerous: bool,
433
+ model: str | None,
434
+ ) -> None:
435
+ """Launch Claude in interactive mode for planning.
436
+
437
+ Args:
438
+ ctx: Erk context
439
+ repo_root: Repository root path
440
+ wt_path: Worktree path
441
+ dangerous: Whether to skip permission prompts
442
+ model: Optional model name
443
+
444
+ Note:
445
+ This function never returns in production - the process is replaced by Claude
446
+ """
447
+ click.echo("Entering interactive planning mode...", err=True)
448
+ try:
449
+ # Launch Claude without a slash command (empty string)
450
+ # The executor handles empty command by not appending it to args
451
+ ctx.claude_executor.execute_interactive(
452
+ wt_path,
453
+ dangerous,
454
+ "", # No slash command - user will drive the planning session
455
+ compute_relative_path_in_worktree(ctx.git.list_worktrees(repo_root), ctx.cwd),
456
+ model=model,
457
+ )
458
+ except RuntimeError as e:
459
+ raise click.ClickException(str(e)) from e
@@ -0,0 +1,40 @@
1
+ """Planner box management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.planner.configure_cmd import configure_planner
6
+ from erk.cli.commands.planner.connect_cmd import connect_planner
7
+ from erk.cli.commands.planner.create_cmd import create_planner
8
+ from erk.cli.commands.planner.list_cmd import list_planners
9
+ from erk.cli.commands.planner.register_cmd import register_planner
10
+ from erk.cli.commands.planner.set_default_cmd import set_default_planner
11
+ from erk.cli.commands.planner.unregister_cmd import unregister_planner
12
+ from erk.cli.help_formatter import ErkCommandGroup
13
+
14
+
15
+ @click.group(
16
+ "planner", cls=ErkCommandGroup, grouped=False, invoke_without_command=True, hidden=True
17
+ )
18
+ @click.pass_context
19
+ def planner_group(ctx: click.Context) -> None:
20
+ """Manage planner boxes (GitHub Codespaces for remote planning).
21
+
22
+ A planner box is a GitHub Codespace pre-configured for remote planning
23
+ with Claude Code. Use 'erk planner register' to register an existing
24
+ codespace, then 'erk planner' to connect.
25
+
26
+ When invoked without a subcommand, connects to the default planner.
27
+ """
28
+ # If no subcommand provided, invoke connect to default
29
+ if ctx.invoked_subcommand is None:
30
+ ctx.invoke(connect_planner, name=None)
31
+
32
+
33
+ # Register subcommands
34
+ planner_group.add_command(connect_planner)
35
+ planner_group.add_command(create_planner)
36
+ planner_group.add_command(configure_planner)
37
+ planner_group.add_command(register_planner)
38
+ planner_group.add_command(unregister_planner)
39
+ planner_group.add_command(list_planners)
40
+ planner_group.add_command(set_default_planner)
@@ -0,0 +1,73 @@
1
+ """Configure a planner box interactively."""
2
+
3
+ import subprocess
4
+
5
+ import click
6
+
7
+ from erk.core.context import ErkContext
8
+
9
+ SETUP_CHECKLIST = """
10
+ ┌─────────────────────────────────────────────────────────────────────┐
11
+ │ Planner Setup Checklist │
12
+ ├─────────────────────────────────────────────────────────────────────┤
13
+ │ │
14
+ │ 1. Claude Code Authentication │
15
+ │ └─ Run: claude │
16
+ │ Follow prompts to authenticate with Anthropic │
17
+ │ │
18
+ │ 2. Verify Setup │
19
+ │ └─ gh auth status (should show logged in) │
20
+ │ └─ claude doctor (should show all green) │
21
+ │ │
22
+ └─────────────────────────────────────────────────────────────────────┘
23
+ """
24
+
25
+
26
+ @click.command("configure")
27
+ @click.argument("name")
28
+ @click.pass_obj
29
+ def configure_planner(ctx: ErkContext, name: str) -> None:
30
+ """Configure a planner box with an interactive SSH session.
31
+
32
+ Opens an interactive SSH session to the codespace for manual setup
33
+ (installing tools, setting up auth, etc.). When you exit the session,
34
+ you'll be prompted to mark the planner as configured.
35
+ """
36
+ planner = ctx.planner_registry.get(name)
37
+ if planner is None:
38
+ click.echo(f"Error: No planner named '{name}' found.", err=True)
39
+ click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
40
+ raise SystemExit(1)
41
+
42
+ if planner.configured:
43
+ click.echo(f"Note: Planner '{name}' is already marked as configured.", err=True)
44
+ if not click.confirm("Continue with configuration session anyway?"):
45
+ raise SystemExit(0)
46
+
47
+ click.echo(SETUP_CHECKLIST, err=True)
48
+ click.echo(f"Opening interactive SSH session to '{name}'...", err=True)
49
+ click.echo(
50
+ "Complete the setup steps above, then exit the session (Ctrl+D or 'exit').", err=True
51
+ )
52
+ click.echo("", err=True)
53
+
54
+ # Run interactive SSH session (waits for completion)
55
+ result = subprocess.run(
56
+ ["gh", "codespace", "ssh", "-c", planner.gh_name],
57
+ check=False,
58
+ )
59
+
60
+ if result.returncode != 0:
61
+ click.echo(f"SSH session ended with error (exit code {result.returncode}).", err=True)
62
+ # Still allow marking as configured if user wants
63
+
64
+ # Ask if configuration is complete
65
+ click.echo("", err=True)
66
+ if click.confirm(f"Mark planner '{name}' as configured?"):
67
+ ctx.planner_registry.mark_configured(name)
68
+ click.echo(f"Planner '{name}' marked as configured.", err=True)
69
+ else:
70
+ click.echo(
71
+ f"Planner '{name}' left unconfigured. Run 'erk planner configure {name}' again later.",
72
+ err=True,
73
+ )
@@ -0,0 +1,96 @@
1
+ """Connect to a planner box."""
2
+
3
+ import os
4
+
5
+ import click
6
+
7
+ from erk.core.context import ErkContext
8
+
9
+
10
+ @click.command("connect")
11
+ @click.argument("name", required=False)
12
+ @click.option("--ssh", is_flag=True, help="Connect via SSH instead of VS Code")
13
+ @click.pass_obj
14
+ def connect_planner(ctx: ErkContext, name: str | None, ssh: bool) -> None:
15
+ """Connect to a planner box.
16
+
17
+ If NAME is provided, connects to that planner. Otherwise, connects
18
+ to the default planner.
19
+
20
+ By default, opens VS Code desktop to prevent idle timeout. Use --ssh
21
+ to connect via SSH and launch Claude directly.
22
+ """
23
+ # Get planner by name or default
24
+ if name is not None:
25
+ planner = ctx.planner_registry.get(name)
26
+ if planner is None:
27
+ click.echo(f"Error: No planner named '{name}' found.", err=True)
28
+ click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
29
+ raise SystemExit(1)
30
+ else:
31
+ planner = ctx.planner_registry.get_default()
32
+ if planner is None:
33
+ default_name = ctx.planner_registry.get_default_name()
34
+ if default_name is not None:
35
+ click.echo(f"Error: Default planner '{default_name}' not found.", err=True)
36
+ else:
37
+ click.echo("Error: No default planner set.", err=True)
38
+ click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
39
+ click.echo("Use 'erk planner set-default <name>' to set a default.", err=True)
40
+ raise SystemExit(1)
41
+
42
+ # Check if configured
43
+ if not planner.configured:
44
+ click.echo(f"Warning: Planner '{planner.name}' has not been configured yet.", err=True)
45
+ click.echo(f"Run 'erk planner configure {planner.name}' for initial setup.", err=True)
46
+
47
+ # Update last connected timestamp
48
+ ctx.planner_registry.update_last_connected(planner.name, ctx.time.now())
49
+
50
+ if ssh:
51
+ # Connect via gh codespace ssh with claude command
52
+ click.echo(f"Connecting to planner '{planner.name}' via SSH...", err=True)
53
+
54
+ # Replace current process with ssh session
55
+ # -t: Force pseudo-terminal allocation (required for interactive TUI like claude)
56
+ # bash -l -c: Use login shell to ensure PATH is set up (claude installs to ~/.claude/local/)
57
+ # Launch Claude in interactive mode for planning workflows
58
+ #
59
+ # IMPORTANT: The entire remote command (bash -l -c '...') must be a single argument.
60
+ # SSH concatenates command arguments with spaces without preserving grouping.
61
+ # If passed as separate args ["bash", "-l", "-c", "cmd"], the remote receives:
62
+ # bash -l -c git pull && uv sync && ...
63
+ # Instead of:
64
+ # bash -l -c "git pull && uv sync && ..."
65
+ # This causes `bash -l -c git` to run `git` with no subcommand (exits with help).
66
+ setup_commands = "git pull && uv sync && source .venv/bin/activate"
67
+ claude_command = "claude --allow-dangerously-skip-permissions --verbose"
68
+ remote_command = f"bash -l -c '{setup_commands} && {claude_command}'"
69
+
70
+ # GH-API-AUDIT: REST - codespace SSH connection
71
+ os.execvp(
72
+ "gh",
73
+ [
74
+ "gh",
75
+ "codespace",
76
+ "ssh",
77
+ "-c",
78
+ planner.gh_name,
79
+ "--",
80
+ "-t",
81
+ remote_command,
82
+ ],
83
+ )
84
+ else:
85
+ # Default: Open VS Code desktop (prevents idle timeout)
86
+ click.echo("Opening VS Code...", err=True)
87
+ click.echo("", err=True)
88
+ click.echo("Run in VS Code terminal:", err=True)
89
+ click.echo(" git pull && uv sync && source .venv/bin/activate", err=True)
90
+ click.echo(
91
+ " claude --allow-dangerously-skip-permissions --verbose",
92
+ err=True,
93
+ )
94
+
95
+ # GH-API-AUDIT: REST - codespace VS Code connection
96
+ os.execvp("gh", ["gh", "codespace", "code", "-c", planner.gh_name])