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,294 @@
1
+ #!/usr/bin/env python3
2
+ """Update PR body with AI-generated summary and footer.
3
+
4
+ This command generates a PR summary from the diff using Claude, then updates
5
+ the PR body with the summary, optional workflow link, and standardized footer.
6
+
7
+ This combines generate-pr-summary + footer construction + gh pr edit in one step,
8
+ replacing ~30 lines of bash in GitHub Actions workflows.
9
+
10
+ Usage:
11
+ erk exec ci-update-pr-body \\
12
+ --issue-number 123 \\
13
+ [--run-id 456789] \\
14
+ [--run-url https://github.com/owner/repo/actions/runs/456789]
15
+
16
+ Output:
17
+ JSON object with success status
18
+
19
+ Exit Codes:
20
+ 0: Success (PR body updated)
21
+ 1: Error (no PR for branch, empty diff, Claude failure, or GitHub API failed)
22
+
23
+ Examples:
24
+ $ erk exec ci-update-pr-body --issue-number 123
25
+ {
26
+ "success": true,
27
+ "pr_number": 789
28
+ }
29
+
30
+ $ erk exec ci-update-pr-body \\
31
+ --issue-number 123 \\
32
+ --run-id 456789 \\
33
+ --run-url https://github.com/owner/repo/actions/runs/456789
34
+ {
35
+ "success": true,
36
+ "pr_number": 789
37
+ }
38
+ """
39
+
40
+ import json
41
+ from dataclasses import asdict, dataclass
42
+ from pathlib import Path
43
+ from typing import Literal
44
+
45
+ import click
46
+
47
+ from erk.cli.config import load_config
48
+ from erk_shared.context.helpers import (
49
+ require_git,
50
+ require_github,
51
+ require_prompt_executor,
52
+ require_repo_root,
53
+ )
54
+ from erk_shared.gateway.gt.prompts import COMMIT_MESSAGE_SYSTEM_PROMPT, truncate_diff
55
+ from erk_shared.git.abc import Git
56
+ from erk_shared.github.abc import GitHub
57
+ from erk_shared.github.pr_footer import build_pr_body_footer, build_remote_execution_note
58
+ from erk_shared.github.types import PRNotFound
59
+ from erk_shared.prompt_executor import PromptExecutor
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class UpdateSuccess:
64
+ """Success result when PR body is updated."""
65
+
66
+ success: bool
67
+ pr_number: int
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class UpdateError:
72
+ """Error result when PR body update fails."""
73
+
74
+ success: bool
75
+ error: Literal[
76
+ "pr-not-found",
77
+ "empty-diff",
78
+ "diff-fetch-failed",
79
+ "claude-execution-failed",
80
+ "claude-empty-output",
81
+ "github-api-failed",
82
+ ]
83
+ message: str
84
+ stderr: str | None
85
+
86
+
87
+ def _build_prompt(diff_content: str, current_branch: str, parent_branch: str) -> str:
88
+ """Build prompt for PR summary generation.
89
+
90
+ Note: We deliberately do NOT include commit messages here. The commit messages
91
+ may contain info about .worker-impl/ deletions that don't appear in the final PR diff.
92
+ """
93
+ context_section = f"""## Context
94
+
95
+ - Current branch: {current_branch}
96
+ - Parent branch: {parent_branch}"""
97
+
98
+ return f"""{COMMIT_MESSAGE_SYSTEM_PROMPT}
99
+
100
+ {context_section}
101
+
102
+ ## Diff
103
+
104
+ ```diff
105
+ {diff_content}
106
+ ```
107
+
108
+ Generate a commit message for this diff:"""
109
+
110
+
111
+ def _build_pr_body(
112
+ summary: str,
113
+ pr_number: int,
114
+ issue_number: int,
115
+ run_id: str | None,
116
+ run_url: str | None,
117
+ plans_repo: str | None,
118
+ ) -> str:
119
+ """Build the full PR body with summary, optional workflow link, and footer.
120
+
121
+ Args:
122
+ summary: AI-generated PR summary
123
+ pr_number: PR number for checkout instructions
124
+ issue_number: Issue number to close on merge
125
+ run_id: Optional workflow run ID
126
+ run_url: Optional workflow run URL
127
+ plans_repo: Target repo in "owner/repo" format for cross-repo plans
128
+
129
+ Returns:
130
+ Formatted PR body markdown
131
+ """
132
+ parts = [f"## Summary\n\n{summary}"]
133
+
134
+ # Add workflow link if provided
135
+ if run_id is not None and run_url is not None:
136
+ parts.append(build_remote_execution_note(run_id, run_url))
137
+
138
+ # Add footer with checkout instructions
139
+ parts.append(
140
+ build_pr_body_footer(pr_number=pr_number, issue_number=issue_number, plans_repo=plans_repo)
141
+ )
142
+
143
+ return "\n".join(parts)
144
+
145
+
146
+ def _update_pr_body_impl(
147
+ git: Git,
148
+ github: GitHub,
149
+ executor: PromptExecutor,
150
+ repo_root: Path,
151
+ issue_number: int,
152
+ run_id: str | None,
153
+ run_url: str | None,
154
+ plans_repo: str | None,
155
+ ) -> UpdateSuccess | UpdateError:
156
+ """Implementation of PR body update.
157
+
158
+ Args:
159
+ git: Git interface
160
+ github: GitHub interface
161
+ executor: PromptExecutor for Claude
162
+ repo_root: Repository root path
163
+ issue_number: Issue number to close on merge
164
+ run_id: Optional workflow run ID
165
+ run_url: Optional workflow run URL
166
+ plans_repo: Target repo in "owner/repo" format for cross-repo plans
167
+
168
+ Returns:
169
+ UpdateSuccess on success, UpdateError on failure
170
+ """
171
+ # Get current branch
172
+ current_branch = git.get_current_branch(repo_root)
173
+ if current_branch is None:
174
+ return UpdateError(
175
+ success=False,
176
+ error="pr-not-found",
177
+ message="Could not determine current branch",
178
+ stderr=None,
179
+ )
180
+
181
+ # Get PR for branch
182
+ pr_result = github.get_pr_for_branch(repo_root, current_branch)
183
+ if isinstance(pr_result, PRNotFound):
184
+ return UpdateError(
185
+ success=False,
186
+ error="pr-not-found",
187
+ message=f"No PR found for branch {current_branch}",
188
+ stderr=None,
189
+ )
190
+
191
+ pr_number = pr_result.number
192
+
193
+ # Get PR diff
194
+ try:
195
+ pr_diff = github.get_pr_diff(repo_root, pr_number)
196
+ except RuntimeError as e:
197
+ return UpdateError(
198
+ success=False,
199
+ error="diff-fetch-failed",
200
+ message=f"Failed to get PR diff: {e}",
201
+ stderr=None,
202
+ )
203
+
204
+ if not pr_diff.strip():
205
+ return UpdateError(
206
+ success=False,
207
+ error="empty-diff",
208
+ message="PR diff is empty",
209
+ stderr=None,
210
+ )
211
+
212
+ # Truncate diff if needed
213
+ diff_content, _was_truncated = truncate_diff(pr_diff)
214
+
215
+ # Get parent branch for context
216
+ parent_branch = git.detect_trunk_branch(repo_root)
217
+
218
+ # Generate summary using Claude
219
+ prompt = _build_prompt(diff_content, current_branch, parent_branch)
220
+ result = executor.execute_prompt(prompt, model="haiku", cwd=repo_root)
221
+
222
+ # Separate failure modes for better diagnostics
223
+ if not result.success:
224
+ stderr_preview = result.error[:500] if result.error else None
225
+ return UpdateError(
226
+ success=False,
227
+ error="claude-execution-failed",
228
+ message="Claude CLI returned non-zero exit code",
229
+ stderr=stderr_preview,
230
+ )
231
+
232
+ # Check for empty output (success=True but no content)
233
+ if not result.output or not result.output.strip():
234
+ stderr_preview = result.error[:500] if result.error else None
235
+ return UpdateError(
236
+ success=False,
237
+ error="claude-empty-output",
238
+ message="Claude returned empty output (check API quota, rate limits, or token)",
239
+ stderr=stderr_preview,
240
+ )
241
+
242
+ # Build full PR body
243
+ pr_body = _build_pr_body(result.output, pr_number, issue_number, run_id, run_url, plans_repo)
244
+
245
+ # Update PR body
246
+ try:
247
+ github.update_pr_body(repo_root, pr_number, pr_body)
248
+ except RuntimeError as e:
249
+ return UpdateError(
250
+ success=False,
251
+ error="github-api-failed",
252
+ message=f"Failed to update PR: {e}",
253
+ stderr=None,
254
+ )
255
+
256
+ return UpdateSuccess(success=True, pr_number=pr_number)
257
+
258
+
259
+ @click.command(name="ci-update-pr-body")
260
+ @click.option("--issue-number", type=int, required=True, help="Issue number to close on merge")
261
+ @click.option("--run-id", type=str, default=None, help="Optional workflow run ID")
262
+ @click.option("--run-url", type=str, default=None, help="Optional workflow run URL")
263
+ @click.pass_context
264
+ def ci_update_pr_body(
265
+ ctx: click.Context,
266
+ issue_number: int,
267
+ run_id: str | None,
268
+ run_url: str | None,
269
+ ) -> None:
270
+ """Update PR body with AI-generated summary and footer.
271
+
272
+ Generates a summary from the PR diff using Claude, then updates the PR body
273
+ with the summary, optional workflow link, and standardized footer with
274
+ checkout instructions.
275
+ """
276
+ git = require_git(ctx)
277
+ github = require_github(ctx)
278
+ executor = require_prompt_executor(ctx)
279
+ repo_root = require_repo_root(ctx)
280
+
281
+ # Load config to get plans_repo
282
+ config = load_config(repo_root)
283
+ plans_repo = config.plans_repo
284
+
285
+ result = _update_pr_body_impl(
286
+ git, github, executor, repo_root, issue_number, run_id, run_url, plans_repo
287
+ )
288
+
289
+ # Output JSON result
290
+ click.echo(json.dumps(asdict(result), indent=2))
291
+
292
+ # Exit with error code if update failed
293
+ if isinstance(result, UpdateError):
294
+ raise SystemExit(1)
@@ -0,0 +1,138 @@
1
+ """Create and push a branch for extraction documentation.
2
+
3
+ Usage:
4
+ erk exec create-extraction-branch \
5
+ --issue-number 123 \
6
+ --trunk-branch master
7
+
8
+ This command:
9
+ 1. Checks out the trunk branch
10
+ 2. Pulls latest changes
11
+ 3. Creates a new branch named extraction-docs-{issue_number}
12
+ 4. Pushes the branch with upstream tracking
13
+
14
+ Output:
15
+ JSON with success status and branch_name
16
+ """
17
+
18
+ import json
19
+ from pathlib import Path
20
+
21
+ import click
22
+
23
+ from erk_shared.context.helpers import require_git, require_repo_root
24
+
25
+
26
+ @click.command(name="create-extraction-branch")
27
+ @click.option(
28
+ "--issue-number",
29
+ type=int,
30
+ required=True,
31
+ help="GitHub issue number",
32
+ )
33
+ @click.option(
34
+ "--trunk-branch",
35
+ type=str,
36
+ required=True,
37
+ help="Name of trunk branch (main/master)",
38
+ )
39
+ @click.pass_context
40
+ def create_extraction_branch(
41
+ ctx: click.Context,
42
+ issue_number: int,
43
+ trunk_branch: str,
44
+ ) -> None:
45
+ """Create and push a branch for extraction documentation.
46
+
47
+ Creates a new branch from the trunk branch for implementing
48
+ documentation extraction from a GitHub issue.
49
+ """
50
+ git = require_git(ctx)
51
+ repo_root = require_repo_root(ctx)
52
+ cwd = Path.cwd()
53
+
54
+ branch_name = f"extraction-docs-P{issue_number}"
55
+
56
+ # Check if branch already exists locally
57
+ local_branches = git.list_local_branches(repo_root)
58
+ if branch_name in local_branches:
59
+ click.echo(
60
+ json.dumps(
61
+ {
62
+ "success": False,
63
+ "error": f"Branch {branch_name} already exists locally",
64
+ }
65
+ )
66
+ )
67
+ raise SystemExit(1)
68
+
69
+ # Checkout trunk branch
70
+ try:
71
+ git.checkout_branch(cwd, trunk_branch)
72
+ except Exception as e:
73
+ click.echo(
74
+ json.dumps(
75
+ {
76
+ "success": False,
77
+ "error": f"Failed to checkout {trunk_branch}: {e}",
78
+ }
79
+ )
80
+ )
81
+ raise SystemExit(1) from e
82
+
83
+ # Pull latest from trunk
84
+ try:
85
+ git.pull_branch(repo_root, "origin", trunk_branch, ff_only=True)
86
+ except Exception as e:
87
+ click.echo(
88
+ json.dumps(
89
+ {
90
+ "success": False,
91
+ "error": f"Failed to pull {trunk_branch}: {e}",
92
+ }
93
+ )
94
+ )
95
+ raise SystemExit(1) from e
96
+
97
+ # Create and checkout new branch
98
+ try:
99
+ git.create_branch(cwd, branch_name, trunk_branch)
100
+ git.checkout_branch(cwd, branch_name)
101
+ except Exception as e:
102
+ click.echo(
103
+ json.dumps(
104
+ {
105
+ "success": False,
106
+ "error": f"Failed to create branch {branch_name}: {e}",
107
+ }
108
+ )
109
+ )
110
+ raise SystemExit(1) from e
111
+
112
+ # Push with upstream tracking
113
+ try:
114
+ git.push_to_remote(cwd, "origin", branch_name, set_upstream=True)
115
+ except Exception as e:
116
+ click.echo(
117
+ json.dumps(
118
+ {
119
+ "success": False,
120
+ "error": f"Failed to push branch {branch_name}: {e}",
121
+ }
122
+ )
123
+ )
124
+ raise SystemExit(1) from e
125
+
126
+ click.echo(
127
+ json.dumps(
128
+ {
129
+ "success": True,
130
+ "branch_name": branch_name,
131
+ "issue_number": issue_number,
132
+ }
133
+ )
134
+ )
135
+
136
+
137
+ if __name__ == "__main__":
138
+ create_extraction_branch()
@@ -0,0 +1,242 @@
1
+ """Create extraction plan issue from file or content with proper metadata.
2
+
3
+ Usage (new - preferred):
4
+ erk exec create-extraction-plan \
5
+ --plan-content="# Plan Title..." \
6
+ --session-id="abc123" \
7
+ --extraction-session-ids="abc123,def456"
8
+
9
+ Usage (legacy - file path):
10
+ erk exec create-extraction-plan \
11
+ --plan-file=".erk/scratch/<session-id>/extraction-plan.md" \
12
+ --source-plan-issues="123,456" \
13
+ --extraction-session-ids="abc123,def456"
14
+
15
+ The --plan-content option is preferred because it:
16
+ 1. Automatically writes to .erk/scratch/<session-id>/extraction-plan.md
17
+ 2. Prevents agents from accidentally using /tmp/
18
+
19
+ This command:
20
+ 1. Creates GitHub issue with erk-plan + erk-extraction labels
21
+ 2. Sets plan_type: extraction in plan-header metadata
22
+ 3. Includes source_plan_issues and extraction_session_ids for tracking
23
+
24
+ Output:
25
+ JSON with success status, issue_number, and issue_url
26
+ """
27
+
28
+ import json
29
+ from pathlib import Path
30
+
31
+ import click
32
+
33
+ from erk_shared.context.helpers import (
34
+ require_cwd,
35
+ require_repo_root,
36
+ )
37
+ from erk_shared.context.helpers import (
38
+ require_issues as require_github_issues,
39
+ )
40
+ from erk_shared.github.plan_issues import create_plan_issue
41
+ from erk_shared.scratch.markers import PENDING_EXTRACTION_MARKER, delete_marker
42
+ from erk_shared.scratch.scratch import write_scratch_file
43
+
44
+
45
+ @click.command(name="create-extraction-plan")
46
+ @click.option(
47
+ "--plan-file",
48
+ type=click.Path(exists=True, path_type=Path),
49
+ default=None,
50
+ help="Path to plan file to create issue from (use --plan-content instead)",
51
+ )
52
+ @click.option(
53
+ "--plan-content",
54
+ type=str,
55
+ default=None,
56
+ help="Plan content to create issue from (preferred over --plan-file)",
57
+ )
58
+ @click.option(
59
+ "--session-id",
60
+ type=str,
61
+ default=None,
62
+ help="Session ID for scratch directory (required with --plan-content)",
63
+ )
64
+ @click.option(
65
+ "--source-plan-issues",
66
+ type=str,
67
+ default="",
68
+ help="Comma-separated list of source plan issue numbers (e.g., '123,456')",
69
+ )
70
+ @click.option(
71
+ "--extraction-session-ids",
72
+ type=str,
73
+ default="",
74
+ help="Comma-separated list of session IDs that were analyzed (e.g., 'abc123,def456')",
75
+ )
76
+ @click.pass_context
77
+ def create_extraction_plan(
78
+ ctx: click.Context,
79
+ plan_file: Path | None,
80
+ plan_content: str | None,
81
+ session_id: str | None,
82
+ source_plan_issues: str,
83
+ extraction_session_ids: str,
84
+ ) -> None:
85
+ """Create extraction plan issue from content or file with proper metadata.
86
+
87
+ Reads plan content from --plan-content or --plan-file and creates a GitHub issue with:
88
+ - erk-plan and erk-extraction labels
89
+ - plan_type: extraction in metadata
90
+ - Source tracking information
91
+
92
+ When using --plan-content, the content is automatically written to
93
+ .erk/scratch/<session-id>/extraction-plan.md
94
+ """
95
+ # Get required context
96
+ github = require_github_issues(ctx)
97
+ repo_root = require_repo_root(ctx)
98
+ cwd = require_cwd(ctx)
99
+
100
+ # Validate options: must provide either --plan-content or --plan-file
101
+ if plan_content is None and plan_file is None:
102
+ click.echo(
103
+ json.dumps(
104
+ {
105
+ "success": False,
106
+ "error": "Must provide either --plan-content or --plan-file",
107
+ }
108
+ )
109
+ )
110
+ raise SystemExit(1)
111
+
112
+ if plan_content is not None and plan_file is not None:
113
+ click.echo(
114
+ json.dumps(
115
+ {
116
+ "success": False,
117
+ "error": "Cannot provide both --plan-content and --plan-file",
118
+ }
119
+ )
120
+ )
121
+ raise SystemExit(1)
122
+
123
+ # Handle --plan-content: requires --session-id, writes to scratch
124
+ if plan_content is not None:
125
+ if session_id is None:
126
+ click.echo(
127
+ json.dumps(
128
+ {
129
+ "success": False,
130
+ "error": "--session-id is required when using --plan-content",
131
+ }
132
+ )
133
+ )
134
+ raise SystemExit(1)
135
+
136
+ # Write to scratch directory
137
+ scratch_path = write_scratch_file(
138
+ plan_content,
139
+ session_id=session_id,
140
+ suffix=".md",
141
+ prefix="extraction-plan-",
142
+ repo_root=repo_root,
143
+ )
144
+ content = plan_content.strip()
145
+ else:
146
+ # Handle --plan-file: read content from file
147
+ # plan_file is guaranteed to be not None here
148
+ assert plan_file is not None # for type checker
149
+ content = plan_file.read_text(encoding="utf-8").strip()
150
+ scratch_path = None
151
+
152
+ if not content:
153
+ click.echo(json.dumps({"success": False, "error": "Empty plan content"}))
154
+ raise SystemExit(1)
155
+
156
+ # Parse source plan issues
157
+ source_issues: list[int] = []
158
+ if source_plan_issues:
159
+ for part in source_plan_issues.split(","):
160
+ part = part.strip()
161
+ if part:
162
+ try:
163
+ source_issues.append(int(part))
164
+ except ValueError as e:
165
+ click.echo(
166
+ json.dumps(
167
+ {
168
+ "success": False,
169
+ "error": f"Invalid issue number: {part}",
170
+ }
171
+ )
172
+ )
173
+ raise SystemExit(1) from e
174
+
175
+ # Parse session IDs
176
+ session_ids: list[str] = []
177
+ if extraction_session_ids:
178
+ for part in extraction_session_ids.split(","):
179
+ part = part.strip()
180
+ if part:
181
+ session_ids.append(part)
182
+
183
+ # Validate: at least one session ID must be provided
184
+ if not session_ids:
185
+ click.echo(
186
+ json.dumps(
187
+ {
188
+ "success": False,
189
+ "error": "At least one extraction_session_id is required",
190
+ }
191
+ )
192
+ )
193
+ raise SystemExit(1)
194
+
195
+ # Use consolidated create_plan_issue for the entire workflow
196
+ result = create_plan_issue(
197
+ github_issues=github,
198
+ repo_root=repo_root,
199
+ plan_content=content,
200
+ title=None,
201
+ plan_type="extraction",
202
+ extra_labels=None,
203
+ title_suffix=None,
204
+ source_plan_issues=source_issues if source_issues else None,
205
+ extraction_session_ids=session_ids,
206
+ source_repo=None,
207
+ objective_issue=None,
208
+ )
209
+
210
+ if not result.success:
211
+ if result.issue_number is not None:
212
+ # Partial success - issue created but comment failed
213
+ click.echo(
214
+ json.dumps(
215
+ {
216
+ "success": False,
217
+ "error": result.error,
218
+ "issue_number": result.issue_number,
219
+ "issue_url": result.issue_url,
220
+ }
221
+ )
222
+ )
223
+ else:
224
+ click.echo(json.dumps({"success": False, "error": result.error}))
225
+ raise SystemExit(1)
226
+
227
+ # Delete pending extraction marker since extraction is complete
228
+ delete_marker(cwd, PENDING_EXTRACTION_MARKER)
229
+
230
+ # Output success
231
+ output: dict[str, object] = {
232
+ "success": True,
233
+ "issue_number": result.issue_number,
234
+ "issue_url": result.issue_url,
235
+ "title": result.title,
236
+ "plan_type": "extraction",
237
+ "source_plan_issues": source_issues,
238
+ "extraction_session_ids": session_ids,
239
+ }
240
+ if scratch_path is not None:
241
+ output["scratch_path"] = str(scratch_path)
242
+ click.echo(json.dumps(output))