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,649 @@
1
+ """Shared utilities for implement commands.
2
+
3
+ This module contains the common logic for erk implement - worktree-based implementation.
4
+ """
5
+
6
+ import re
7
+ import shlex
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import NamedTuple, TypeVar
12
+
13
+ import click
14
+
15
+ from erk.cli.activation import render_activation_script
16
+ from erk.cli.help_formatter import script_option
17
+ from erk.core.claude_executor import ClaudeExecutor
18
+ from erk.core.context import ErkContext
19
+ from erk.core.worktree_utils import compute_relative_path_in_worktree
20
+ from erk_shared.issue_workflow import (
21
+ IssueBranchSetup,
22
+ IssueValidationFailed,
23
+ prepare_plan_for_worktree,
24
+ )
25
+ from erk_shared.naming import (
26
+ sanitize_worktree_name,
27
+ strip_plan_from_filename,
28
+ )
29
+ from erk_shared.output.output import user_output
30
+
31
+ # Valid model names and their aliases
32
+ _MODEL_ALIASES: dict[str, str] = {
33
+ "h": "haiku",
34
+ "s": "sonnet",
35
+ "o": "opus",
36
+ }
37
+ _VALID_MODELS = {"haiku", "sonnet", "opus"}
38
+
39
+ F = TypeVar("F", bound=Callable[..., object])
40
+
41
+
42
+ def implement_common_options(fn: F) -> F:
43
+ """Decorator that applies common options shared between implement commands.
44
+
45
+ This decorator applies the following options (in order from top to bottom in help):
46
+ - --dry-run: Print what would be executed without doing it
47
+ - --submit: Automatically run CI validation and submit PR
48
+ - --dangerous: Skip permission prompts
49
+ - --no-interactive: Execute commands via subprocess
50
+ - --script: Output shell script for integration (hidden)
51
+ - --yolo: Equivalent to --dangerous --submit --no-interactive
52
+ - --verbose: Show full Claude Code output
53
+ - -m/--model: Model to use for Claude
54
+
55
+ Each command using this decorator must also define its own --force option
56
+ since the behavior differs (worktree deletion vs pool slot unassignment).
57
+
58
+ Example:
59
+ @click.command("implement", cls=CommandWithHiddenOptions)
60
+ @click.argument("target")
61
+ @implement_common_options
62
+ @click.option("-f", "--force", ...) # Command-specific force behavior
63
+ @click.pass_obj
64
+ def implement(ctx, target, dry_run, submit, dangerous, ...):
65
+ ...
66
+ """
67
+ # Apply options in reverse order (Click decorators are applied bottom-up)
68
+ # This results in options appearing in this order in --help
69
+ fn = click.option(
70
+ "-m",
71
+ "--model",
72
+ type=str,
73
+ default=None,
74
+ help="Model to use for Claude (haiku/h, sonnet/s, opus/o)",
75
+ )(fn)
76
+ fn = click.option(
77
+ "--verbose",
78
+ is_flag=True,
79
+ default=False,
80
+ help="Show full Claude Code output (default: filtered)",
81
+ )(fn)
82
+ fn = click.option(
83
+ "--yolo",
84
+ is_flag=True,
85
+ default=False,
86
+ help="Equivalent to --dangerous --submit --no-interactive (full automation)",
87
+ )(fn)
88
+ fn = script_option(fn)
89
+ fn = click.option(
90
+ "--no-interactive",
91
+ is_flag=True,
92
+ default=False,
93
+ help="Execute commands via subprocess without user interaction",
94
+ )(fn)
95
+ fn = click.option(
96
+ "--dangerous",
97
+ is_flag=True,
98
+ default=False,
99
+ help="Skip permission prompts by passing --dangerously-skip-permissions to Claude",
100
+ )(fn)
101
+ fn = click.option(
102
+ "--submit",
103
+ is_flag=True,
104
+ help="Automatically run CI validation and submit PR after implementation",
105
+ )(fn)
106
+ fn = click.option(
107
+ "--dry-run",
108
+ is_flag=True,
109
+ help="Print what would be executed without doing it",
110
+ )(fn)
111
+ return fn
112
+
113
+
114
+ def normalize_model_name(model: str | None) -> str | None:
115
+ """Normalize model name, expanding aliases and validating.
116
+
117
+ Args:
118
+ model: User-provided model name or alias (haiku, sonnet, opus, h, s, o, or None)
119
+
120
+ Returns:
121
+ Normalized full model name (haiku, sonnet, opus) or None if not provided
122
+
123
+ Raises:
124
+ click.ClickException: If model name is invalid
125
+ """
126
+ if model is None:
127
+ return None
128
+
129
+ # Expand alias if present
130
+ normalized = _MODEL_ALIASES.get(model.lower(), model.lower())
131
+
132
+ if normalized not in _VALID_MODELS:
133
+ valid_options = ", ".join(sorted(_VALID_MODELS | set(_MODEL_ALIASES.keys())))
134
+ raise click.ClickException(f"Invalid model: '{model}'\nValid options: {valid_options}")
135
+
136
+ return normalized
137
+
138
+
139
+ def determine_base_branch(ctx: ErkContext, repo_root: Path) -> str:
140
+ """Determine the base branch for new worktree creation.
141
+
142
+ When Graphite is enabled and the user is on a non-trunk branch,
143
+ stack on the current branch. Otherwise, use trunk.
144
+
145
+ Args:
146
+ ctx: Erk context
147
+ repo_root: Repository root path
148
+
149
+ Returns:
150
+ Base branch name to use as ref for worktree creation
151
+ """
152
+ trunk_branch = ctx.git.detect_trunk_branch(repo_root)
153
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
154
+
155
+ if not use_graphite:
156
+ return trunk_branch
157
+
158
+ current_branch = ctx.git.get_current_branch(ctx.cwd)
159
+ if current_branch and current_branch != trunk_branch:
160
+ return current_branch
161
+
162
+ return trunk_branch
163
+
164
+
165
+ def validate_flags(submit: bool, no_interactive: bool, script: bool) -> None:
166
+ """Validate flag combinations and raise ClickException if invalid.
167
+
168
+ Args:
169
+ submit: Whether to auto-submit PR after implementation
170
+ no_interactive: Whether to execute non-interactively
171
+ script: Whether to output shell integration script
172
+
173
+ Raises:
174
+ click.ClickException: If flag combination is invalid
175
+ """
176
+ # --submit requires --no-interactive UNLESS using --script mode
177
+ # Script mode generates shell code, so --submit is allowed
178
+ if submit and not no_interactive and not script:
179
+ raise click.ClickException(
180
+ "--submit requires --no-interactive\n"
181
+ "Automated workflows must run non-interactively\n"
182
+ "(or use --script to generate shell integration code)"
183
+ )
184
+
185
+ if no_interactive and script:
186
+ raise click.ClickException(
187
+ "--no-interactive and --script are mutually exclusive\n"
188
+ "--script generates shell integration code for manual execution\n"
189
+ "--no-interactive executes commands programmatically"
190
+ )
191
+
192
+
193
+ def build_command_sequence(submit: bool) -> list[str]:
194
+ """Build list of slash commands to execute.
195
+
196
+ Args:
197
+ submit: Whether to include full CI/PR workflow
198
+
199
+ Returns:
200
+ List of slash commands to execute in sequence
201
+ """
202
+ commands = ["/erk:plan-implement"]
203
+ if submit:
204
+ commands.extend(["/fast-ci", "/gt:pr-submit"])
205
+ return commands
206
+
207
+
208
+ def build_claude_args(slash_command: str, dangerous: bool, model: str | None) -> list[str]:
209
+ """Build Claude command argument list for interactive script mode.
210
+
211
+ Args:
212
+ slash_command: The slash command to execute
213
+ dangerous: Whether to skip permission prompts
214
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
215
+
216
+ Returns:
217
+ List of command arguments suitable for subprocess
218
+ """
219
+ args = ["claude", "--permission-mode", "acceptEdits"]
220
+ if dangerous:
221
+ args.append("--dangerously-skip-permissions")
222
+ if model is not None:
223
+ args.extend(["--model", model])
224
+ args.append(slash_command)
225
+ return args
226
+
227
+
228
+ def build_claude_command(slash_command: str, dangerous: bool, model: str | None) -> str:
229
+ """Build a Claude CLI invocation for interactive mode.
230
+
231
+ Args:
232
+ slash_command: The slash command to execute (e.g., "/erk:plan-implement")
233
+ dangerous: Whether to skip permission prompts
234
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
235
+
236
+ Returns:
237
+ Complete Claude CLI command string
238
+ """
239
+ cmd = "claude --permission-mode acceptEdits"
240
+ if dangerous:
241
+ cmd += " --dangerously-skip-permissions"
242
+ if model is not None:
243
+ cmd += f" --model {model}"
244
+ cmd += f' "{slash_command}"'
245
+ return cmd
246
+
247
+
248
+ def execute_interactive_mode(
249
+ ctx: ErkContext,
250
+ repo_root: Path,
251
+ worktree_path: Path,
252
+ dangerous: bool,
253
+ model: str | None,
254
+ executor: ClaudeExecutor,
255
+ ) -> None:
256
+ """Execute implementation in interactive mode using executor.
257
+
258
+ Args:
259
+ ctx: Erk context for accessing git and current working directory
260
+ repo_root: Path to repository root for listing worktrees
261
+ worktree_path: Path to worktree directory
262
+ dangerous: Whether to skip permission prompts
263
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
264
+ executor: Claude CLI executor for process replacement
265
+
266
+ Raises:
267
+ click.ClickException: If Claude CLI not found
268
+
269
+ Note:
270
+ This function never returns in production - the process is replaced by Claude
271
+ """
272
+ click.echo("Entering interactive implementation mode...", err=True)
273
+ try:
274
+ executor.execute_interactive(
275
+ worktree_path,
276
+ dangerous,
277
+ "/erk:plan-implement",
278
+ compute_relative_path_in_worktree(ctx.git.list_worktrees(repo_root), ctx.cwd),
279
+ model=model,
280
+ )
281
+ except RuntimeError as e:
282
+ raise click.ClickException(str(e)) from e
283
+
284
+
285
+ def execute_non_interactive_mode(
286
+ *,
287
+ worktree_path: Path,
288
+ commands: list[str],
289
+ dangerous: bool,
290
+ verbose: bool,
291
+ model: str | None,
292
+ executor: ClaudeExecutor,
293
+ ) -> None:
294
+ """Execute commands via Claude CLI executor with rich output formatting.
295
+
296
+ Args:
297
+ worktree_path: Path to worktree directory
298
+ commands: List of slash commands to execute
299
+ dangerous: Whether to skip permission prompts
300
+ verbose: Whether to show raw output (True) or filtered output (False)
301
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
302
+ executor: Claude CLI executor for command execution
303
+
304
+ Raises:
305
+ click.ClickException: If Claude CLI not found or command fails
306
+ """
307
+ import time
308
+
309
+ from rich.console import Console
310
+
311
+ from erk.cli.output import format_implement_summary, stream_command_with_feedback
312
+ from erk.core.claude_executor import CommandResult
313
+
314
+ # Verify Claude is available
315
+ if not executor.is_claude_available():
316
+ raise click.ClickException(
317
+ "Claude CLI not found\nInstall from: https://claude.com/download"
318
+ )
319
+
320
+ console = Console()
321
+ total_start = time.time()
322
+ all_results: list[CommandResult] = []
323
+
324
+ for cmd in commands:
325
+ if verbose:
326
+ # Verbose mode - simple output, no spinner
327
+ click.echo(f"Running {cmd}...", err=True)
328
+ result = executor.execute_command(
329
+ cmd, worktree_path, dangerous, verbose=True, model=model
330
+ )
331
+ else:
332
+ # Filtered mode - streaming with live print-based feedback
333
+ result = stream_command_with_feedback(
334
+ executor=executor,
335
+ command=cmd,
336
+ worktree_path=worktree_path,
337
+ dangerous=dangerous,
338
+ model=model,
339
+ )
340
+
341
+ all_results.append(result)
342
+
343
+ # Stop on first failure
344
+ if not result.success:
345
+ break
346
+
347
+ # Show final summary (unless verbose mode)
348
+ if not verbose:
349
+ total_duration = time.time() - total_start
350
+ summary = format_implement_summary(all_results, total_duration)
351
+ console.print(summary)
352
+
353
+ # Raise exception if any command failed
354
+ if not all(r.success for r in all_results):
355
+ raise click.ClickException("One or more commands failed")
356
+
357
+
358
+ def build_activation_script_with_commands(
359
+ worktree_path: Path, commands: list[str], dangerous: bool, model: str | None
360
+ ) -> str:
361
+ """Build activation script with Claude commands.
362
+
363
+ Args:
364
+ worktree_path: Path to worktree
365
+ commands: List of slash commands to include
366
+ dangerous: Whether to skip permission prompts
367
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
368
+
369
+ Returns:
370
+ Complete activation script with commands
371
+ """
372
+ # Get base activation script (cd + venv + env)
373
+ script = render_activation_script(
374
+ worktree_path=worktree_path,
375
+ target_subpath=None,
376
+ post_cd_commands=None,
377
+ final_message="", # We'll add commands instead
378
+ comment="implement activation",
379
+ )
380
+
381
+ # Add Claude commands
382
+ shell_commands = []
383
+ for cmd in commands:
384
+ cmd_args = build_claude_args(cmd, dangerous, model)
385
+ # Build shell command string
386
+ shell_cmd = " ".join(shlex.quote(arg) for arg in cmd_args)
387
+ shell_commands.append(shell_cmd)
388
+
389
+ # Chain commands with && so they only run if previous command succeeded
390
+ script += " && \\\n".join(shell_commands) + "\n"
391
+
392
+ return script
393
+
394
+
395
+ class TargetInfo(NamedTuple):
396
+ """Information about detected target type.
397
+
398
+ Attributes:
399
+ target_type: Type of target - "issue_number", "issue_url", or "file_path"
400
+ issue_number: Extracted issue number for GitHub targets, None for file paths
401
+ """
402
+
403
+ target_type: str
404
+ issue_number: str | None
405
+
406
+
407
+ def detect_target_type(target: str) -> TargetInfo:
408
+ """Detect whether target is an issue number, issue URL, or file path.
409
+
410
+ Args:
411
+ target: User-provided target argument
412
+
413
+ Returns:
414
+ TargetInfo with target type and extracted issue number (if applicable)
415
+ """
416
+ # Check if starts with # followed by digits (issue number)
417
+ if target.startswith("#") and target[1:].isdigit():
418
+ return TargetInfo(target_type="issue_number", issue_number=target[1:])
419
+
420
+ # Check if GitHub issue URL
421
+ github_issue_pattern = r"github\.com/[^/]+/[^/]+/issues/(\d+)"
422
+ match = re.search(github_issue_pattern, target)
423
+ if match:
424
+ issue_number = match.group(1)
425
+ return TargetInfo(target_type="issue_url", issue_number=issue_number)
426
+
427
+ # Check if plain digits (issue number without # prefix)
428
+ if target.isdigit():
429
+ return TargetInfo(target_type="issue_number", issue_number=target)
430
+
431
+ # Otherwise, treat as file path
432
+ return TargetInfo(target_type="file_path", issue_number=None)
433
+
434
+
435
+ @dataclass(frozen=True)
436
+ class PlanSource:
437
+ """Source information for creating a worktree with plan.
438
+
439
+ Attributes:
440
+ plan_content: The plan content as a string
441
+ base_name: Base name for generating worktree name
442
+ dry_run_description: Description to show in dry-run mode
443
+ """
444
+
445
+ plan_content: str
446
+ base_name: str
447
+ dry_run_description: str
448
+
449
+
450
+ @dataclass(frozen=True)
451
+ class IssuePlanSource:
452
+ """Extended plan source with issue-specific metadata.
453
+
454
+ Attributes:
455
+ plan_source: The base PlanSource with content and metadata
456
+ branch_name: The development branch name for this issue
457
+ already_existed: Whether the branch already existed
458
+ """
459
+
460
+ plan_source: PlanSource
461
+ branch_name: str
462
+ already_existed: bool
463
+
464
+
465
+ def prepare_plan_source_from_issue(
466
+ ctx: ErkContext, repo_root: Path, issue_number: str, base_branch: str
467
+ ) -> IssuePlanSource:
468
+ """Prepare plan source from GitHub issue.
469
+
470
+ Creates a branch for the issue and fetches plan content.
471
+
472
+ Args:
473
+ ctx: Erk context
474
+ repo_root: Repository root path
475
+ issue_number: GitHub issue number
476
+ base_branch: Base branch for creating the development branch
477
+
478
+ Returns:
479
+ IssuePlanSource with plan content, metadata, and branch name
480
+
481
+ Raises:
482
+ SystemExit: If issue not found or doesn't have erk-plan label
483
+ """
484
+ # Output fetching diagnostic
485
+ ctx.feedback.info("Fetching issue from GitHub...")
486
+
487
+ # Fetch plan from GitHub
488
+ try:
489
+ plan = ctx.plan_store.get_plan(repo_root, issue_number)
490
+ except RuntimeError as e:
491
+ ctx.feedback.error(f"Error: {e}")
492
+ raise SystemExit(1) from e
493
+
494
+ # Output issue title
495
+ ctx.feedback.info(f"Issue: {plan.title}")
496
+
497
+ # Prepare and validate using shared helper (returns union type)
498
+ result = prepare_plan_for_worktree(plan, ctx.time.now())
499
+
500
+ if isinstance(result, IssueValidationFailed):
501
+ user_output(click.style("Error: ", fg="red") + result.message)
502
+ raise SystemExit(1) from None
503
+
504
+ setup: IssueBranchSetup = result
505
+ for warning in setup.warnings:
506
+ user_output(click.style("Warning: ", fg="yellow") + warning)
507
+
508
+ dry_run_desc = f"Would create worktree from issue #{issue_number}\n Title: {plan.title}"
509
+
510
+ plan_source = PlanSource(
511
+ plan_content=setup.plan_content,
512
+ base_name=setup.worktree_name,
513
+ dry_run_description=dry_run_desc,
514
+ )
515
+
516
+ # Check if the branch already exists locally
517
+ local_branches = ctx.git.list_local_branches(repo_root)
518
+ branch_already_exists = setup.branch_name in local_branches
519
+
520
+ return IssuePlanSource(
521
+ plan_source=plan_source,
522
+ branch_name=setup.branch_name,
523
+ already_existed=branch_already_exists,
524
+ )
525
+
526
+
527
+ def prepare_plan_source_from_file(ctx: ErkContext, plan_file: Path) -> PlanSource:
528
+ """Prepare plan source from file.
529
+
530
+ Args:
531
+ ctx: Erk context
532
+ plan_file: Path to plan file
533
+
534
+ Returns:
535
+ PlanSource with plan content and metadata
536
+
537
+ Raises:
538
+ SystemExit: If plan file doesn't exist
539
+ """
540
+ # Validate plan file exists
541
+ if not plan_file.exists():
542
+ ctx.feedback.error(f"Error: Plan file not found: {plan_file}")
543
+ raise SystemExit(1) from None
544
+
545
+ # Output reading diagnostic
546
+ ctx.feedback.info("Reading plan file...")
547
+
548
+ # Read plan content
549
+ plan_content = plan_file.read_text(encoding="utf-8")
550
+
551
+ # Extract title from plan content for display
552
+ title = plan_file.stem
553
+ for line in plan_content.split("\n"):
554
+ stripped = line.strip()
555
+ if stripped.startswith("#"):
556
+ # Extract title from first heading
557
+ title = stripped.lstrip("#").strip()
558
+ break
559
+
560
+ # Output plan title
561
+ ctx.feedback.info(f"Plan: {title}")
562
+
563
+ # Derive base name from filename
564
+ plan_stem = plan_file.stem
565
+ cleaned_stem = strip_plan_from_filename(plan_stem)
566
+ base_name = sanitize_worktree_name(cleaned_stem)
567
+
568
+ dry_run_desc = (
569
+ f"Would create worktree from plan file: {plan_file}\n"
570
+ f" Plan file would be deleted: {plan_file}"
571
+ )
572
+
573
+ return PlanSource(
574
+ plan_content=plan_content,
575
+ base_name=base_name,
576
+ dry_run_description=dry_run_desc,
577
+ )
578
+
579
+
580
+ def output_activation_instructions(
581
+ ctx: ErkContext,
582
+ *,
583
+ wt_path: Path,
584
+ branch: str,
585
+ script: bool,
586
+ submit: bool,
587
+ dangerous: bool,
588
+ model: str | None,
589
+ target_description: str,
590
+ ) -> None:
591
+ """Output activation script or manual instructions.
592
+
593
+ This is only called when in script mode (for manual shell integration).
594
+ Interactive and non-interactive modes handle execution directly.
595
+
596
+ Args:
597
+ ctx: Erk context
598
+ wt_path: Worktree path
599
+ branch: Branch name
600
+ script: Whether to output activation script
601
+ submit: Whether to auto-submit PR after implementation
602
+ dangerous: Whether to skip permission prompts
603
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
604
+ target_description: Description of target for user messages
605
+ """
606
+ if script:
607
+ # Build command sequence
608
+ commands = build_command_sequence(submit)
609
+
610
+ # Generate activation script with commands
611
+ full_script = build_activation_script_with_commands(wt_path, commands, dangerous, model)
612
+
613
+ comment_suffix = "implement, CI, and submit" if submit else "implement"
614
+ result = ctx.script_writer.write_activation_script(
615
+ full_script,
616
+ command_name="implement",
617
+ comment=f"activate {wt_path.name} and {comment_suffix}",
618
+ )
619
+
620
+ result.output_for_shell_integration()
621
+ else:
622
+ # Provide manual instructions
623
+ user_output("\n" + click.style("Next steps:", fg="cyan", bold=True))
624
+ user_output(f" 1. Change to worktree: erk br co {branch}")
625
+ if submit:
626
+ user_output(" 2. Run implementation, CI, and submit PR:")
627
+ user_output(f" {build_claude_command('/erk:plan-implement', dangerous, model)}")
628
+ user_output(f" {build_claude_command('/fast-ci', dangerous, model)}")
629
+ user_output(f" {build_claude_command('/gt:pr-submit', dangerous, model)}")
630
+ else:
631
+ claude_cmd = build_claude_command("/erk:plan-implement", dangerous, model)
632
+ user_output(f" 2. Run implementation: {claude_cmd}")
633
+ user_output("\n" + click.style("Shell integration not detected.", fg="yellow"))
634
+ user_output("To activate environment and run commands, use:")
635
+ script_flag = "--submit --script" if submit else "--script"
636
+ user_output(f" source <(erk implement {target_description} {script_flag})")
637
+
638
+
639
+ @dataclass(frozen=True)
640
+ class WorktreeCreationResult:
641
+ """Result of creating a worktree with plan content.
642
+
643
+ Attributes:
644
+ worktree_path: Path to the created worktree root
645
+ impl_dir: Path to the .impl/ directory (always at worktree root)
646
+ """
647
+
648
+ worktree_path: Path
649
+ impl_dir: Path
@@ -0,0 +1,14 @@
1
+ """Info command group for viewing information about erk."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.info.release_notes_cmd import release_notes_cmd
6
+
7
+
8
+ @click.group("info")
9
+ def info_group() -> None:
10
+ """View information about erk."""
11
+ pass
12
+
13
+
14
+ info_group.add_command(release_notes_cmd)