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,103 @@
1
+ """Extract plan from Claude session and create GitHub issue.
2
+
3
+ Usage:
4
+ erk exec create-issue-from-session [--session-id SESSION_ID]
5
+
6
+ This command combines plan extraction from Claude session files with GitHub
7
+ issue creation. It extracts the latest ExitPlanMode plan, ensures the erk-plan
8
+ label exists, and creates a GitHub issue with the plan content.
9
+
10
+ SCHEMA VERSION 2: This command uses the new two-step creation flow:
11
+ 1. Create issue with metadata-only body (using format_plan_header_body())
12
+ 2. Add first comment with plan content (using format_plan_content_comment())
13
+
14
+ Output:
15
+ JSON result on stdout: {"success": true, "issue_number": N, "issue_url": "..."}
16
+ Error messages on stderr with exit code 1 on failure
17
+
18
+ Exit Codes:
19
+ 0: Success - issue created
20
+ 1: Error - no plan found, gh CLI not available, or other error
21
+ """
22
+
23
+ import json
24
+
25
+ import click
26
+
27
+ from erk_shared.context.helpers import (
28
+ require_claude_installation,
29
+ require_cwd,
30
+ require_repo_root,
31
+ )
32
+ from erk_shared.context.helpers import (
33
+ require_issues as require_github_issues,
34
+ )
35
+ from erk_shared.github.plan_issues import create_plan_issue
36
+
37
+
38
+ @click.command(name="create-issue-from-session")
39
+ @click.option(
40
+ "--session-id",
41
+ help="Session ID to search within (optional, searches all sessions if not provided)",
42
+ )
43
+ @click.pass_context
44
+ def create_issue_from_session(ctx: click.Context, session_id: str | None) -> None:
45
+ """Extract plan from Claude session and create GitHub issue.
46
+
47
+ Combines plan extraction with GitHub issue creation in a single operation.
48
+
49
+ Schema Version 2 format:
50
+ - Issue body: metadata-only (schema_version, created_at, created_by, worktree_name)
51
+ - First comment: plan content wrapped in markers
52
+ """
53
+ # Get dependencies from context
54
+ github = require_github_issues(ctx)
55
+ repo_root = require_repo_root(ctx)
56
+ cwd = require_cwd(ctx)
57
+ claude_installation = require_claude_installation(ctx)
58
+
59
+ # Extract latest plan from session
60
+ plan_text = claude_installation.get_latest_plan(cwd, session_id=session_id)
61
+
62
+ if not plan_text:
63
+ result = {"success": False, "error": "No plan found in Claude session files"}
64
+ click.echo(json.dumps(result))
65
+ raise SystemExit(1)
66
+
67
+ # Use consolidated create_plan_issue for the entire workflow
68
+ result = create_plan_issue(
69
+ github_issues=github,
70
+ repo_root=repo_root,
71
+ plan_content=plan_text,
72
+ title=None,
73
+ plan_type=None,
74
+ extra_labels=None,
75
+ title_suffix=None,
76
+ source_plan_issues=None,
77
+ extraction_session_ids=None,
78
+ source_repo=None,
79
+ objective_issue=None,
80
+ )
81
+
82
+ if not result.success:
83
+ if result.issue_number is not None:
84
+ # Partial success - issue created but comment failed
85
+ output = {
86
+ "success": False,
87
+ "error": result.error,
88
+ "issue_number": result.issue_number,
89
+ "issue_url": result.issue_url,
90
+ }
91
+ else:
92
+ output = {"success": False, "error": result.error}
93
+ click.echo(json.dumps(output))
94
+ raise SystemExit(1)
95
+
96
+ # Return success result
97
+ output = {
98
+ "success": True,
99
+ "issue_number": result.issue_number,
100
+ "issue_url": result.issue_url,
101
+ "title": result.title,
102
+ }
103
+ click.echo(json.dumps(output))
@@ -0,0 +1,103 @@
1
+ """Create GitHub issue from plan content (via stdin) with erk-plan label.
2
+
3
+ This exec command handles the complete workflow for creating a plan:
4
+ 1. Read plan from stdin
5
+ 2. Extract title from plan
6
+ 3. Ensure erk-plan label exists
7
+ 4. Create GitHub issue with plan body and label
8
+ 5. Return structured JSON result
9
+
10
+ This replaces the complex shell orchestration in the slash command with a single,
11
+ well-tested Python command that uses the ABC interface for GitHub operations.
12
+ """
13
+
14
+ import json
15
+ import sys
16
+
17
+ import click
18
+
19
+ from erk_shared.context.helpers import require_issues as require_github_issues
20
+ from erk_shared.context.helpers import require_repo_root
21
+ from erk_shared.github.metadata.core import format_plan_issue_body
22
+ from erk_shared.plan_utils import extract_title_from_plan
23
+
24
+
25
+ @click.command(name="create-plan-from-context")
26
+ @click.pass_context
27
+ def create_plan_from_context(ctx: click.Context) -> None:
28
+ """Create GitHub issue from plan content with erk-plan label.
29
+
30
+ Reads plan content from stdin, extracts title, ensures erk-plan label exists,
31
+ creates issue with collapsible plan body and execution commands, and returns JSON result.
32
+
33
+ Workflow:
34
+ 1. Create issue with plan body wrapped in collapsible metadata block
35
+ 2. Update issue body to include execution commands (using returned issue number)
36
+
37
+ Usage:
38
+ echo "$plan" | erk exec create-plan-from-context
39
+
40
+ Exit Codes:
41
+ 0: Success
42
+ 1: Error (empty plan, gh failure, etc.)
43
+
44
+ Output:
45
+ JSON object: {"success": true, "issue_number": 123, "issue_url": "..."}
46
+ """
47
+ # Get GitHub Issues from context (LBYL check in helper)
48
+ github = require_github_issues(ctx)
49
+ repo_root = require_repo_root(ctx)
50
+
51
+ # Read plan from stdin
52
+ plan = sys.stdin.read()
53
+
54
+ # Validate plan not empty
55
+ if not plan or not plan.strip():
56
+ click.echo("Error: Empty plan content received", err=True)
57
+ raise SystemExit(1)
58
+
59
+ # Extract title (pure function call)
60
+ title = extract_title_from_plan(plan)
61
+
62
+ # Initial body: just the plan content (without commands, since we don't have issue number yet)
63
+ # We'll update it after creation with the full formatted body including commands
64
+ initial_body = plan.strip()
65
+
66
+ # Ensure label exists (ABC interface)
67
+ try:
68
+ github.ensure_label_exists(
69
+ repo_root=repo_root,
70
+ label="erk-plan",
71
+ description="Implementation plan for manual execution",
72
+ color="0E8A16",
73
+ )
74
+ except RuntimeError as e:
75
+ click.echo(f"Error: Failed to ensure label exists: {e}", err=True)
76
+ raise SystemExit(1) from e
77
+
78
+ # Create issue (ABC interface with EAFP pattern)
79
+ # Add [erk-plan] suffix to title for visibility
80
+ issue_title = f"{title} [erk-plan]"
81
+ try:
82
+ result = github.create_issue(repo_root, issue_title, initial_body, ["erk-plan"])
83
+ except RuntimeError as e:
84
+ click.echo(f"Error: Failed to create GitHub issue: {e}", err=True)
85
+ raise SystemExit(1) from e
86
+
87
+ # Now that we have the issue number, format the complete body with commands
88
+ formatted_body = format_plan_issue_body(plan.strip(), result.number)
89
+
90
+ # Update the issue body with the formatted version
91
+ try:
92
+ github.update_issue_body(repo_root, result.number, formatted_body)
93
+ except RuntimeError as e:
94
+ click.echo(f"Error: Failed to update issue body: {e}", err=True)
95
+ raise SystemExit(1) from e
96
+
97
+ # Output structured JSON
98
+ output = {
99
+ "success": True,
100
+ "issue_number": result.number,
101
+ "issue_url": result.url,
102
+ }
103
+ click.echo(json.dumps(output))
@@ -0,0 +1,93 @@
1
+ """Create .worker-impl/ folder from GitHub issue with plan content.
2
+
3
+ This exec command fetches a plan from a GitHub issue and creates the .worker-impl/
4
+ folder structure, providing a testable alternative to inline workflow scripts.
5
+
6
+ Usage:
7
+ erk exec create-worker-impl-from-issue <issue-number>
8
+
9
+ Output:
10
+ Structured JSON output with success status and folder details
11
+
12
+ Exit Codes:
13
+ 0: Success (.worker-impl/ folder created)
14
+ 1: Error (issue not found, plan fetch failed, folder creation failed)
15
+
16
+ Examples:
17
+ $ erk exec create-worker-impl-from-issue 1028
18
+ {"success": true, "worker_impl_path": "/path/to/.worker-impl", "issue_number": 1028}
19
+
20
+ $ erk exec create-worker-impl-from-issue 999
21
+ {"success": false, "error": "issue_not_found", "message": "..."}
22
+ """
23
+
24
+ import json
25
+ from pathlib import Path
26
+
27
+ import click
28
+
29
+ from erk_shared.gateway.time.real import RealTime
30
+ from erk_shared.github.issues import RealGitHubIssues
31
+ from erk_shared.plan_store.github import GitHubPlanStore
32
+ from erk_shared.worker_impl_folder import create_worker_impl_folder
33
+
34
+
35
+ @click.command(name="create-worker-impl-from-issue")
36
+ @click.argument("issue_number", type=int)
37
+ @click.option(
38
+ "--repo-root",
39
+ type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
40
+ default=None,
41
+ help="Repository root directory (defaults to current directory)",
42
+ )
43
+ def create_worker_impl_from_issue(
44
+ issue_number: int,
45
+ repo_root: Path | None,
46
+ ) -> None:
47
+ """Create .worker-impl/ folder from GitHub issue with plan content.
48
+
49
+ Fetches plan content from GitHub issue and creates .worker-impl/ folder structure
50
+ with plan.md, issue.json, and metadata.
51
+
52
+ ISSUE_NUMBER: GitHub issue number containing the plan
53
+ """
54
+ # Default to current directory if not specified
55
+ if repo_root is None:
56
+ repo_root = Path.cwd()
57
+
58
+ # Direct instantiation of required dependencies (avoids erk import)
59
+ # This allows the command to work when run via erk kit exec without uv
60
+ time = RealTime()
61
+ github_issues = RealGitHubIssues(target_repo=None)
62
+ plan_store = GitHubPlanStore(github_issues, time)
63
+
64
+ # Fetch plan from GitHub (raises RuntimeError if not found)
65
+ try:
66
+ plan = plan_store.get_plan(repo_root, str(issue_number))
67
+ except RuntimeError as e:
68
+ error_output = {
69
+ "success": False,
70
+ "error": "plan_not_found",
71
+ "message": f"Could not fetch plan for issue #{issue_number}: {e}. "
72
+ f"Ensure issue has erk-plan label and plan content.",
73
+ }
74
+ click.echo(json.dumps(error_output), err=True)
75
+ raise SystemExit(1) from e
76
+
77
+ # Create .worker-impl/ folder with plan content
78
+ worker_impl_path = repo_root / ".worker-impl"
79
+ create_worker_impl_folder(
80
+ plan_content=plan.body,
81
+ issue_number=issue_number,
82
+ issue_url=plan.url,
83
+ repo_root=repo_root,
84
+ )
85
+
86
+ # Output structured success result
87
+ output = {
88
+ "success": True,
89
+ "worker_impl_path": str(worker_impl_path),
90
+ "issue_number": issue_number,
91
+ "issue_url": plan.url,
92
+ }
93
+ click.echo(json.dumps(output))
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """Detect whether repo uses `main` or `master` as trunk branch.
3
+
4
+ This command checks the remote (origin) for the existence of trunk branches,
5
+ suitable for CI environments where local branches may not be available.
6
+
7
+ This replaces bash-based detection in GitHub Actions workflows:
8
+ ```bash
9
+ if git ls-remote --heads origin main | grep -q main; then
10
+ echo "trunk_branch=main"
11
+ elif git ls-remote --heads origin master | grep -q master; then
12
+ echo "trunk_branch=master"
13
+ else
14
+ exit 1
15
+ fi
16
+ ```
17
+
18
+ Usage:
19
+ erk exec detect-trunk-branch
20
+
21
+ Output:
22
+ JSON object with success status and detected trunk branch
23
+
24
+ Exit Codes:
25
+ 0: Success (trunk branch detected)
26
+ 1: Error (neither main nor master exists on remote)
27
+
28
+ Examples:
29
+ $ erk exec detect-trunk-branch
30
+ {
31
+ "success": true,
32
+ "trunk_branch": "main"
33
+ }
34
+
35
+ $ erk exec detect-trunk-branch # in repo with master only
36
+ {
37
+ "success": true,
38
+ "trunk_branch": "master"
39
+ }
40
+
41
+ $ erk exec detect-trunk-branch # in repo without main/master
42
+ {
43
+ "success": false,
44
+ "error": "trunk_not_found",
45
+ "message": "Could not detect trunk branch (neither 'main' nor 'master' exists on origin)"
46
+ }
47
+ """
48
+
49
+ import json
50
+ from dataclasses import asdict, dataclass
51
+ from pathlib import Path
52
+ from typing import Literal
53
+
54
+ import click
55
+
56
+ from erk_shared.context.helpers import require_git, require_repo_root
57
+ from erk_shared.git.abc import Git
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class DetectedTrunk:
62
+ """Success result with detected trunk branch."""
63
+
64
+ success: bool
65
+ trunk_branch: str
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class DetectionError:
70
+ """Error result when trunk branch cannot be detected."""
71
+
72
+ success: bool
73
+ error: Literal["trunk-not-found", "git-error"]
74
+ message: str
75
+
76
+
77
+ def _detect_trunk_branch_impl(git: Git, repo_root: Path) -> DetectedTrunk | DetectionError:
78
+ """Detect trunk branch by checking remote for main or master.
79
+
80
+ Checks origin remote for 'main' first, then 'master'. Returns error
81
+ if neither exists.
82
+
83
+ Args:
84
+ git: Git interface for operations
85
+ repo_root: Path to the git repository root
86
+
87
+ Returns:
88
+ DetectedTrunk on success, DetectionError if neither branch found
89
+ """
90
+ # Check main first (modern convention), then master (legacy convention)
91
+ for candidate in ["main", "master"]:
92
+ if git.branch_exists_on_remote(repo_root, "origin", candidate):
93
+ return DetectedTrunk(success=True, trunk_branch=candidate)
94
+
95
+ # Neither found
96
+ return DetectionError(
97
+ success=False,
98
+ error="trunk-not-found",
99
+ message="Could not detect trunk branch (neither 'main' nor 'master' exists on origin)",
100
+ )
101
+
102
+
103
+ @click.command(name="detect-trunk-branch")
104
+ @click.pass_context
105
+ def detect_trunk_branch(ctx: click.Context) -> None:
106
+ """Detect whether repo uses main or master as trunk branch.
107
+
108
+ Queries the origin remote to check for trunk branch existence.
109
+ Checks for 'main' first, then 'master'. Exits with error if neither exists.
110
+ """
111
+ git = require_git(ctx)
112
+ repo_root = require_repo_root(ctx)
113
+
114
+ result = _detect_trunk_branch_impl(git, repo_root)
115
+
116
+ # Output JSON result
117
+ click.echo(json.dumps(asdict(result), indent=2))
118
+
119
+ # Exit with error code if detection failed
120
+ if isinstance(result, DetectionError):
121
+ raise SystemExit(1)