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,148 @@
1
+ """Create a new planner codespace."""
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ import click
7
+
8
+ from erk.core.context import ErkContext
9
+ from erk.core.planner.types import RegisteredPlanner
10
+
11
+
12
+ def _find_codespace_by_display_name(display_name: str) -> dict | None:
13
+ """Find a codespace by its display name."""
14
+ # GH-API-AUDIT: REST - GET user/codespaces
15
+ result = subprocess.run(
16
+ ["gh", "codespace", "list", "--json", "name,repository,displayName"],
17
+ capture_output=True,
18
+ text=True,
19
+ check=False,
20
+ )
21
+ if result.returncode != 0:
22
+ return None
23
+
24
+ content = result.stdout.strip()
25
+ if not content:
26
+ return None
27
+
28
+ try:
29
+ codespaces = json.loads(content)
30
+ for cs in codespaces:
31
+ if cs.get("displayName") == display_name:
32
+ return cs
33
+ return None
34
+ except json.JSONDecodeError:
35
+ return None
36
+
37
+
38
+ @click.command("create")
39
+ @click.argument("name", default="erk-planner-node")
40
+ @click.option(
41
+ "-r",
42
+ "--repo",
43
+ default=None,
44
+ help="Repository to create codespace from (owner/repo). Defaults to current repo.",
45
+ )
46
+ @click.option(
47
+ "-b",
48
+ "--branch",
49
+ default=None,
50
+ help="Branch to create codespace from. Defaults to default branch.",
51
+ )
52
+ @click.option(
53
+ "--run/--dry-run",
54
+ default=False,
55
+ help="Actually run the command (default: just print it).",
56
+ )
57
+ @click.pass_obj
58
+ def create_planner(
59
+ ctx: ErkContext,
60
+ name: str,
61
+ repo: str | None,
62
+ branch: str | None,
63
+ run: bool,
64
+ ) -> None:
65
+ """Create a new GitHub Codespace for use as a planner.
66
+
67
+ Creates a codespace with the right devcontainer configuration and
68
+ automatically registers it as a planner.
69
+
70
+ The codespace will be created with:
71
+ - GitHub CLI pre-installed
72
+ - Claude Code pre-installed
73
+ - uv and project dependencies
74
+
75
+ After creation, run 'erk planner configure NAME' to set up authentication.
76
+ """
77
+ # Check if name already exists
78
+ existing = ctx.planner_registry.get(name)
79
+ if existing is not None:
80
+ click.echo(f"Error: A planner named '{name}' already exists.", err=True)
81
+ raise SystemExit(1)
82
+
83
+ # GH-API-AUDIT: REST - POST user/codespaces
84
+ cmd = ["gh", "codespace", "create"]
85
+
86
+ if repo:
87
+ cmd.extend(["--repo", repo])
88
+
89
+ if branch:
90
+ cmd.extend(["--branch", branch])
91
+
92
+ cmd.extend(["--display-name", name])
93
+ cmd.extend(["--devcontainer-path", ".devcontainer/devcontainer.json"])
94
+
95
+ if run:
96
+ click.echo(f"Creating codespace '{name}'...", err=True)
97
+ click.echo(f"Running: {' '.join(cmd)}", err=True)
98
+ click.echo("", err=True)
99
+
100
+ result = subprocess.run(cmd, check=False)
101
+
102
+ if result.returncode != 0:
103
+ click.echo(f"\nCodespace creation failed (exit code {result.returncode}).", err=True)
104
+ raise SystemExit(1)
105
+
106
+ # Find and register the created codespace
107
+ click.echo("", err=True)
108
+ click.echo("Looking up created codespace...", err=True)
109
+
110
+ codespace = _find_codespace_by_display_name(name)
111
+ if codespace is None:
112
+ click.echo(f"Warning: Could not find codespace '{name}' to register.", err=True)
113
+ click.echo(f"Run 'erk planner register {name}' manually.", err=True)
114
+ raise SystemExit(1)
115
+
116
+ gh_name = codespace.get("name", "")
117
+ repository = codespace.get("repository", "")
118
+
119
+ planner = RegisteredPlanner(
120
+ name=name,
121
+ gh_name=gh_name,
122
+ repository=repository,
123
+ configured=False,
124
+ registered_at=ctx.time.now(),
125
+ last_connected_at=None,
126
+ )
127
+ ctx.planner_registry.register(planner)
128
+
129
+ # Set as default if first planner
130
+ if len(ctx.planner_registry.list_planners()) == 1:
131
+ ctx.planner_registry.set_default(name)
132
+ click.echo(f"Registered planner '{name}' (set as default)", err=True)
133
+ else:
134
+ click.echo(f"Registered planner '{name}'", err=True)
135
+
136
+ click.echo("", err=True)
137
+ click.echo("Next step:", err=True)
138
+ click.echo(f" erk planner configure {name}", err=True)
139
+ else:
140
+ click.echo("Run this command to create the codespace:", err=True)
141
+ click.echo("", err=True)
142
+ click.echo(f" {' '.join(cmd)}", err=True)
143
+ click.echo("", err=True)
144
+ click.echo("Or run with --run to execute directly:", err=True)
145
+ click.echo(f" erk planner create {name} --run", err=True)
146
+ click.echo("", err=True)
147
+ click.echo("After creation, configure authentication:", err=True)
148
+ click.echo(f" erk planner configure {name}", err=True)
@@ -0,0 +1,51 @@
1
+ """List registered planner boxes."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from erk.core.context import ErkContext
8
+
9
+
10
+ @click.command("list")
11
+ @click.pass_obj
12
+ def list_planners(ctx: ErkContext) -> None:
13
+ """List all registered planner boxes."""
14
+ planners = ctx.planner_registry.list_planners()
15
+ default_name = ctx.planner_registry.get_default_name()
16
+
17
+ if not planners:
18
+ click.echo("No planners registered.", err=True)
19
+ click.echo("\nUse 'erk planner register <name>' to register a codespace.", err=True)
20
+ return
21
+
22
+ # Create Rich table
23
+ table = Table(show_header=True, header_style="bold", box=None)
24
+ table.add_column("name", style="cyan", no_wrap=True)
25
+ table.add_column("repository", style="yellow", no_wrap=True)
26
+ table.add_column("configured", no_wrap=True)
27
+ table.add_column("last connected", no_wrap=True)
28
+
29
+ for planner in sorted(planners, key=lambda p: p.name):
30
+ # Name with default indicator
31
+ name_cell = planner.name
32
+ if planner.name == default_name:
33
+ name_cell = f"[cyan bold]{planner.name}[/cyan bold] (default)"
34
+ else:
35
+ name_cell = f"[cyan]{planner.name}[/cyan]"
36
+
37
+ # Configured status
38
+ configured_cell = "[green]yes[/green]" if planner.configured else "[yellow]no[/yellow]"
39
+
40
+ # Last connected
41
+ if planner.last_connected_at:
42
+ # Format as relative time or date
43
+ last_connected = planner.last_connected_at.strftime("%Y-%m-%d %H:%M")
44
+ else:
45
+ last_connected = "-"
46
+
47
+ table.add_row(name_cell, planner.repository, configured_cell, last_connected)
48
+
49
+ # Output table to stderr (consistent with erk conventions)
50
+ console = Console(stderr=True, force_terminal=True)
51
+ console.print(table)
@@ -0,0 +1,105 @@
1
+ """Register an existing GitHub Codespace as a planner box."""
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ import click
7
+
8
+ from erk.cli.ensure import Ensure
9
+ from erk.core.context import ErkContext
10
+ from erk.core.planner.types import RegisteredPlanner
11
+
12
+
13
+ def _list_codespaces() -> list[dict]:
14
+ """List available codespaces from GitHub.
15
+
16
+ Returns:
17
+ List of codespace dicts with name, repository, displayName fields
18
+ """
19
+ # GH-API-AUDIT: REST - GET user/codespaces
20
+ result = subprocess.run(
21
+ ["gh", "codespace", "list", "--json", "name,repository,displayName"],
22
+ capture_output=True,
23
+ text=True,
24
+ check=False,
25
+ )
26
+
27
+ if result.returncode != 0:
28
+ return []
29
+
30
+ # JSON parsing requires exception handling for malformed data
31
+ content = result.stdout.strip()
32
+ if not content:
33
+ return []
34
+ try:
35
+ parsed = json.loads(content)
36
+ if isinstance(parsed, list):
37
+ return parsed
38
+ return []
39
+ except json.JSONDecodeError:
40
+ return []
41
+
42
+
43
+ @click.command("register")
44
+ @click.argument("name")
45
+ @click.pass_obj
46
+ def register_planner(ctx: ErkContext, name: str) -> None:
47
+ """Register an existing GitHub Codespace as a planner box.
48
+
49
+ Lists available codespaces and prompts you to select one.
50
+ The selected codespace will be registered under NAME.
51
+ """
52
+ # Check if name already exists
53
+ existing = ctx.planner_registry.get(name)
54
+ if existing is not None:
55
+ click.echo(f"Error: A planner named '{name}' already exists.", err=True)
56
+ click.echo(f"Use 'erk planner unregister {name}' first to remove it.", err=True)
57
+ raise SystemExit(1)
58
+
59
+ # List available codespaces
60
+ click.echo("Fetching available codespaces...", err=True)
61
+ codespaces = _list_codespaces()
62
+
63
+ if not codespaces:
64
+ click.echo("No codespaces found.", err=True)
65
+ click.echo("\nCreate a codespace first, then run this command again.", err=True)
66
+ raise SystemExit(1)
67
+
68
+ # Display available codespaces
69
+ click.echo("\nAvailable codespaces:", err=True)
70
+ for i, cs in enumerate(codespaces, 1):
71
+ display_name = cs.get("displayName", cs.get("name", "unknown"))
72
+ repo = cs.get("repository", "unknown")
73
+ click.echo(f" {i}. {display_name} ({repo})", err=True)
74
+
75
+ # Prompt for selection
76
+ click.echo("", err=True)
77
+ selection = click.prompt(
78
+ "Select codespace number",
79
+ type=click.IntRange(1, len(codespaces)),
80
+ )
81
+
82
+ selected = codespaces[selection - 1]
83
+ gh_name = Ensure.truthy(selected.get("name", ""), "Could not get codespace name.")
84
+ repository = selected.get("repository", "")
85
+
86
+ # Create and register the planner
87
+ planner = RegisteredPlanner(
88
+ name=name,
89
+ gh_name=gh_name,
90
+ repository=repository,
91
+ configured=False,
92
+ registered_at=ctx.time.now(),
93
+ last_connected_at=None,
94
+ )
95
+
96
+ ctx.planner_registry.register(planner)
97
+
98
+ # If this is the first planner, set it as default
99
+ if len(ctx.planner_registry.list_planners()) == 1:
100
+ ctx.planner_registry.set_default(name)
101
+ click.echo(f"\nRegistered planner '{name}' (set as default)", err=True)
102
+ else:
103
+ click.echo(f"\nRegistered planner '{name}'", err=True)
104
+
105
+ click.echo(f"\nRun 'erk planner configure {name}' for initial setup.", err=True)
@@ -0,0 +1,23 @@
1
+ """Set the default planner box."""
2
+
3
+ import click
4
+
5
+ from erk.cli.ensure import Ensure
6
+ from erk.core.context import ErkContext
7
+
8
+
9
+ @click.command("set-default")
10
+ @click.argument("name")
11
+ @click.pass_obj
12
+ def set_default_planner(ctx: ErkContext, name: str) -> None:
13
+ """Set the default planner box.
14
+
15
+ The default planner is used when running 'erk planner' without arguments.
16
+ """
17
+ _planner = Ensure.not_none(
18
+ ctx.planner_registry.get(name),
19
+ f"No planner named '{name}' found.\n\nUse 'erk planner list' to see registered planners.",
20
+ )
21
+
22
+ ctx.planner_registry.set_default(name)
23
+ click.echo(f"Set '{name}' as the default planner.", err=True)
@@ -0,0 +1,43 @@
1
+ """Unregister a planner box."""
2
+
3
+ import click
4
+
5
+ from erk.core.context import ErkContext
6
+
7
+
8
+ @click.command("unregister")
9
+ @click.argument("name")
10
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
11
+ @click.pass_obj
12
+ def unregister_planner(ctx: ErkContext, name: str, force: bool) -> None:
13
+ """Remove a planner box from the registry.
14
+
15
+ This does not delete the codespace - it only removes the registration.
16
+ """
17
+ planner = ctx.planner_registry.get(name)
18
+ if planner is None:
19
+ click.echo(f"Error: No planner named '{name}' found.", err=True)
20
+ click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
21
+ raise SystemExit(1)
22
+
23
+ # Check if this is the default
24
+ is_default = ctx.planner_registry.get_default_name() == name
25
+
26
+ if not force:
27
+ msg = f"Unregister planner '{name}'?"
28
+ if is_default:
29
+ msg = f"Unregister planner '{name}' (currently the default)?"
30
+ if not click.confirm(msg):
31
+ click.echo("Cancelled.", err=True)
32
+ raise SystemExit(0)
33
+
34
+ ctx.planner_registry.unregister(name)
35
+
36
+ click.echo(f"Unregistered planner '{name}'.", err=True)
37
+ if is_default:
38
+ click.echo("Note: Default planner has been cleared.", err=True)
39
+
40
+ # Suggest setting a new default if there are other planners
41
+ remaining = ctx.planner_registry.list_planners()
42
+ if remaining and is_default:
43
+ click.echo("\nUse 'erk planner set-default <name>' to set a new default.", err=True)
@@ -0,0 +1,23 @@
1
+ """PR management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.alias import register_with_aliases
6
+ from erk.cli.commands.pr.check_cmd import pr_check
7
+ from erk.cli.commands.pr.checkout_cmd import pr_checkout
8
+ from erk.cli.commands.pr.fix_conflicts_cmd import pr_fix_conflicts
9
+ from erk.cli.commands.pr.submit_cmd import pr_submit
10
+ from erk.cli.commands.pr.sync_cmd import pr_sync
11
+
12
+
13
+ @click.group("pr")
14
+ def pr_group() -> None:
15
+ """Manage pull requests."""
16
+ pass
17
+
18
+
19
+ pr_group.add_command(pr_check, name="check")
20
+ register_with_aliases(pr_group, pr_checkout)
21
+ pr_group.add_command(pr_fix_conflicts, name="fix-conflicts")
22
+ pr_group.add_command(pr_submit, name="submit")
23
+ pr_group.add_command(pr_sync, name="sync")
@@ -0,0 +1,112 @@
1
+ """Command to validate PR rules for the current branch."""
2
+
3
+ import click
4
+
5
+ from erk.cli.ensure import Ensure
6
+ from erk.core.context import ErkContext
7
+ from erk_shared.gateway.pr.submit import (
8
+ has_checkout_footer_for_pr,
9
+ has_issue_closing_reference,
10
+ )
11
+ from erk_shared.github.types import PRNotFound
12
+ from erk_shared.impl_folder import read_issue_reference, validate_issue_linkage
13
+ from erk_shared.output.output import user_output
14
+
15
+
16
+ @click.command("check")
17
+ @click.pass_obj
18
+ def pr_check(ctx: ErkContext) -> None:
19
+ """Validate PR rules for the current branch.
20
+
21
+ Checks that the PR:
22
+ 1. Has issue closing reference (Closes #N) when .impl/issue.json exists
23
+ 2. Has the standard checkout command footer
24
+ """
25
+ # Get current branch
26
+ branch = Ensure.not_none(
27
+ ctx.git.get_current_branch(ctx.cwd),
28
+ "Not on a branch (detached HEAD)",
29
+ )
30
+
31
+ # Get repo root for GitHub operations
32
+ repo_root = ctx.git.get_repository_root(ctx.cwd)
33
+
34
+ # Get PR for branch
35
+ pr = ctx.github.get_pr_for_branch(repo_root, branch)
36
+ if isinstance(pr, PRNotFound):
37
+ user_output(
38
+ click.style("Error: ", fg="red") + f"No pull request found for branch '{branch}'"
39
+ )
40
+ raise SystemExit(1)
41
+
42
+ pr_number = pr.number
43
+
44
+ user_output(f"Checking PR #{pr_number} for branch {branch}...")
45
+ user_output("")
46
+
47
+ # Track validation results
48
+ checks: list[tuple[bool, str]] = []
49
+
50
+ pr_body = pr.body
51
+
52
+ # .impl always lives at worktree/repo root
53
+ impl_dir = repo_root / ".impl"
54
+
55
+ # Check 0: Branch/issue.json agreement
56
+ # This catches cases where branch name says "P42-..." but issue.json says #99
57
+ issue_number: int | None = None
58
+ try:
59
+ issue_number = validate_issue_linkage(impl_dir, branch)
60
+ if issue_number is not None:
61
+ checks.append((True, f"Branch name and .impl/issue.json agree (#{issue_number})"))
62
+ except ValueError as e:
63
+ checks.append((False, str(e)))
64
+ # Continue with other checks - use the issue from .impl/issue.json as fallback
65
+ issue_ref_fallback = read_issue_reference(impl_dir)
66
+ if issue_ref_fallback is not None:
67
+ issue_number = issue_ref_fallback.issue_number
68
+
69
+ # Check 1: Issue closing reference (if issue number is discoverable)
70
+ issue_ref = read_issue_reference(impl_dir)
71
+
72
+ if issue_ref is not None:
73
+ expected_issue_number = issue_ref.issue_number
74
+ plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
75
+ if has_issue_closing_reference(pr_body, expected_issue_number, plans_repo):
76
+ # Format expected reference for display
77
+ if plans_repo is None:
78
+ ref_display = f"#{expected_issue_number}"
79
+ else:
80
+ ref_display = f"{plans_repo}#{expected_issue_number}"
81
+ msg = f"PR body contains issue closing reference (Closes {ref_display})"
82
+ checks.append((True, msg))
83
+ else:
84
+ if plans_repo is None:
85
+ expected = f"Closes #{expected_issue_number}"
86
+ else:
87
+ expected = f"Closes {plans_repo}#{expected_issue_number}"
88
+ msg = f"PR body missing issue closing reference (expected: {expected})"
89
+ checks.append((False, msg))
90
+
91
+ # Check 2: Checkout footer
92
+ if has_checkout_footer_for_pr(pr_body, pr_number):
93
+ checks.append((True, "PR body contains checkout footer"))
94
+ else:
95
+ checks.append((False, "PR body missing checkout footer"))
96
+
97
+ # Output results
98
+ for passed, description in checks:
99
+ status = click.style("[PASS]", fg="green") if passed else click.style("[FAIL]", fg="red")
100
+ user_output(f"{status} {description}")
101
+
102
+ user_output("")
103
+
104
+ # Determine overall result
105
+ failed_count = sum(1 for passed, _ in checks if not passed)
106
+ if failed_count == 0:
107
+ user_output(click.style("All checks passed", fg="green"))
108
+ raise SystemExit(0)
109
+ else:
110
+ check_word = "check" if failed_count == 1 else "checks"
111
+ user_output(click.style(f"{failed_count} {check_word} failed", fg="red"))
112
+ raise SystemExit(1)
@@ -0,0 +1,165 @@
1
+ """Checkout a pull request into a worktree.
2
+
3
+ This command fetches PR code and creates a worktree for local review/testing.
4
+ """
5
+
6
+ import click
7
+
8
+ from erk.cli.activation import render_activation_script
9
+ from erk.cli.alias import alias
10
+ from erk.cli.commands.pr.parse_pr_reference import parse_pr_reference
11
+ from erk.cli.core import worktree_path_for
12
+ from erk.cli.ensure import Ensure
13
+ from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
14
+ from erk.core.context import ErkContext
15
+ from erk.core.repo_discovery import NoRepoSentinel, RepoContext
16
+ from erk_shared.github.types import PRNotFound
17
+ from erk_shared.output.output import user_output
18
+
19
+
20
+ @alias("co")
21
+ @click.command("checkout", cls=CommandWithHiddenOptions)
22
+ @click.argument("pr_reference")
23
+ @script_option
24
+ @click.pass_obj
25
+ def pr_checkout(ctx: ErkContext, pr_reference: str, script: bool) -> None:
26
+ """Checkout PR into a worktree for review.
27
+
28
+ PR_REFERENCE can be a plain number (123) or GitHub URL
29
+ (https://github.com/owner/repo/pull/123).
30
+
31
+ Examples:
32
+
33
+ # Checkout by PR number
34
+ erk pr checkout 123
35
+
36
+ # Checkout by GitHub URL
37
+ erk pr checkout https://github.com/owner/repo/pull/123
38
+ """
39
+ # Validate preconditions upfront (LBYL)
40
+ Ensure.gh_authenticated(ctx)
41
+
42
+ if isinstance(ctx.repo, NoRepoSentinel):
43
+ ctx.feedback.error("Not in a git repository")
44
+ raise SystemExit(1)
45
+ repo: RepoContext = ctx.repo
46
+
47
+ pr_number = parse_pr_reference(pr_reference)
48
+
49
+ # Get PR details from GitHub
50
+ ctx.feedback.info(f"Fetching PR #{pr_number}...")
51
+ pr = ctx.github.get_pr(repo.root, pr_number)
52
+ if isinstance(pr, PRNotFound):
53
+ ctx.feedback.error(
54
+ f"Could not find PR #{pr_number}\n\n"
55
+ "Check the PR number and ensure you're authenticated with gh CLI."
56
+ )
57
+ raise SystemExit(1)
58
+
59
+ # Warn for closed/merged PRs
60
+ if pr.state != "OPEN":
61
+ ctx.feedback.info(f"Warning: PR #{pr_number} is {pr.state}")
62
+
63
+ # Determine branch name strategy
64
+ # For cross-repository PRs (forks), use pr/<number> to avoid conflicts
65
+ # For same-repository PRs, use the actual branch name
66
+ if pr.is_cross_repository:
67
+ branch_name = f"pr/{pr_number}"
68
+ else:
69
+ branch_name = pr.head_ref_name
70
+
71
+ # Check if branch already exists in a worktree
72
+ existing_worktree = ctx.git.find_worktree_for_branch(repo.root, branch_name)
73
+ if existing_worktree is not None:
74
+ # Branch already exists in a worktree - activate it
75
+ if script:
76
+ activation_script = render_activation_script(
77
+ worktree_path=existing_worktree,
78
+ target_subpath=None,
79
+ post_cd_commands=None,
80
+ final_message=f'echo "Went to existing worktree for PR #{pr_number}"',
81
+ comment="work activate-script",
82
+ )
83
+ result = ctx.script_writer.write_activation_script(
84
+ activation_script,
85
+ command_name="pr-checkout",
86
+ comment=f"activate PR #{pr_number}",
87
+ )
88
+ result.output_for_shell_integration()
89
+ else:
90
+ styled_path = click.style(str(existing_worktree), fg="cyan", bold=True)
91
+ user_output(f"PR #{pr_number} already checked out at {styled_path}")
92
+ user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
93
+ user_output(f"Or use: source <(erk pr checkout {pr_reference} --script)")
94
+ return
95
+
96
+ # For cross-repository PRs, always fetch via refs/pull/<n>/head
97
+ # For same-repo PRs, check if branch exists locally first
98
+ if pr.is_cross_repository:
99
+ # Fetch PR ref directly
100
+ ctx.git.fetch_pr_ref(repo.root, "origin", pr_number, branch_name)
101
+ else:
102
+ # Check if branch exists locally or on remote
103
+ local_branches = ctx.git.list_local_branches(repo.root)
104
+ if branch_name in local_branches:
105
+ # Branch already exists locally - just need to create worktree
106
+ pass
107
+ else:
108
+ # Check remote and fetch if needed
109
+ remote_branches = ctx.git.list_remote_branches(repo.root)
110
+ remote_ref = f"origin/{branch_name}"
111
+ if remote_ref in remote_branches:
112
+ ctx.git.fetch_branch(repo.root, "origin", branch_name)
113
+ ctx.git.create_tracking_branch(repo.root, branch_name, remote_ref)
114
+ else:
115
+ # Branch not on remote (maybe local-only PR?), fetch via PR ref
116
+ ctx.git.fetch_pr_ref(repo.root, "origin", pr_number, branch_name)
117
+
118
+ # Create worktree
119
+ worktree_path = worktree_path_for(repo.worktrees_dir, branch_name)
120
+ ctx.git.add_worktree(
121
+ repo.root,
122
+ worktree_path,
123
+ branch=branch_name,
124
+ ref=None,
125
+ create_branch=False,
126
+ )
127
+
128
+ # For stacked PRs (base is not trunk), rebase onto base branch
129
+ # This ensures git history includes the base branch as an ancestor,
130
+ # which `gt track` requires for proper stacking
131
+ trunk_branch = ctx.git.detect_trunk_branch(repo.root)
132
+ if pr.base_ref_name != trunk_branch and not pr.is_cross_repository:
133
+ ctx.feedback.info(f"Fetching base branch '{pr.base_ref_name}'...")
134
+ ctx.git.fetch_branch(repo.root, "origin", pr.base_ref_name)
135
+
136
+ ctx.feedback.info("Rebasing onto base branch...")
137
+ rebase_result = ctx.git.rebase_onto(worktree_path, f"origin/{pr.base_ref_name}")
138
+
139
+ if not rebase_result.success:
140
+ ctx.git.rebase_abort(worktree_path)
141
+ ctx.feedback.info(
142
+ f"Warning: Rebase had conflicts. Worktree created but needs manual rebase.\n"
143
+ f"Run: cd {worktree_path} && git rebase origin/{pr.base_ref_name}"
144
+ )
145
+
146
+ # Output based on mode
147
+ if script:
148
+ activation_script = render_activation_script(
149
+ worktree_path=worktree_path,
150
+ target_subpath=None,
151
+ post_cd_commands=None,
152
+ final_message=f'echo "Checked out PR #{pr_number} at $(pwd)"',
153
+ comment="work activate-script",
154
+ )
155
+ result = ctx.script_writer.write_activation_script(
156
+ activation_script,
157
+ command_name="pr-checkout",
158
+ comment=f"activate PR #{pr_number}",
159
+ )
160
+ result.output_for_shell_integration()
161
+ else:
162
+ styled_path = click.style(str(worktree_path), fg="cyan", bold=True)
163
+ user_output(f"Created worktree for PR #{pr_number} at {styled_path}")
164
+ user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
165
+ user_output(f"Or use: source <(erk pr checkout {pr_reference} --script)")