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,695 @@
1
+ """Command to implement features from GitHub issues or plan files.
2
+
3
+ This unified command provides two modes:
4
+ - GitHub issue mode: erk implement 123 or erk implement <URL>
5
+ - Plan file mode: erk implement path/to/plan.md
6
+
7
+ Both modes assign a pool slot and invoke Claude for implementation.
8
+ Can be run from any location, including from within pool slots.
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from erk.cli.alias import alias
17
+ from erk.cli.commands.completions import complete_plan_files
18
+ from erk.cli.commands.implement_shared import (
19
+ PlanSource,
20
+ WorktreeCreationResult,
21
+ build_claude_args,
22
+ build_command_sequence,
23
+ detect_target_type,
24
+ determine_base_branch,
25
+ execute_interactive_mode,
26
+ execute_non_interactive_mode,
27
+ implement_common_options,
28
+ normalize_model_name,
29
+ output_activation_instructions,
30
+ prepare_plan_source_from_file,
31
+ prepare_plan_source_from_issue,
32
+ validate_flags,
33
+ )
34
+ from erk.cli.commands.slot.common import (
35
+ find_branch_assignment,
36
+ find_inactive_slot,
37
+ find_next_available_slot,
38
+ generate_slot_name,
39
+ get_pool_size,
40
+ handle_pool_full_interactive,
41
+ )
42
+ from erk.cli.commands.wt.create_cmd import run_post_worktree_setup
43
+ from erk.cli.config import LoadedConfig
44
+ from erk.cli.core import discover_repo_context
45
+ from erk.cli.help_formatter import CommandWithHiddenOptions
46
+ from erk.core.claude_executor import ClaudeExecutor
47
+ from erk.core.context import ErkContext
48
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
49
+ from erk.core.worktree_pool import (
50
+ PoolState,
51
+ SlotAssignment,
52
+ SlotInfo,
53
+ load_pool_state,
54
+ save_pool_state,
55
+ update_slot_objective,
56
+ )
57
+ from erk_shared.github.metadata.plan_header import extract_plan_header_objective_issue
58
+ from erk_shared.impl_folder import create_impl_folder, save_issue_reference
59
+ from erk_shared.naming import sanitize_worktree_name
60
+ from erk_shared.output.output import user_output
61
+
62
+
63
+ def _check_worktree_clean_for_checkout(
64
+ ctx: ErkContext,
65
+ wt_path: Path,
66
+ slot_name: str,
67
+ ) -> None:
68
+ """Raise ClickException if worktree has uncommitted changes.
69
+
70
+ Checks for uncommitted changes before checkout to provide a friendly error
71
+ message with actionable remediation steps, rather than letting git fail
72
+ with an ugly traceback.
73
+ """
74
+ if ctx.git.has_uncommitted_changes(wt_path):
75
+ raise click.ClickException(
76
+ f"Slot '{slot_name}' has uncommitted changes that would be overwritten.\n\n"
77
+ f"Remediation options:\n"
78
+ f" 1. cd {wt_path} && git stash\n"
79
+ f" 2. cd {wt_path} && git commit -am 'WIP'\n"
80
+ f" 3. erk slot unassign {slot_name} # discard changes and reset slot"
81
+ )
82
+
83
+
84
+ def _create_worktree_with_plan_content(
85
+ ctx: ErkContext,
86
+ *,
87
+ plan_source: PlanSource,
88
+ dry_run: bool,
89
+ submit: bool,
90
+ dangerous: bool,
91
+ no_interactive: bool,
92
+ linked_branch_name: str | None,
93
+ base_branch: str,
94
+ model: str | None,
95
+ force: bool,
96
+ objective_issue: int | None,
97
+ ) -> WorktreeCreationResult | None:
98
+ """Create worktree with plan content using slot assignment.
99
+
100
+ Always assigns a new pool slot for the implementation, even when running
101
+ from within a managed slot. This maximizes parallelism by keeping the
102
+ parent branch assigned to its current slot.
103
+
104
+ Args:
105
+ ctx: Erk context
106
+ plan_source: Plan source with content and metadata
107
+ dry_run: Whether to perform dry run
108
+ submit: Whether to auto-submit PR after implementation
109
+ dangerous: Whether to skip permission prompts
110
+ no_interactive: Whether to execute non-interactively
111
+ linked_branch_name: Optional branch name for issue-based worktrees
112
+ (when provided, use this branch instead of creating new)
113
+ base_branch: Base branch to use as ref for worktree creation
114
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
115
+ force: Whether to auto-unassign oldest slot if pool is full
116
+ objective_issue: Optional objective issue number from plan metadata
117
+
118
+ Returns:
119
+ WorktreeCreationResult with paths, or None if dry-run mode
120
+ """
121
+ # Discover repository context
122
+ repo = discover_repo_context(ctx, ctx.cwd)
123
+ ensure_erk_metadata_dir(repo)
124
+ repo_root = repo.root
125
+
126
+ # Determine branch name
127
+ if linked_branch_name is not None:
128
+ # For issue mode: use the branch created for this issue
129
+ branch = linked_branch_name
130
+ else:
131
+ # For file mode: derive branch from plan name
132
+ branch = sanitize_worktree_name(plan_source.base_name)
133
+
134
+ # Get pool size from config
135
+ pool_size = get_pool_size(ctx)
136
+
137
+ # Load or create pool state
138
+ state = load_pool_state(repo.pool_json_path)
139
+ if state is None:
140
+ state = PoolState(
141
+ version="1.0",
142
+ pool_size=pool_size,
143
+ slots=(),
144
+ assignments=(),
145
+ )
146
+ elif state.pool_size != pool_size:
147
+ # Update pool_size from config if it changed
148
+ state = PoolState(
149
+ version=state.version,
150
+ pool_size=pool_size,
151
+ slots=state.slots,
152
+ assignments=state.assignments,
153
+ )
154
+
155
+ # Check if branch is already assigned to a slot
156
+ existing_assignment = find_branch_assignment(state, branch)
157
+ if existing_assignment is not None:
158
+ # Branch already has a slot - use it
159
+ slot_name = existing_assignment.slot_name
160
+ wt_path = existing_assignment.worktree_path
161
+ ctx.feedback.info(f"Branch '{branch}' already assigned to {slot_name}")
162
+
163
+ # Handle dry-run mode
164
+ if dry_run:
165
+ _show_dry_run_output(slot_name, plan_source, submit, dangerous, no_interactive, model)
166
+ return None
167
+
168
+ # Just update .impl/ folder with new plan content
169
+ ctx.feedback.info("Updating .impl/ folder with plan...")
170
+ create_impl_folder(
171
+ worktree_path=wt_path,
172
+ plan_content=plan_source.plan_content,
173
+ overwrite=True,
174
+ )
175
+ ctx.feedback.success("✓ Updated .impl/ folder")
176
+
177
+ return WorktreeCreationResult(
178
+ worktree_path=wt_path,
179
+ impl_dir=wt_path / ".impl",
180
+ )
181
+
182
+ # Check if branch already exists locally
183
+ local_branches = ctx.git.list_local_branches(repo_root)
184
+ use_existing_branch = branch in local_branches
185
+
186
+ # Find available slot
187
+ inactive_slot = find_inactive_slot(state, ctx.git, repo_root)
188
+ if inactive_slot is not None:
189
+ # Fast path: reuse existing worktree
190
+ slot_name, wt_path = inactive_slot
191
+ else:
192
+ # Find next available slot number
193
+ slot_num = find_next_available_slot(state, repo.worktrees_dir)
194
+ if slot_num is None:
195
+ # Pool is full - handle interactively or with --force
196
+ to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
197
+ if to_unassign is None:
198
+ raise SystemExit(1) from None
199
+
200
+ # Remove the assignment from state
201
+ new_assignments = tuple(
202
+ a for a in state.assignments if a.slot_name != to_unassign.slot_name
203
+ )
204
+ state = PoolState(
205
+ version=state.version,
206
+ pool_size=state.pool_size,
207
+ slots=state.slots,
208
+ assignments=new_assignments,
209
+ )
210
+ save_pool_state(repo.pool_json_path, state)
211
+ user_output(
212
+ click.style("✓ ", fg="green")
213
+ + f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
214
+ + f"from {click.style(to_unassign.slot_name, fg='cyan')}"
215
+ )
216
+
217
+ # Use the slot we just unassigned (it has a worktree directory that can be reused)
218
+ slot_name = to_unassign.slot_name
219
+ wt_path = to_unassign.worktree_path
220
+ else:
221
+ slot_name = generate_slot_name(slot_num)
222
+ wt_path = repo.worktrees_dir / slot_name
223
+
224
+ # Handle dry-run mode
225
+ if dry_run:
226
+ _show_dry_run_output(slot_name, plan_source, submit, dangerous, no_interactive, model)
227
+ return None
228
+
229
+ # Create worktree at slot path
230
+ ctx.feedback.info(f"Assigning to slot '{slot_name}'...")
231
+
232
+ # Load local config
233
+ config = ctx.local_config if ctx.local_config is not None else LoadedConfig.test()
234
+
235
+ # Respect global use_graphite config
236
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
237
+
238
+ if inactive_slot is not None:
239
+ # Fast path: checkout branch in existing worktree
240
+ # Check for uncommitted changes before checkout
241
+ _check_worktree_clean_for_checkout(ctx, wt_path, slot_name)
242
+ if use_existing_branch:
243
+ ctx.feedback.info(f"Checking out existing branch '{branch}'...")
244
+ ctx.git.checkout_branch(wt_path, branch)
245
+ else:
246
+ # Create branch and checkout
247
+ ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
248
+ ctx.git.create_branch(repo_root, branch, base_branch)
249
+ if use_graphite:
250
+ ctx.graphite.track_branch(repo_root, branch, base_branch)
251
+ ctx.git.checkout_branch(wt_path, branch)
252
+ else:
253
+ # On-demand slot creation
254
+ if not use_existing_branch:
255
+ # Create branch first
256
+ ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
257
+ ctx.git.create_branch(repo_root, branch, base_branch)
258
+ if use_graphite:
259
+ ctx.graphite.track_branch(repo_root, branch, base_branch)
260
+
261
+ # Check if worktree directory already exists (from pool initialization)
262
+ if wt_path.exists():
263
+ # Check for uncommitted changes before checkout
264
+ _check_worktree_clean_for_checkout(ctx, wt_path, slot_name)
265
+ # Worktree already exists - check out the branch
266
+ ctx.git.checkout_branch(wt_path, branch)
267
+ else:
268
+ # Create directory for worktree
269
+ wt_path.mkdir(parents=True, exist_ok=True)
270
+
271
+ # Add worktree
272
+ ctx.git.add_worktree(
273
+ repo_root,
274
+ wt_path,
275
+ branch=branch,
276
+ ref=None,
277
+ create_branch=False,
278
+ )
279
+
280
+ ctx.feedback.success(f"✓ Assigned {branch} to {slot_name}")
281
+
282
+ # Create slot assignment
283
+ now = ctx.time.now().isoformat()
284
+ new_assignment = SlotAssignment(
285
+ slot_name=slot_name,
286
+ branch_name=branch,
287
+ assigned_at=now,
288
+ worktree_path=wt_path,
289
+ )
290
+
291
+ # Update state with new assignment
292
+ new_state = PoolState(
293
+ version=state.version,
294
+ pool_size=state.pool_size,
295
+ slots=state.slots,
296
+ assignments=(*state.assignments, new_assignment),
297
+ )
298
+
299
+ # Save state
300
+ save_pool_state(repo.pool_json_path, new_state)
301
+
302
+ # Update slot with objective (if provided)
303
+ if objective_issue is not None:
304
+ # Check if slot exists in slots list
305
+ slot_exists = any(s.name == slot_name for s in new_state.slots)
306
+ if slot_exists:
307
+ # Update existing slot
308
+ new_state = update_slot_objective(new_state, slot_name, objective_issue)
309
+ else:
310
+ # Add new slot with objective
311
+ new_slot = SlotInfo(name=slot_name, last_objective_issue=objective_issue)
312
+ new_state = PoolState(
313
+ version=new_state.version,
314
+ pool_size=new_state.pool_size,
315
+ slots=(*new_state.slots, new_slot),
316
+ assignments=new_state.assignments,
317
+ )
318
+ save_pool_state(repo.pool_json_path, new_state)
319
+ ctx.feedback.info(f"Linked to objective #{objective_issue}")
320
+
321
+ # Run post-worktree setup
322
+ run_post_worktree_setup(ctx, config, wt_path, repo_root, slot_name)
323
+
324
+ # Create .impl/ folder with plan content at worktree root
325
+ ctx.feedback.info("Creating .impl/ folder with plan...")
326
+ create_impl_folder(
327
+ worktree_path=wt_path,
328
+ plan_content=plan_source.plan_content,
329
+ overwrite=True,
330
+ )
331
+ ctx.feedback.success("✓ Created .impl/ folder")
332
+
333
+ return WorktreeCreationResult(
334
+ worktree_path=wt_path,
335
+ impl_dir=wt_path / ".impl",
336
+ )
337
+
338
+
339
+ def _show_dry_run_output(
340
+ slot_name: str,
341
+ plan_source: PlanSource,
342
+ submit: bool,
343
+ dangerous: bool,
344
+ no_interactive: bool,
345
+ model: str | None,
346
+ ) -> None:
347
+ """Show dry-run output for slot assignment."""
348
+ dry_run_header = click.style("Dry-run mode:", fg="cyan", bold=True)
349
+ user_output(dry_run_header + " No changes will be made\n")
350
+
351
+ # Show execution mode
352
+ mode = "non-interactive" if no_interactive else "interactive"
353
+ user_output(f"Execution mode: {mode}\n")
354
+
355
+ user_output(f"Would assign to slot '{slot_name}'")
356
+ user_output(f" {plan_source.dry_run_description}")
357
+
358
+ # Show command sequence
359
+ commands = build_command_sequence(submit)
360
+ user_output("\nCommand sequence:")
361
+ for i, cmd in enumerate(commands, 1):
362
+ cmd_args = build_claude_args(cmd, dangerous, model)
363
+ user_output(f" {i}. {' '.join(cmd_args)}")
364
+
365
+
366
+ def _implement_from_issue(
367
+ ctx: ErkContext,
368
+ *,
369
+ issue_number: str,
370
+ dry_run: bool,
371
+ submit: bool,
372
+ dangerous: bool,
373
+ script: bool,
374
+ no_interactive: bool,
375
+ verbose: bool,
376
+ model: str | None,
377
+ force: bool,
378
+ executor: ClaudeExecutor,
379
+ ) -> None:
380
+ """Implement feature from GitHub issue.
381
+
382
+ Args:
383
+ ctx: Erk context
384
+ issue_number: GitHub issue number
385
+ dry_run: Whether to perform dry run
386
+ submit: Whether to auto-submit PR after implementation
387
+ dangerous: Whether to skip permission prompts
388
+ script: Whether to output activation script
389
+ no_interactive: Whether to execute non-interactively
390
+ verbose: Whether to show raw output or filtered output
391
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
392
+ force: Whether to auto-unassign oldest slot if pool is full
393
+ executor: Claude CLI executor for command execution
394
+ """
395
+ # Discover repo context for issue fetch
396
+ repo = discover_repo_context(ctx, ctx.cwd)
397
+ ensure_erk_metadata_dir(repo)
398
+
399
+ # Determine base branch (respects worktree stacking)
400
+ base_branch = determine_base_branch(ctx, repo.root)
401
+
402
+ # Prepare plan source from issue (creates branch via git)
403
+ issue_plan_source = prepare_plan_source_from_issue(
404
+ ctx, repo.root, issue_number, base_branch=base_branch
405
+ )
406
+
407
+ # Extract objective from plan metadata (if present)
408
+ plan = ctx.plan_store.get_plan(repo.root, issue_number)
409
+ objective_issue = extract_plan_header_objective_issue(plan.body)
410
+
411
+ # Create worktree with plan content, using the branch name
412
+ result = _create_worktree_with_plan_content(
413
+ ctx,
414
+ plan_source=issue_plan_source.plan_source,
415
+ dry_run=dry_run,
416
+ submit=submit,
417
+ dangerous=dangerous,
418
+ no_interactive=no_interactive,
419
+ linked_branch_name=issue_plan_source.branch_name,
420
+ base_branch=base_branch,
421
+ model=model,
422
+ force=force,
423
+ objective_issue=objective_issue,
424
+ )
425
+
426
+ # Early return for dry-run mode
427
+ if result is None:
428
+ return
429
+
430
+ wt_path = result.worktree_path
431
+
432
+ # Save issue reference for PR linking (issue-specific)
433
+ # Use impl_dir from result to handle monorepo project-root placement
434
+ ctx.feedback.info("Saving issue reference for PR linking...")
435
+ plan = ctx.plan_store.get_plan(repo.root, issue_number)
436
+ save_issue_reference(result.impl_dir, int(issue_number), plan.url, plan.title)
437
+
438
+ ctx.feedback.success(f"✓ Saved issue reference: {plan.url}")
439
+
440
+ # Execute based on mode
441
+ if script:
442
+ # Script mode - output activation script
443
+ branch = wt_path.name
444
+ target_description = f"#{issue_number}"
445
+ output_activation_instructions(
446
+ ctx,
447
+ wt_path=wt_path,
448
+ branch=branch,
449
+ script=script,
450
+ submit=submit,
451
+ dangerous=dangerous,
452
+ model=model,
453
+ target_description=target_description,
454
+ )
455
+ elif no_interactive:
456
+ # Non-interactive mode - execute via subprocess
457
+ commands = build_command_sequence(submit)
458
+ execute_non_interactive_mode(
459
+ worktree_path=wt_path,
460
+ commands=commands,
461
+ dangerous=dangerous,
462
+ verbose=verbose,
463
+ model=model,
464
+ executor=executor,
465
+ )
466
+ else:
467
+ # Interactive mode - hand off to Claude (never returns)
468
+ execute_interactive_mode(ctx, repo.root, wt_path, dangerous, model, executor)
469
+
470
+
471
+ def _implement_from_file(
472
+ ctx: ErkContext,
473
+ *,
474
+ plan_file: Path,
475
+ dry_run: bool,
476
+ submit: bool,
477
+ dangerous: bool,
478
+ script: bool,
479
+ no_interactive: bool,
480
+ verbose: bool,
481
+ model: str | None,
482
+ force: bool,
483
+ executor: ClaudeExecutor,
484
+ ) -> None:
485
+ """Implement feature from plan file.
486
+
487
+ Args:
488
+ ctx: Erk context
489
+ plan_file: Path to plan file
490
+ dry_run: Whether to perform dry run
491
+ submit: Whether to auto-submit PR after implementation
492
+ dangerous: Whether to skip permission prompts
493
+ script: Whether to output activation script
494
+ no_interactive: Whether to execute non-interactively
495
+ verbose: Whether to show raw output or filtered output
496
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
497
+ force: Whether to auto-unassign oldest slot if pool is full
498
+ executor: Claude CLI executor for command execution
499
+ """
500
+ # Discover repo context
501
+ repo = discover_repo_context(ctx, ctx.cwd)
502
+
503
+ # Determine base branch (respects worktree stacking)
504
+ base_branch = determine_base_branch(ctx, repo.root)
505
+
506
+ # Prepare plan source from file
507
+ plan_source = prepare_plan_source_from_file(ctx, plan_file)
508
+
509
+ # Create worktree with plan content
510
+ # File mode has no objective metadata
511
+ result = _create_worktree_with_plan_content(
512
+ ctx,
513
+ plan_source=plan_source,
514
+ dry_run=dry_run,
515
+ submit=submit,
516
+ dangerous=dangerous,
517
+ no_interactive=no_interactive,
518
+ linked_branch_name=None,
519
+ base_branch=base_branch,
520
+ model=model,
521
+ force=force,
522
+ objective_issue=None,
523
+ )
524
+
525
+ # Early return for dry-run mode
526
+ if result is None:
527
+ return
528
+
529
+ wt_path = result.worktree_path
530
+
531
+ # Delete original plan file (move semantics, file-specific)
532
+ ctx.feedback.info(f"Removing original plan file: {plan_file.name}...")
533
+ plan_file.unlink()
534
+
535
+ ctx.feedback.success("✓ Moved plan file to worktree")
536
+
537
+ # Execute based on mode
538
+ if script:
539
+ # Script mode - output activation script
540
+ branch = wt_path.name
541
+ target_description = str(plan_file)
542
+ output_activation_instructions(
543
+ ctx,
544
+ wt_path=wt_path,
545
+ branch=branch,
546
+ script=script,
547
+ submit=submit,
548
+ dangerous=dangerous,
549
+ model=model,
550
+ target_description=target_description,
551
+ )
552
+ elif no_interactive:
553
+ # Non-interactive mode - execute via subprocess
554
+ commands = build_command_sequence(submit)
555
+ execute_non_interactive_mode(
556
+ worktree_path=wt_path,
557
+ commands=commands,
558
+ dangerous=dangerous,
559
+ verbose=verbose,
560
+ model=model,
561
+ executor=executor,
562
+ )
563
+ else:
564
+ # Interactive mode - hand off to Claude (never returns)
565
+ execute_interactive_mode(ctx, repo.root, wt_path, dangerous, model, executor)
566
+
567
+
568
+ @alias("impl")
569
+ @click.command("implement", cls=CommandWithHiddenOptions)
570
+ @click.argument("target", shell_complete=complete_plan_files)
571
+ @implement_common_options
572
+ @click.option(
573
+ "-f",
574
+ "--force",
575
+ is_flag=True,
576
+ default=False,
577
+ help="Auto-unassign oldest slot if pool is full (no interactive prompt).",
578
+ )
579
+ @click.pass_obj
580
+ def implement(
581
+ ctx: ErkContext,
582
+ target: str,
583
+ dry_run: bool,
584
+ submit: bool,
585
+ dangerous: bool,
586
+ no_interactive: bool,
587
+ script: bool,
588
+ yolo: bool,
589
+ verbose: bool,
590
+ force: bool,
591
+ model: str | None,
592
+ ) -> None:
593
+ """Create worktree from GitHub issue or plan file and execute implementation.
594
+
595
+ By default, runs in interactive mode where you can interact with Claude
596
+ during implementation. Use --no-interactive for automated execution.
597
+
598
+ TARGET can be:
599
+ - GitHub issue number (e.g., #123 or 123)
600
+ - GitHub issue URL (e.g., https://github.com/user/repo/issues/123)
601
+ - Path to plan file (e.g., ./my-feature-plan.md)
602
+
603
+ Note: Plain numbers (e.g., 809) are always interpreted as GitHub issues.
604
+ For files with numeric names, use ./ prefix (e.g., ./809).
605
+
606
+ For GitHub issues, the issue must have the 'erk-plan' label.
607
+
608
+ Examples:
609
+
610
+ \b
611
+ # Interactive mode (default)
612
+ erk implement 123
613
+
614
+ \b
615
+ # Interactive mode, skip permissions
616
+ erk implement 123 --dangerous
617
+
618
+ \b
619
+ # Non-interactive mode (automated execution)
620
+ erk implement 123 --no-interactive
621
+
622
+ \b
623
+ # Full CI/PR workflow (requires --no-interactive)
624
+ erk implement 123 --no-interactive --submit
625
+
626
+ \b
627
+ # YOLO mode - full automation (dangerous + submit + no-interactive)
628
+ erk implement 123 --yolo
629
+
630
+ \b
631
+ # Shell integration
632
+ source <(erk implement 123 --script)
633
+
634
+ \b
635
+ # From plan file
636
+ erk implement ./my-feature-plan.md
637
+ """
638
+ # Handle --yolo flag (shorthand for dangerous + submit + no-interactive)
639
+ if yolo:
640
+ dangerous = True
641
+ submit = True
642
+ no_interactive = True
643
+
644
+ # Normalize model name (validates and expands aliases)
645
+ model = normalize_model_name(model)
646
+
647
+ # Validate flag combinations
648
+ validate_flags(submit, no_interactive, script)
649
+
650
+ # Detect target type
651
+ target_info = detect_target_type(target)
652
+
653
+ # Output target detection diagnostic
654
+ if target_info.target_type in ("issue_number", "issue_url"):
655
+ ctx.feedback.info(f"Detected GitHub issue #{target_info.issue_number}")
656
+ elif target_info.target_type == "file_path":
657
+ ctx.feedback.info(f"Detected plan file: {target}")
658
+
659
+ if target_info.target_type in ("issue_number", "issue_url"):
660
+ # GitHub issue mode
661
+ if target_info.issue_number is None:
662
+ user_output(
663
+ click.style("Error: ", fg="red") + "Failed to extract issue number from target"
664
+ )
665
+ raise SystemExit(1) from None
666
+
667
+ _implement_from_issue(
668
+ ctx,
669
+ issue_number=target_info.issue_number,
670
+ dry_run=dry_run,
671
+ submit=submit,
672
+ dangerous=dangerous,
673
+ script=script,
674
+ no_interactive=no_interactive,
675
+ verbose=verbose,
676
+ model=model,
677
+ force=force,
678
+ executor=ctx.claude_executor,
679
+ )
680
+ else:
681
+ # Plan file mode
682
+ plan_file = Path(target)
683
+ _implement_from_file(
684
+ ctx,
685
+ plan_file=plan_file,
686
+ dry_run=dry_run,
687
+ submit=submit,
688
+ dangerous=dangerous,
689
+ script=script,
690
+ no_interactive=no_interactive,
691
+ verbose=verbose,
692
+ model=model,
693
+ force=force,
694
+ executor=ctx.claude_executor,
695
+ )