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,82 @@
1
+ """Fix merge conflicts with AI-powered resolution.
2
+
3
+ Uses Claude to resolve merge conflicts on the current branch without
4
+ any Graphite stack manipulation. Invokes the /erk:fix-conflicts
5
+ Claude slash command.
6
+ """
7
+
8
+ import click
9
+
10
+ from erk.cli.output import stream_fix_conflicts
11
+ from erk.core.context import ErkContext
12
+
13
+
14
+ @click.command("fix-conflicts")
15
+ @click.option(
16
+ "-f",
17
+ "--dangerous",
18
+ is_flag=True,
19
+ help="Acknowledge that this command invokes Claude with --dangerously-skip-permissions.",
20
+ )
21
+ @click.pass_obj
22
+ def pr_fix_conflicts(ctx: ErkContext, *, dangerous: bool) -> None:
23
+ """Fix merge conflicts with AI-powered resolution.
24
+
25
+ Resolves merge conflicts on the current branch using Claude.
26
+ Does not require or interact with Graphite stacks.
27
+
28
+ Examples:
29
+
30
+ \b
31
+ # Fix conflicts with Claude
32
+ erk pr fix-conflicts --dangerous
33
+
34
+ To disable the --dangerous flag requirement:
35
+
36
+ \b
37
+ erk config set fix_conflicts_require_dangerous_flag false
38
+ """
39
+ # Runtime validation: require --dangerous unless config disables requirement
40
+ if not dangerous:
41
+ require_flag = (
42
+ ctx.global_config is None or ctx.global_config.fix_conflicts_require_dangerous_flag
43
+ )
44
+ if require_flag:
45
+ raise click.UsageError(
46
+ "Missing option '--dangerous'.\n"
47
+ "To disable: erk config set fix_conflicts_require_dangerous_flag false"
48
+ )
49
+
50
+ cwd = ctx.cwd
51
+
52
+ # Check for conflicts
53
+ conflicted_files = ctx.git.get_conflicted_files(cwd)
54
+ if not conflicted_files:
55
+ click.echo("No merge conflicts detected.")
56
+ return
57
+
58
+ # Check Claude availability
59
+ executor = ctx.claude_executor
60
+ if not executor.is_claude_available():
61
+ raise click.ClickException(
62
+ "Claude CLI is required for conflict resolution.\n\n"
63
+ "Install from: https://claude.com/download"
64
+ )
65
+
66
+ # Show conflict info
67
+ click.echo(
68
+ click.style(
69
+ f"Found {len(conflicted_files)} conflicted file(s). Invoking Claude...",
70
+ fg="yellow",
71
+ )
72
+ )
73
+
74
+ # Execute conflict resolution
75
+ result = stream_fix_conflicts(executor, cwd)
76
+
77
+ if result.requires_interactive:
78
+ raise click.ClickException("Semantic conflict requires interactive resolution")
79
+ if not result.success:
80
+ raise click.ClickException(result.error_message or "Conflict resolution failed")
81
+
82
+ click.echo(click.style("\n✅ Conflicts resolved!", fg="green", bold=True))
@@ -0,0 +1,10 @@
1
+ """Parse PR reference from user input.
2
+
3
+ This module re-exports parse_pr_identifier from the centralized CLI parsing module.
4
+ Kept for backwards compatibility with existing imports.
5
+ """
6
+
7
+ from erk.cli.github_parsing import parse_pr_identifier
8
+
9
+ # Re-export with explicit assignment per PEP 484 to indicate intentional re-export
10
+ parse_pr_reference = parse_pr_identifier
@@ -0,0 +1,360 @@
1
+ """Submit current branch as a pull request.
2
+
3
+ Unified PR submission with two-layer architecture:
4
+ 1. Core layer: git push + gh pr create (works without Graphite)
5
+ 2. Graphite layer: Optional enhancement via gt submit
6
+
7
+ The workflow:
8
+ 1. Core submit: git push + gh pr create
9
+ 2. Get diff for AI: GitHub API
10
+ 3. Generate: AI-generated commit message via Claude CLI
11
+ 4. Graphite enhance: Optional gt submit for stack metadata
12
+ 5. Finalize: Update PR with AI-generated title/body
13
+ """
14
+
15
+ import os
16
+ import uuid
17
+ from pathlib import Path
18
+
19
+ import click
20
+
21
+ from erk.core.commit_message_generator import (
22
+ CommitMessageGenerator,
23
+ CommitMessageRequest,
24
+ CommitMessageResult,
25
+ )
26
+ from erk.core.context import ErkContext
27
+ from erk_shared.gateway.gt.events import CompletionEvent, ProgressEvent
28
+ from erk_shared.gateway.gt.operations.finalize import execute_finalize
29
+ from erk_shared.gateway.gt.types import FinalizeResult, PostAnalysisError
30
+ from erk_shared.gateway.pr.diff_extraction import execute_diff_extraction
31
+ from erk_shared.gateway.pr.graphite_enhance import execute_graphite_enhance
32
+ from erk_shared.gateway.pr.submit import execute_core_submit
33
+ from erk_shared.gateway.pr.types import (
34
+ CoreSubmitError,
35
+ CoreSubmitResult,
36
+ GraphiteEnhanceError,
37
+ GraphiteEnhanceResult,
38
+ GraphiteSkipped,
39
+ )
40
+
41
+
42
+ def _render_progress(event: ProgressEvent) -> None:
43
+ """Render a progress event to the CLI."""
44
+ message = f" {event.message}"
45
+ if event.style == "info":
46
+ click.echo(click.style(message, dim=True))
47
+ elif event.style == "success":
48
+ click.echo(click.style(message, fg="green"))
49
+ elif event.style == "warning":
50
+ click.echo(click.style(message, fg="yellow"))
51
+ elif event.style == "error":
52
+ click.echo(click.style(message, fg="red"))
53
+ else:
54
+ click.echo(message)
55
+
56
+
57
+ @click.command("submit")
58
+ @click.option("--debug", is_flag=True, help="Show diagnostic output")
59
+ @click.option(
60
+ "--no-graphite",
61
+ is_flag=True,
62
+ help="Skip Graphite enhancement (use git + gh only)",
63
+ )
64
+ @click.option(
65
+ "-f",
66
+ "--force",
67
+ is_flag=True,
68
+ help="Force push (use when branch has diverged from remote)",
69
+ )
70
+ @click.pass_obj
71
+ def pr_submit(ctx: ErkContext, debug: bool, no_graphite: bool, force: bool) -> None:
72
+ """Submit PR with AI-generated commit message.
73
+
74
+ Uses a two-layer architecture:
75
+ - Core layer (always): git push + gh pr create
76
+ - Graphite layer (optional): gt submit for stack metadata
77
+
78
+ The core layer works without Graphite installed. When Graphite is
79
+ available and the branch is tracked, it will enhance the PR with
80
+ stack metadata unless --no-graphite is specified.
81
+
82
+ Examples:
83
+
84
+ \b
85
+ # Submit PR (with Graphite if available)
86
+ erk pr submit
87
+
88
+ # Submit PR without Graphite enhancement
89
+ erk pr submit --no-graphite
90
+
91
+ # Force push when branch has diverged
92
+ erk pr submit -f
93
+ """
94
+ _execute_pr_submit(ctx, debug=debug, use_graphite=not no_graphite, force=force)
95
+
96
+
97
+ def _execute_pr_submit(ctx: ErkContext, debug: bool, use_graphite: bool, force: bool) -> None:
98
+ """Execute PR submission with positively-named parameters."""
99
+ # Verify Claude is available (needed for commit message generation)
100
+ if not ctx.claude_executor.is_claude_available():
101
+ raise click.ClickException(
102
+ "Claude CLI not found\n\nInstall from: https://claude.com/download"
103
+ )
104
+
105
+ click.echo(click.style("🚀 Submitting PR...", bold=True))
106
+ click.echo("")
107
+
108
+ cwd = Path.cwd()
109
+ session_id = os.environ.get("SESSION_ID", str(uuid.uuid4()))
110
+
111
+ # Phase 1: Core submit (git push + gh pr create)
112
+ click.echo(click.style("Phase 1: Creating PR", bold=True))
113
+ core_result = _run_core_submit(ctx, cwd, debug, force)
114
+
115
+ if isinstance(core_result, CoreSubmitError):
116
+ raise click.ClickException(core_result.message)
117
+
118
+ click.echo(click.style(f" PR #{core_result.pr_number} created", fg="green"))
119
+ click.echo("")
120
+
121
+ # Phase 2: Get diff for AI
122
+ click.echo(click.style("Phase 2: Getting diff", bold=True))
123
+ diff_file = _run_diff_extraction(ctx, cwd, core_result.pr_number, session_id, debug)
124
+
125
+ if diff_file is None:
126
+ raise click.ClickException("Failed to extract diff for AI analysis")
127
+
128
+ click.echo("")
129
+
130
+ # Get branch info for AI context
131
+ repo_root = ctx.git.get_repository_root(cwd)
132
+ current_branch = ctx.git.get_current_branch(cwd) or core_result.branch_name
133
+ trunk_branch = ctx.git.detect_trunk_branch(repo_root)
134
+
135
+ # Get parent branch (Graphite-aware, falls back to trunk)
136
+ parent_branch = (
137
+ ctx.graphite.get_parent_branch(ctx.git, Path(repo_root), current_branch) or trunk_branch
138
+ )
139
+
140
+ # Get commit messages for AI context (only from current branch)
141
+ commit_messages = ctx.git.get_commit_messages_since(cwd, parent_branch)
142
+
143
+ # Phase 3: Generate commit message
144
+ click.echo(click.style("Phase 3: Generating PR description", bold=True))
145
+ msg_gen = CommitMessageGenerator(ctx.claude_executor)
146
+ msg_result = _run_commit_message_generation(
147
+ msg_gen,
148
+ diff_file=diff_file,
149
+ repo_root=Path(repo_root),
150
+ current_branch=current_branch,
151
+ parent_branch=parent_branch,
152
+ commit_messages=commit_messages,
153
+ debug=debug,
154
+ )
155
+
156
+ if not msg_result.success:
157
+ raise click.ClickException(f"Failed to generate message: {msg_result.error_message}")
158
+
159
+ click.echo("")
160
+
161
+ # Phase 4: Graphite enhancement (optional)
162
+ graphite_url: str | None = None
163
+ if use_graphite:
164
+ click.echo(click.style("Phase 4: Graphite enhancement", bold=True))
165
+ graphite_result = _run_graphite_enhance(ctx, cwd, core_result.pr_number, debug, force)
166
+
167
+ if isinstance(graphite_result, GraphiteEnhanceResult):
168
+ graphite_url = graphite_result.graphite_url
169
+ click.echo("")
170
+ elif isinstance(graphite_result, GraphiteSkipped):
171
+ if debug:
172
+ click.echo(click.style(f" {graphite_result.message}", dim=True))
173
+ click.echo("")
174
+ elif isinstance(graphite_result, GraphiteEnhanceError):
175
+ # Graphite errors are warnings, not fatal
176
+ click.echo(click.style(f" Warning: {graphite_result.message}", fg="yellow"))
177
+ click.echo("")
178
+
179
+ # Phase 5: Finalize (update PR metadata)
180
+ click.echo(click.style("Phase 5: Updating PR metadata", bold=True))
181
+ finalize_result = _run_finalize(
182
+ ctx,
183
+ cwd,
184
+ pr_number=core_result.pr_number,
185
+ title=msg_result.title or "Update",
186
+ body=msg_result.body or "",
187
+ diff_file=str(diff_file),
188
+ debug=debug,
189
+ )
190
+
191
+ if isinstance(finalize_result, PostAnalysisError):
192
+ raise click.ClickException(finalize_result.message)
193
+
194
+ click.echo(click.style(" PR metadata updated", fg="green"))
195
+ click.echo("")
196
+
197
+ # Success output with clickable URL
198
+ styled_url = click.style(finalize_result.pr_url, fg="cyan", underline=True)
199
+ clickable_url = f"\033]8;;{finalize_result.pr_url}\033\\{styled_url}\033]8;;\033\\"
200
+ click.echo(f"✅ {clickable_url}")
201
+
202
+ # Show Graphite URL if available
203
+ if graphite_url:
204
+ styled_graphite = click.style(graphite_url, fg="cyan", underline=True)
205
+ clickable_graphite = f"\033]8;;{graphite_url}\033\\{styled_graphite}\033]8;;\033\\"
206
+ click.echo(f"📊 {clickable_graphite}")
207
+
208
+
209
+ def _run_core_submit(
210
+ ctx: ErkContext,
211
+ cwd: Path,
212
+ debug: bool,
213
+ force: bool,
214
+ ) -> CoreSubmitResult | CoreSubmitError:
215
+ """Run core submit phase (git push + gh pr create)."""
216
+ result: CoreSubmitResult | CoreSubmitError | None = None
217
+ plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
218
+
219
+ for event in execute_core_submit(
220
+ ctx, cwd, pr_title="WIP", pr_body="", force=force, plans_repo=plans_repo
221
+ ):
222
+ if isinstance(event, ProgressEvent):
223
+ if debug:
224
+ _render_progress(event)
225
+ elif isinstance(event, CompletionEvent):
226
+ result = event.result
227
+
228
+ if result is None:
229
+ return CoreSubmitError(
230
+ success=False,
231
+ error_type="submit-failed",
232
+ message="Core submit did not complete",
233
+ details={},
234
+ )
235
+
236
+ return result
237
+
238
+
239
+ def _run_diff_extraction(
240
+ ctx: ErkContext,
241
+ cwd: Path,
242
+ pr_number: int,
243
+ session_id: str,
244
+ debug: bool,
245
+ ) -> Path | None:
246
+ """Run diff extraction phase."""
247
+ result: Path | None = None
248
+
249
+ for event in execute_diff_extraction(ctx, cwd, pr_number, session_id):
250
+ if isinstance(event, ProgressEvent):
251
+ if debug:
252
+ _render_progress(event)
253
+ elif isinstance(event, CompletionEvent):
254
+ result = event.result
255
+
256
+ return result
257
+
258
+
259
+ def _run_graphite_enhance(
260
+ ctx: ErkContext,
261
+ cwd: Path,
262
+ pr_number: int,
263
+ debug: bool,
264
+ force: bool,
265
+ ) -> GraphiteEnhanceResult | GraphiteEnhanceError | GraphiteSkipped:
266
+ """Run Graphite enhancement phase."""
267
+ result: GraphiteEnhanceResult | GraphiteEnhanceError | GraphiteSkipped | None = None
268
+
269
+ for event in execute_graphite_enhance(ctx, cwd, pr_number, force=force):
270
+ if isinstance(event, ProgressEvent):
271
+ if debug:
272
+ _render_progress(event)
273
+ elif isinstance(event, CompletionEvent):
274
+ result = event.result
275
+
276
+ if result is None:
277
+ return GraphiteSkipped(
278
+ success=True,
279
+ reason="incomplete",
280
+ message="Graphite enhancement did not complete",
281
+ )
282
+
283
+ return result
284
+
285
+
286
+ def _run_finalize(
287
+ ctx: ErkContext,
288
+ cwd: Path,
289
+ pr_number: int,
290
+ title: str,
291
+ body: str,
292
+ diff_file: str,
293
+ debug: bool,
294
+ ) -> FinalizeResult | PostAnalysisError:
295
+ """Run finalize phase and return result."""
296
+ result: FinalizeResult | PostAnalysisError | None = None
297
+
298
+ plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
299
+ for event in execute_finalize(
300
+ ctx,
301
+ cwd,
302
+ pr_number=pr_number,
303
+ pr_title=title,
304
+ pr_body=body,
305
+ pr_body_file=None,
306
+ diff_file=diff_file,
307
+ plans_repo=plans_repo,
308
+ ):
309
+ if isinstance(event, ProgressEvent):
310
+ if debug:
311
+ _render_progress(event)
312
+ elif isinstance(event, CompletionEvent):
313
+ result = event.result
314
+
315
+ if result is None:
316
+ return PostAnalysisError(
317
+ success=False,
318
+ error_type="submit-failed",
319
+ message="Finalize did not complete",
320
+ details={},
321
+ )
322
+
323
+ return result
324
+
325
+
326
+ def _run_commit_message_generation(
327
+ generator: CommitMessageGenerator,
328
+ diff_file: Path,
329
+ repo_root: Path,
330
+ current_branch: str,
331
+ parent_branch: str,
332
+ commit_messages: list[str] | None,
333
+ debug: bool,
334
+ ) -> CommitMessageResult:
335
+ """Run commit message generation and return result."""
336
+ result: CommitMessageResult | None = None
337
+
338
+ for event in generator.generate(
339
+ CommitMessageRequest(
340
+ diff_file=diff_file,
341
+ repo_root=repo_root,
342
+ current_branch=current_branch,
343
+ parent_branch=parent_branch,
344
+ commit_messages=commit_messages,
345
+ )
346
+ ):
347
+ if isinstance(event, ProgressEvent):
348
+ _render_progress(event)
349
+ elif isinstance(event, CompletionEvent):
350
+ result = event.result
351
+
352
+ if result is None:
353
+ return CommitMessageResult(
354
+ success=False,
355
+ title=None,
356
+ body=None,
357
+ error_message="Commit message generation did not complete",
358
+ )
359
+
360
+ return result
@@ -0,0 +1,181 @@
1
+ """Synchronize current PR branch with Graphite.
2
+
3
+ This command registers a checked-out PR branch with Graphite so it can be managed
4
+ using gt commands (gt pr, gt land, etc.). Useful after checking out a PR from a
5
+ remote source (like GitHub Actions).
6
+
7
+ Flow:
8
+ 1. Validate preconditions (gh/gt auth, on branch, PR exists and is OPEN)
9
+ 2. Check if already tracked by Graphite (idempotent)
10
+ 3. Get PR base branch from GitHub
11
+ 4. Track with Graphite: gt track --branch <current> --parent <base>
12
+ 5. Squash commits: gt squash --no-edit --no-interactive
13
+ 6. Update local commit message with PR title/body from GitHub
14
+ 7. Restack: gt restack (manual conflict resolution if needed)
15
+ 8. Submit: gt submit --no-edit --no-interactive (force-push to link with Graphite)
16
+ """
17
+
18
+ from pathlib import Path
19
+
20
+ import click
21
+
22
+ from erk.cli.ensure import Ensure
23
+ from erk.cli.graphite_command import GraphiteCommand
24
+ from erk.core.context import ErkContext
25
+ from erk.core.repo_discovery import NoRepoSentinel, RepoContext
26
+ from erk_shared.gateway.gt.events import CompletionEvent
27
+ from erk_shared.gateway.gt.operations import execute_squash
28
+ from erk_shared.gateway.gt.types import RestackError, SquashError, SquashSuccess
29
+ from erk_shared.github.types import PRNotFound
30
+ from erk_shared.output.output import user_output
31
+
32
+
33
+ def _squash_commits(ctx: ErkContext, repo_root: Path) -> None:
34
+ """Squash all commits on the current branch into one."""
35
+ user_output("Squashing commits...")
36
+ squash_result = None
37
+ for event in execute_squash(ctx, repo_root):
38
+ if isinstance(event, CompletionEvent):
39
+ squash_result = event.result
40
+ squash_result = Ensure.not_none(squash_result, "Squash operation produced no result")
41
+ if isinstance(squash_result, SquashError):
42
+ Ensure.invariant(False, squash_result.message)
43
+ assert isinstance(squash_result, SquashSuccess) # Type narrowing after error check
44
+ user_output(click.style("✓", fg="green") + f" {squash_result.message}")
45
+
46
+
47
+ def _update_commit_message_from_pr(ctx: ErkContext, repo_root: Path, pr_number: int) -> None:
48
+ """Update the commit message with PR title and body from GitHub."""
49
+ pr = ctx.github.get_pr(repo_root, pr_number)
50
+ if isinstance(pr, PRNotFound):
51
+ # PR was verified to exist earlier, so this shouldn't happen
52
+ return
53
+ if pr.title:
54
+ commit_message = pr.title
55
+ if pr.body:
56
+ commit_message = f"{pr.title}\n\n{pr.body}"
57
+ user_output("Updating commit message from PR...")
58
+ ctx.git.amend_commit(repo_root, commit_message)
59
+ user_output(click.style("✓", fg="green") + " Commit message updated")
60
+
61
+
62
+ @click.command("sync", cls=GraphiteCommand)
63
+ @click.option(
64
+ "--dangerous",
65
+ is_flag=True,
66
+ required=True,
67
+ help="Acknowledge that this command invokes Claude with --dangerously-skip-permissions.",
68
+ )
69
+ @click.pass_obj
70
+ def pr_sync(ctx: ErkContext, *, dangerous: bool) -> None:
71
+ """Synchronize current PR branch with Graphite.
72
+
73
+ Registers the current PR branch with Graphite for stack management.
74
+ After syncing, you can use standard gt commands (gt pr, gt land, etc.).
75
+
76
+ This is typically used after 'erk pr checkout' to enable Graphite workflows
77
+ on a PR that was created elsewhere (like from a GitHub Actions run).
78
+
79
+ Examples:
80
+
81
+ # Checkout and sync a PR
82
+ erk pr checkout 1973
83
+ erk pr sync --dangerous
84
+
85
+ # Now you can use Graphite commands
86
+ gt pr
87
+ gt land
88
+
89
+ Requirements:
90
+ - On a branch (not detached HEAD)
91
+ - PR exists and is OPEN
92
+ - PR is not from a fork (cross-repo PRs cannot be tracked)
93
+ """
94
+ # dangerous flag is required to indicate acknowledgment
95
+ _ = dangerous
96
+ # Step 1: Validate preconditions
97
+ Ensure.gh_authenticated(ctx)
98
+ Ensure.gt_authenticated(ctx)
99
+ Ensure.invariant(
100
+ not isinstance(ctx.repo, NoRepoSentinel),
101
+ "Not in a git repository",
102
+ )
103
+ assert not isinstance(ctx.repo, NoRepoSentinel) # Type narrowing for ty
104
+ repo: RepoContext = ctx.repo
105
+
106
+ # Check we're on a branch (not detached HEAD)
107
+ current_branch = Ensure.not_none(
108
+ ctx.git.get_current_branch(ctx.cwd),
109
+ "Not on a branch - checkout a branch before syncing",
110
+ )
111
+
112
+ # Step 2: Check if PR exists and get status
113
+ pr = ctx.github.get_pr_for_branch(repo.root, current_branch)
114
+ Ensure.invariant(
115
+ not isinstance(pr, PRNotFound),
116
+ f"No pull request found for branch '{current_branch}'",
117
+ )
118
+ # Type narrowing after invariant check
119
+ assert not isinstance(pr, PRNotFound)
120
+ Ensure.invariant(
121
+ pr.state == "OPEN",
122
+ f"Cannot sync {pr.state} PR - only OPEN PRs can be synchronized",
123
+ )
124
+
125
+ pr_number = pr.number
126
+
127
+ # Check if PR is from a fork (cross-repo)
128
+ Ensure.invariant(
129
+ not pr.is_cross_repository,
130
+ "Cannot sync fork PRs - Graphite cannot track branches from forks",
131
+ )
132
+
133
+ # Step 3: Check if already tracked by Graphite (idempotent)
134
+ parent_branch = ctx.graphite.get_parent_branch(ctx.git, repo.root, current_branch)
135
+ if parent_branch is not None:
136
+ user_output(
137
+ click.style("✓", fg="green")
138
+ + f" Branch '{current_branch}' already tracked by Graphite (parent: {parent_branch})"
139
+ )
140
+ return
141
+
142
+ # Step 4: Get PR base branch from GitHub (use same PRDetails object)
143
+ base_branch = pr.base_ref_name
144
+ user_output(f"Base branch: {base_branch}")
145
+
146
+ # Step 5: Track with Graphite
147
+ user_output(f"Tracking branch '{current_branch}' with parent '{base_branch}'...")
148
+ ctx.graphite.track_branch(ctx.cwd, current_branch, base_branch)
149
+ user_output(click.style("✓", fg="green") + " Branch tracked with Graphite")
150
+
151
+ # Step 6: Squash commits (idempotent)
152
+ _squash_commits(ctx, repo.root)
153
+
154
+ # Step 6b: Update commit message with PR title/body
155
+ _update_commit_message_from_pr(ctx, repo.root, pr_number)
156
+
157
+ # Step 7: Restack with Graphite (manual conflict resolution if needed)
158
+ user_output("Restacking branch...")
159
+ restack_result = ctx.graphite.restack_idempotent(repo.root, no_interactive=True, quiet=False)
160
+ if isinstance(restack_result, RestackError):
161
+ if restack_result.error_type == "restack-conflict":
162
+ user_output(click.style("\nRestack paused due to merge conflicts.", fg="yellow"))
163
+ user_output("To resolve conflicts, run:")
164
+ user_output(click.style(" erk pr fix-conflicts --dangerous", fg="cyan"))
165
+ user_output("\nOr manually:")
166
+ user_output(" 1. Resolve conflicts in the listed files")
167
+ user_output(" 2. Run: gt add -A")
168
+ user_output(" 3. Run: gt continue")
169
+ raise SystemExit(1)
170
+ # Non-conflict error
171
+ raise click.ClickException(restack_result.message)
172
+ user_output(click.style("✓", fg="green") + " Branch restacked")
173
+
174
+ # Step 8: Submit to link with Graphite
175
+ # Force push is required because squashing rewrites history, causing divergence from remote
176
+ user_output("Submitting to link with Graphite...")
177
+ ctx.graphite.submit_stack(repo.root, quiet=True, force=True)
178
+ user_output(click.style("✓", fg="green") + f" PR #{pr_number} synchronized with Graphite")
179
+
180
+ user_output(f"\nBranch '{current_branch}' is now tracked by Graphite.")
181
+ user_output("You can now use: gt pr, gt land, etc.")
@@ -0,0 +1,60 @@
1
+ """Hidden command that pre-generates recovery scripts for passthrough flows."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.cli.core import discover_repo_context
8
+ from erk.cli.shell_utils import render_navigation_script
9
+ from erk.core.context import ErkContext
10
+ from erk_shared.output.output import user_output
11
+
12
+
13
+ def generate_recovery_script(ctx: ErkContext) -> Path | None:
14
+ """Create a recovery script that returns to the repo root if cwd vanishes.
15
+
16
+ This helper intentionally guards against runtime cwd races:
17
+ - ctx.cwd is a snapshot from CLI entry; it may no longer exist by the time this runs.
18
+ - discover_repo_context() performs the authoritative repo lookup; probing earlier provides
19
+ no additional safety and merely repeats the work.
20
+ - Returning None signals that graceful degradation is preferred to exploding at the boundary.
21
+ """
22
+ current_dir = ctx.cwd
23
+
24
+ if not current_dir.exists():
25
+ return None
26
+
27
+ try:
28
+ repo = discover_repo_context(ctx, current_dir)
29
+ except (FileNotFoundError, ValueError):
30
+ return None
31
+
32
+ script_content = render_navigation_script(
33
+ repo.root,
34
+ repo.root,
35
+ comment="erk passthrough recovery script",
36
+ success_message="",
37
+ )
38
+
39
+ result = ctx.script_writer.write_activation_script(
40
+ script_content,
41
+ command_name="prepare",
42
+ comment="pre-generated by __prepare_cwd_recovery",
43
+ )
44
+
45
+ return result.path
46
+
47
+
48
+ @click.command(
49
+ "__prepare_cwd_recovery",
50
+ hidden=True,
51
+ add_help_option=False,
52
+ )
53
+ @click.pass_obj
54
+ def prepare_cwd_recovery_cmd(ctx: ErkContext) -> None:
55
+ """Emit a recovery script if we are inside a managed repository."""
56
+ script_path = generate_recovery_script(ctx)
57
+ if script_path is None:
58
+ return
59
+
60
+ user_output(str(script_path), nl=False)