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,49 @@
1
+ """Extract the latest plan from Claude session files.
2
+
3
+ Usage:
4
+ erk exec extract-latest-plan [--session-id SESSION_ID]
5
+
6
+ This command searches Claude session files for the most recent ExitPlanMode
7
+ tool use and extracts the plan text. It can search either the current session
8
+ (if --session-id is provided) or all sessions for the project.
9
+
10
+ Output:
11
+ Plan text on stdout
12
+ Error message on stderr with exit code 1 on failure
13
+
14
+ Exit Codes:
15
+ 0: Success - plan found and output
16
+ 1: Error - no plan found or other error
17
+ """
18
+
19
+ import click
20
+
21
+ from erk_shared.context.helpers import require_claude_installation, require_cwd
22
+
23
+
24
+ @click.command(name="extract-latest-plan")
25
+ @click.option(
26
+ "--session-id",
27
+ help="Session ID to search within (optional, searches all sessions if not provided)",
28
+ )
29
+ @click.pass_context
30
+ def extract_latest_plan(ctx: click.Context, session_id: str | None) -> None:
31
+ """Extract the latest plan from Claude session files.
32
+
33
+ Searches for the most recent ExitPlanMode tool use and extracts the plan text.
34
+ """
35
+ # Get dependencies from context
36
+ cwd = require_cwd(ctx)
37
+ claude_installation = require_claude_installation(ctx)
38
+
39
+ # Extract latest plan
40
+ plan_text = claude_installation.get_latest_plan(cwd, session_id=session_id)
41
+
42
+ if not plan_text:
43
+ click.echo(
44
+ click.style("Error: ", fg="red") + "No plan found in Claude session files", err=True
45
+ )
46
+ raise SystemExit(1)
47
+
48
+ # Output plan text to stdout
49
+ click.echo(plan_text)
@@ -0,0 +1,150 @@
1
+ """Extract session XML content from a GitHub issue's comments.
2
+
3
+ Usage:
4
+ erk exec extract-session-from-issue <issue-number> [--output <path>]
5
+ erk exec extract-session-from-issue <issue-number> --stdout
6
+
7
+ This command:
8
+ 1. Fetches all comments from the specified GitHub issue
9
+ 2. Parses session-content metadata blocks from the comments
10
+ 3. Handles chunked content by combining in order
11
+ 4. Writes the combined XML to the output path (or stdout with --stdout)
12
+ 5. Returns metadata about the extracted session
13
+
14
+ Output:
15
+ Default: JSON with success status, session_file path, session_ids, and chunk_count
16
+ With --stdout: Session XML to stdout, metadata JSON to stderr
17
+ """
18
+
19
+ import json
20
+ from pathlib import Path
21
+
22
+ import click
23
+
24
+ from erk_shared.context.helpers import require_issues as require_github_issues
25
+ from erk_shared.context.helpers import require_repo_root
26
+ from erk_shared.github.metadata.session import extract_session_content_from_comments
27
+ from erk_shared.scratch.scratch import write_scratch_file
28
+
29
+
30
+ @click.command(name="extract-session-from-issue")
31
+ @click.argument("issue_number", type=int)
32
+ @click.option(
33
+ "--output",
34
+ type=click.Path(path_type=Path),
35
+ default=None,
36
+ help="Output path for the session XML (default: auto-generated in .erk/scratch/)",
37
+ )
38
+ @click.option(
39
+ "--session-id",
40
+ type=str,
41
+ default=None,
42
+ help="Session ID for scratch directory (used if --output not provided)",
43
+ )
44
+ @click.option(
45
+ "--stdout",
46
+ "use_stdout",
47
+ is_flag=True,
48
+ default=False,
49
+ help="Output session XML to stdout instead of writing to a file",
50
+ )
51
+ @click.pass_context
52
+ def extract_session_from_issue(
53
+ ctx: click.Context,
54
+ issue_number: int,
55
+ output: Path | None,
56
+ session_id: str | None,
57
+ use_stdout: bool,
58
+ ) -> None:
59
+ """Extract session XML from GitHub issue comments.
60
+
61
+ Reads all comments from the specified issue, finds session-content
62
+ metadata blocks, combines chunked content in order, and writes
63
+ the result to the output path.
64
+
65
+ ISSUE_NUMBER is the GitHub issue number to extract session data from.
66
+ """
67
+ # Get required context
68
+ github = require_github_issues(ctx)
69
+ repo_root = require_repo_root(ctx)
70
+
71
+ # Fetch issue comments
72
+ comments = github.get_issue_comments(repo_root, issue_number)
73
+
74
+ # Extract session content
75
+ session_xml, session_ids = extract_session_content_from_comments(comments)
76
+
77
+ if session_xml is None:
78
+ click.echo(
79
+ json.dumps(
80
+ {
81
+ "success": False,
82
+ "error": f"No session content found in issue #{issue_number}",
83
+ "issue_number": issue_number,
84
+ }
85
+ )
86
+ )
87
+ raise SystemExit(1)
88
+
89
+ # Handle --stdout: output XML to stdout, metadata to stderr
90
+ if use_stdout:
91
+ click.echo(session_xml)
92
+ click.echo(
93
+ json.dumps(
94
+ {
95
+ "success": True,
96
+ "issue_number": issue_number,
97
+ "session_ids": session_ids,
98
+ "chunk_count": len(session_ids) if session_ids else 1,
99
+ }
100
+ ),
101
+ err=True,
102
+ )
103
+ return
104
+
105
+ # Determine output path
106
+ if output is not None:
107
+ # Use explicit output path
108
+ output_path = output
109
+ output.parent.mkdir(parents=True, exist_ok=True)
110
+ output.write_text(session_xml, encoding="utf-8")
111
+ elif session_id is not None:
112
+ # Use scratch directory with provided session ID
113
+ output_path = write_scratch_file(
114
+ session_xml,
115
+ session_id=session_id,
116
+ suffix=".xml",
117
+ prefix="session-from-issue-",
118
+ repo_root=repo_root,
119
+ )
120
+ else:
121
+ # Generate session ID from first extracted session ID, or use issue number
122
+ fallback_session_id = session_ids[0] if session_ids else f"issue-{issue_number}"
123
+ output_path = write_scratch_file(
124
+ session_xml,
125
+ session_id=fallback_session_id,
126
+ suffix=".xml",
127
+ prefix="session-from-issue-",
128
+ repo_root=repo_root,
129
+ )
130
+
131
+ # Calculate chunk count from the original comments
132
+ chunk_count = len([s for s in session_ids]) # Approximate based on unique session IDs
133
+ if chunk_count == 0:
134
+ chunk_count = 1 # At least one chunk if we got content
135
+
136
+ click.echo(
137
+ json.dumps(
138
+ {
139
+ "success": True,
140
+ "issue_number": issue_number,
141
+ "session_file": str(output_path),
142
+ "session_ids": session_ids,
143
+ "chunk_count": chunk_count,
144
+ }
145
+ )
146
+ )
147
+
148
+
149
+ if __name__ == "__main__":
150
+ extract_session_from_issue()
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """Find Claude Code project directory for a given filesystem path.
3
+
4
+ This command provides deterministic mapping between filesystem paths and
5
+ Claude Code project directories in ~/.claude/projects/.
6
+
7
+ Claude Code encodes filesystem paths using a simple rule:
8
+ - Replace "/" with "-"
9
+ - Replace "." with "-"
10
+
11
+ Examples:
12
+ /Users/foo/bar → -Users-foo-bar
13
+ /Users/foo/.config/bar → -Users-foo--config-bar (double dash for dot)
14
+
15
+ This command returns the project directory path and metadata about session logs.
16
+
17
+ Usage:
18
+ # Find project directory for current directory
19
+ erk exec find-project-dir
20
+
21
+ # Find project directory for specific path
22
+ erk exec find-project-dir --path /some/path
23
+
24
+ # JSON output for scripting
25
+ erk exec find-project-dir --json
26
+
27
+ Output:
28
+ JSON object with success status and project information
29
+
30
+ Exit Codes:
31
+ 0: Success (project directory found)
32
+ 1: Error (project directory not found or other error)
33
+
34
+ Examples:
35
+ $ erk exec find-project-dir
36
+ {
37
+ "success": true,
38
+ "project_dir": "/Users/foo/.claude/projects/-Users-foo-code-erk",
39
+ "cwd": "/Users/foo/code/erk",
40
+ "encoded_path": "-Users-foo-code-erk",
41
+ "session_logs": ["abc123.jsonl", "agent-17cfd3f4.jsonl"],
42
+ "latest_session_id": "abc123"
43
+ }
44
+
45
+ $ erk exec find-project-dir --path /nonexistent
46
+ {
47
+ "success": false,
48
+ "error": "Project directory not found",
49
+ "help": "No Claude Code project found for /nonexistent",
50
+ "context": {
51
+ "path": "/nonexistent",
52
+ "encoded_path": "-nonexistent"
53
+ }
54
+ }
55
+ """
56
+
57
+ import json
58
+ import os
59
+ from dataclasses import asdict, dataclass
60
+ from pathlib import Path
61
+
62
+ import click
63
+
64
+ from erk_shared.context.helpers import require_claude_installation
65
+ from erk_shared.extraction.claude_installation import ClaudeInstallation
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ProjectInfo:
70
+ """Success result with project information."""
71
+
72
+ success: bool
73
+ project_dir: str
74
+ cwd: str
75
+ encoded_path: str
76
+ session_logs: list[str]
77
+ latest_session_id: str | None
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ProjectError:
82
+ """Error result when project directory not found."""
83
+
84
+ success: bool
85
+ error: str
86
+ help: str
87
+ context: dict[str, str]
88
+
89
+
90
+ def encode_path_to_project_folder(path: Path) -> str:
91
+ """Encode filesystem path to Claude Code project folder name.
92
+
93
+ Claude Code uses a simple encoding scheme:
94
+ - Replace "/" with "-"
95
+ - Replace "." with "-"
96
+
97
+ This creates deterministic folder names in ~/.claude/projects/.
98
+
99
+ Args:
100
+ path: Filesystem path to encode
101
+
102
+ Returns:
103
+ Encoded path suitable for project folder name
104
+
105
+ Examples:
106
+ >>> encode_path_to_project_folder(Path("/Users/foo/bar"))
107
+ '-Users-foo-bar'
108
+ >>> encode_path_to_project_folder(Path("/Users/foo/.config"))
109
+ '-Users-foo--config'
110
+ """
111
+ return str(path).replace("/", "-").replace(".", "-")
112
+
113
+
114
+ def find_project_info(path: Path, installation: ClaudeInstallation) -> ProjectInfo | ProjectError:
115
+ """Find Claude Code project directory and metadata for given path.
116
+
117
+ Args:
118
+ path: Filesystem path to find project for
119
+ installation: ClaudeInstallation gateway for accessing projects directory
120
+
121
+ Returns:
122
+ ProjectInfo on success, ProjectError if not found
123
+ """
124
+ projects_dir = installation.get_projects_dir_path()
125
+ if not installation.projects_dir_exists():
126
+ return ProjectError(
127
+ success=False,
128
+ error="Claude Code projects directory not found",
129
+ help="~/.claude/projects/ does not exist. Is Claude Code installed?",
130
+ context={
131
+ "path": str(path),
132
+ "projects_dir": str(projects_dir),
133
+ },
134
+ )
135
+
136
+ # Encode path and find project directory
137
+ encoded_path = encode_path_to_project_folder(path)
138
+ project_dir = projects_dir / encoded_path
139
+
140
+ if not project_dir.exists():
141
+ return ProjectError(
142
+ success=False,
143
+ error="Project directory not found",
144
+ help=f"No Claude Code project found for {path}",
145
+ context={
146
+ "path": str(path),
147
+ "encoded_path": encoded_path,
148
+ "expected_dir": str(project_dir),
149
+ },
150
+ )
151
+
152
+ # Find all session logs (main sessions and agent logs)
153
+ session_logs = []
154
+ latest_session: tuple[str, float] | None = None
155
+
156
+ for log_file in project_dir.iterdir():
157
+ if log_file.is_file() and log_file.suffix == ".jsonl":
158
+ session_logs.append(log_file.name)
159
+
160
+ # Track latest main session (not agent logs)
161
+ if not log_file.name.startswith("agent-"):
162
+ mtime = log_file.stat().st_mtime
163
+ if latest_session is None or mtime > latest_session[1]:
164
+ # Extract session ID (filename without .jsonl)
165
+ session_id = log_file.stem
166
+ latest_session = (session_id, mtime)
167
+
168
+ # Sort logs for consistent output
169
+ session_logs.sort()
170
+
171
+ return ProjectInfo(
172
+ success=True,
173
+ project_dir=str(project_dir),
174
+ cwd=str(path),
175
+ encoded_path=encoded_path,
176
+ session_logs=session_logs,
177
+ latest_session_id=latest_session[0] if latest_session else None,
178
+ )
179
+
180
+
181
+ @click.command(name="find-project-dir")
182
+ @click.option(
183
+ "--path",
184
+ type=click.Path(exists=True, path_type=Path),
185
+ help="Path to find project for (defaults to current directory)",
186
+ )
187
+ @click.option(
188
+ "--json",
189
+ "json_output",
190
+ is_flag=True,
191
+ help="Output in JSON format",
192
+ )
193
+ @click.pass_context
194
+ def find_project_dir(ctx: click.Context, path: Path | None, json_output: bool) -> None:
195
+ """Find Claude Code project directory for a filesystem path.
196
+
197
+ This command maps filesystem paths to Claude Code project directories
198
+ in ~/.claude/projects/ using deterministic encoding rules.
199
+ """
200
+ installation = require_claude_installation(ctx)
201
+
202
+ # Default to current directory if no path specified
203
+ if path is None:
204
+ path = Path(os.getcwd())
205
+
206
+ # Find project information
207
+ result = find_project_info(path, installation)
208
+
209
+ # Always output JSON (the --json flag is for future extensibility)
210
+ click.echo(json.dumps(asdict(result), indent=2))
211
+
212
+ # Exit with error code if not found
213
+ if isinstance(result, ProjectError):
214
+ raise SystemExit(1)
@@ -0,0 +1,112 @@
1
+ """Generate PR summary from PR diff.
2
+
3
+ This exec command generates a PR summary by analyzing the PR diff
4
+ using Claude. It uses the same prompt as commit message generation but
5
+ does NOT include commit messages (which may contain misleading info
6
+ about .worker-impl/ deletions).
7
+
8
+ This is used by the GitHub Actions workflow when updating PR bodies
9
+ after implementation.
10
+
11
+ Usage:
12
+ erk exec generate-pr-summary --pr-number 123
13
+
14
+ Output:
15
+ PR summary text (title on first line, body follows)
16
+
17
+ Exit Codes:
18
+ 0: Success
19
+ 1: Error (missing pr-number, no diff, Claude failure)
20
+
21
+ Examples:
22
+ $ erk exec generate-pr-summary --pr-number 1895
23
+ Fix authentication flow for OAuth providers
24
+
25
+ This PR fixes the OAuth authentication flow...
26
+ ...
27
+ """
28
+
29
+ import click
30
+
31
+ from erk_shared.context.helpers import (
32
+ require_git,
33
+ require_github,
34
+ require_prompt_executor,
35
+ require_repo_root,
36
+ )
37
+ from erk_shared.gateway.gt.prompts import COMMIT_MESSAGE_SYSTEM_PROMPT, truncate_diff
38
+
39
+
40
+ def _build_prompt(diff_content: str, current_branch: str, parent_branch: str) -> str:
41
+ """Build prompt for PR summary generation.
42
+
43
+ Note: We deliberately do NOT include commit messages here, unlike
44
+ CommitMessageGenerator. The commit messages may contain info about
45
+ .worker-impl/ deletions that don't appear in the final PR diff.
46
+ """
47
+ context_section = f"""## Context
48
+
49
+ - Current branch: {current_branch}
50
+ - Parent branch: {parent_branch}"""
51
+
52
+ return f"""{COMMIT_MESSAGE_SYSTEM_PROMPT}
53
+
54
+ {context_section}
55
+
56
+ ## Diff
57
+
58
+ ```diff
59
+ {diff_content}
60
+ ```
61
+
62
+ Generate a commit message for this diff:"""
63
+
64
+
65
+ @click.command(name="generate-pr-summary")
66
+ @click.option("--pr-number", type=int, required=True, help="PR number to summarize")
67
+ @click.pass_context
68
+ def generate_pr_summary(ctx: click.Context, pr_number: int) -> None:
69
+ """Generate PR summary from PR diff using Claude.
70
+
71
+ Analyzes the PR diff (what GitHub shows) and generates a summary.
72
+ Does NOT use commit messages, which may contain misleading info
73
+ about files that net to zero in the final diff.
74
+
75
+ Args:
76
+ pr_number: The PR number to analyze
77
+ """
78
+ repo_root = require_repo_root(ctx)
79
+ github = require_github(ctx)
80
+ git = require_git(ctx)
81
+ executor = require_prompt_executor(ctx)
82
+
83
+ # Get PR diff
84
+ try:
85
+ pr_diff = github.get_pr_diff(repo_root, pr_number)
86
+ except RuntimeError as e:
87
+ click.echo(f"Error: Failed to get PR diff: {e}", err=True)
88
+ raise SystemExit(1) from e
89
+
90
+ if not pr_diff.strip():
91
+ click.echo("Error: PR diff is empty", err=True)
92
+ raise SystemExit(1)
93
+
94
+ # Truncate if needed
95
+ diff_content, was_truncated = truncate_diff(pr_diff)
96
+ if was_truncated:
97
+ click.echo("Warning: Diff truncated for size", err=True)
98
+
99
+ # Get branch context using injected Git
100
+ current_branch = git.get_current_branch(repo_root) or f"pr-{pr_number}"
101
+ parent_branch = git.detect_trunk_branch(repo_root)
102
+
103
+ # Build prompt and run Claude via injected executor
104
+ prompt = _build_prompt(diff_content, current_branch, parent_branch)
105
+ result = executor.execute_prompt(prompt, model="haiku", cwd=repo_root)
106
+
107
+ if not result.success:
108
+ click.echo(f"Error: Claude execution failed: {result.error}", err=True)
109
+ raise SystemExit(1) from None
110
+
111
+ # Output the summary (no trailing newline, let caller handle formatting)
112
+ click.echo(result.output, nl=False)
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Get closing text for PR body based on .impl/issue.json or branch name.
3
+
4
+ This command determines the issue number from .impl/issue.json or the branch
5
+ name (P{issue_number}-... pattern) and outputs the appropriate closing text.
6
+
7
+ Usage:
8
+ erk exec get-closing-text
9
+
10
+ Output:
11
+ Plain text "Closes #N" (same-repo) or "Closes owner/repo#N" (cross-repo)
12
+ Empty output if no issue reference found
13
+
14
+ Exit Codes:
15
+ 0: Success (whether issue reference exists or not)
16
+ 1: Error (branch/issue.json mismatch)
17
+
18
+ Examples:
19
+ $ erk exec get-closing-text
20
+ Closes #776
21
+
22
+ $ erk exec get-closing-text # Cross-repo plans
23
+ Closes owner/plans-repo#776
24
+
25
+ $ erk exec get-closing-text # No .impl/issue.json but branch is P123-feature
26
+ Closes #123
27
+
28
+ $ erk exec get-closing-text # No .impl/ and branch is feature-branch
29
+ (no output)
30
+ """
31
+
32
+ from pathlib import Path
33
+
34
+ import click
35
+
36
+ from erk.cli.config import load_config
37
+ from erk_shared.context.helpers import get_current_branch, require_cwd
38
+ from erk_shared.impl_folder import validate_issue_linkage
39
+
40
+
41
+ def _find_repo_root(start: Path) -> Path | None:
42
+ """Find repository root by looking for .git directory."""
43
+ current = start
44
+ while current != current.parent:
45
+ if (current / ".git").exists():
46
+ return current
47
+ current = current.parent
48
+ return None
49
+
50
+
51
+ @click.command(name="get-closing-text")
52
+ @click.pass_context
53
+ def get_closing_text(ctx: click.Context) -> None:
54
+ """Get closing text for PR body based on .impl/issue.json or branch name.
55
+
56
+ Validates that branch name and .impl/issue.json agree (if both present).
57
+ Falls back to branch name if no .impl/ folder exists.
58
+
59
+ Outputs nothing and exits successfully if no issue number is discoverable.
60
+ """
61
+ cwd = require_cwd(ctx)
62
+
63
+ # Get current branch name for validation and fallback
64
+ branch_name = get_current_branch(ctx)
65
+ if branch_name is None:
66
+ # Not on a branch (detached HEAD) - can't determine issue number
67
+ return
68
+
69
+ # Check .impl/ first, then .worker-impl/
70
+ impl_dir = cwd / ".impl"
71
+ if not impl_dir.exists():
72
+ impl_dir = cwd / ".worker-impl"
73
+
74
+ # Validate linkage and get issue number (branch fallback if no .impl/)
75
+ try:
76
+ issue_number = validate_issue_linkage(impl_dir, branch_name)
77
+ except ValueError as e:
78
+ click.echo(f"Error: {e}", err=True)
79
+ raise SystemExit(1) from None
80
+
81
+ if issue_number is None:
82
+ # No issue to close (neither branch nor .impl/ has one)
83
+ return
84
+
85
+ # Load config to check for cross-repo plans
86
+ repo_root = _find_repo_root(cwd)
87
+ plans_repo: str | None = None
88
+ if repo_root is not None:
89
+ config = load_config(repo_root)
90
+ plans_repo = config.plans_repo
91
+
92
+ # Format closing text
93
+ if plans_repo is None:
94
+ closing_text = f"Closes #{issue_number}"
95
+ else:
96
+ closing_text = f"Closes {plans_repo}#{issue_number}"
97
+
98
+ click.echo(closing_text)
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Get embedded prompt content from bundled prompts.
3
+
4
+ This command reads prompt files bundled with the erk package and outputs
5
+ their content. Useful for GitHub Actions workflows that need prompt content.
6
+
7
+ Usage:
8
+ erk exec get-embedded-prompt <prompt-name>
9
+
10
+ Output:
11
+ The prompt content (markdown)
12
+
13
+ Exit Codes:
14
+ 0: Success
15
+ 1: Prompt not found
16
+
17
+ Examples:
18
+ $ erk exec get-embedded-prompt dignified-python-review
19
+ # Dignified Python Review Prompt
20
+ ...
21
+
22
+ $ erk exec get-embedded-prompt dignified-python-review > /tmp/prompt.md
23
+ """
24
+
25
+ import click
26
+
27
+ from erk.artifacts.sync import get_bundled_github_dir
28
+
29
+ # Available prompts that can be retrieved
30
+ AVAILABLE_PROMPTS = frozenset(
31
+ {
32
+ "ci-autofix",
33
+ "dignified-python-review",
34
+ }
35
+ )
36
+
37
+
38
+ @click.command(name="get-embedded-prompt")
39
+ @click.argument("prompt_name")
40
+ def get_embedded_prompt(prompt_name: str) -> None:
41
+ """Get embedded prompt content from bundled prompts.
42
+
43
+ Reads the specified prompt from the erk package's bundled prompts
44
+ and outputs its content to stdout.
45
+
46
+ PROMPT_NAME is the name of the prompt (without .md extension).
47
+ """
48
+ if prompt_name not in AVAILABLE_PROMPTS:
49
+ available = ", ".join(sorted(AVAILABLE_PROMPTS))
50
+ click.echo(f"Unknown prompt: {prompt_name}", err=True)
51
+ click.echo(f"Available prompts: {available}", err=True)
52
+ raise SystemExit(1)
53
+
54
+ bundled_github_dir = get_bundled_github_dir()
55
+ prompt_path = bundled_github_dir / "prompts" / f"{prompt_name}.md"
56
+
57
+ if not prompt_path.exists():
58
+ click.echo(f"Prompt file not found: {prompt_path}", err=True)
59
+ raise SystemExit(1)
60
+
61
+ content = prompt_path.read_text(encoding="utf-8")
62
+ click.echo(content, nl=False)