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,95 @@
1
+ """Extract arbitrary metadata fields from a plan issue's plan-header block.
2
+
3
+ Usage:
4
+ erk exec get-plan-metadata <issue-number> <field-name>
5
+
6
+ Output:
7
+ JSON with success status and field value (or null if field doesn't exist)
8
+
9
+ Exit Codes:
10
+ 0: Success (field found or null)
11
+ 1: Error (issue not found)
12
+ """
13
+
14
+ import json
15
+ from dataclasses import asdict, dataclass
16
+ from typing import Any
17
+
18
+ import click
19
+
20
+ from erk_shared.context.helpers import require_issues as require_github_issues
21
+ from erk_shared.context.helpers import require_repo_root
22
+ from erk_shared.github.metadata.core import find_metadata_block
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MetadataSuccess:
27
+ """Success response for metadata extraction."""
28
+
29
+ success: bool
30
+ value: Any
31
+ issue_number: int
32
+ field: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class MetadataError:
37
+ """Error response for metadata extraction."""
38
+
39
+ success: bool
40
+ error: str
41
+ message: str
42
+
43
+
44
+ @click.command(name="get-plan-metadata")
45
+ @click.argument("issue_number", type=int)
46
+ @click.argument("field_name")
47
+ @click.pass_context
48
+ def get_plan_metadata(
49
+ ctx: click.Context,
50
+ issue_number: int,
51
+ field_name: str,
52
+ ) -> None:
53
+ """Extract a metadata field from a plan issue's plan-header block.
54
+
55
+ Fetches the issue, extracts the plan-header block, and returns the
56
+ specified field value. Returns null if the field doesn't exist.
57
+ """
58
+ github_issues = require_github_issues(ctx)
59
+ repo_root = require_repo_root(ctx)
60
+
61
+ # Fetch current issue
62
+ try:
63
+ issue = github_issues.get_issue(repo_root, issue_number)
64
+ except RuntimeError as e:
65
+ result = MetadataError(
66
+ success=False,
67
+ error="issue_not_found",
68
+ message=f"Issue #{issue_number} not found: {e}",
69
+ )
70
+ click.echo(json.dumps(asdict(result)), err=True)
71
+ raise SystemExit(1) from None
72
+
73
+ # Extract plan-header block
74
+ block = find_metadata_block(issue.body, "plan-header")
75
+ if block is None:
76
+ # No plan-header block - return null for the field
77
+ result_success = MetadataSuccess(
78
+ success=True,
79
+ value=None,
80
+ issue_number=issue_number,
81
+ field=field_name,
82
+ )
83
+ click.echo(json.dumps(asdict(result_success)))
84
+ return
85
+
86
+ # Get field value (None if field doesn't exist)
87
+ field_value = block.data.get(field_name)
88
+
89
+ result_success = MetadataSuccess(
90
+ success=True,
91
+ value=field_value,
92
+ issue_number=issue_number,
93
+ field=field_name,
94
+ )
95
+ click.echo(json.dumps(asdict(result_success)))
@@ -0,0 +1,70 @@
1
+ """Generate PR body footer for remote implementation PRs.
2
+
3
+ This exec command generates a footer section for PR descriptions that includes
4
+ the `erk pr checkout` command. This is used by the GitHub Actions workflow when
5
+ creating PRs from remote implementations.
6
+
7
+ Usage:
8
+ erk exec get-pr-body-footer --pr-number 123
9
+ erk exec get-pr-body-footer --pr-number 123 --issue-number 456
10
+
11
+ Output:
12
+ Markdown footer with checkout command and optional issue closing reference
13
+
14
+ Exit Codes:
15
+ 0: Success
16
+ 1: Error (missing pr-number)
17
+
18
+ Examples:
19
+ $ erk exec get-pr-body-footer --pr-number 1895
20
+
21
+ ---
22
+
23
+ To checkout this PR in a fresh worktree and environment locally, run:
24
+
25
+ ```
26
+ erk pr checkout 1895 && erk pr sync --dangerous
27
+ ```
28
+
29
+ $ erk exec get-pr-body-footer --pr-number 1895 --issue-number 123
30
+
31
+ ---
32
+
33
+ Closes #123
34
+
35
+ To checkout this PR in a fresh worktree and environment locally, run:
36
+
37
+ ```
38
+ erk pr checkout 1895 && erk pr sync --dangerous
39
+ ```
40
+ """
41
+
42
+ import click
43
+
44
+ from erk_shared.github.pr_footer import build_pr_body_footer
45
+
46
+
47
+ @click.command(name="get-pr-body-footer")
48
+ @click.option("--pr-number", type=int, required=True, help="PR number for checkout command")
49
+ @click.option("--issue-number", type=int, required=False, help="Issue number to close")
50
+ @click.option(
51
+ "--plans-repo", type=str, required=False, help="Target repo in owner/repo format (cross-repo)"
52
+ )
53
+ def get_pr_body_footer(pr_number: int, issue_number: int | None, plans_repo: str | None) -> None:
54
+ """Generate PR body footer with checkout command.
55
+
56
+ Outputs a markdown footer section that includes the `erk pr checkout` command,
57
+ allowing users to easily checkout the PR in a fresh worktree locally.
58
+
59
+ When issue_number is provided, includes "Closes #N" (or "Closes owner/repo#N"
60
+ for cross-repo plans) to auto-close the issue when the PR is merged.
61
+
62
+ Args:
63
+ pr_number: The PR number to include in the checkout command
64
+ issue_number: Optional issue number to close when PR is merged
65
+ plans_repo: Optional target repo in "owner/repo" format for cross-repo
66
+ """
67
+ output = build_pr_body_footer(
68
+ pr_number=pr_number, issue_number=issue_number, plans_repo=plans_repo
69
+ )
70
+ click.echo(output, nl=False)
@@ -0,0 +1,149 @@
1
+ """Fetch PR discussion comments (main conversation thread) for agent context injection.
2
+
3
+ This exec command fetches discussion comments from the PR's main conversation
4
+ (not inline code review comments) and outputs them as JSON for agent processing.
5
+
6
+ Usage:
7
+ erk exec get-pr-discussion-comments
8
+ erk exec get-pr-discussion-comments --pr 123
9
+
10
+ Output:
11
+ JSON with success status, PR info, and discussion comments
12
+
13
+ Exit Codes:
14
+ 0: Success (or graceful error with JSON output)
15
+ 1: Context not initialized
16
+
17
+ Examples:
18
+ $ erk exec get-pr-discussion-comments
19
+ {"success": true, "pr_number": 123, "comments": [...]}
20
+
21
+ $ erk exec get-pr-discussion-comments --pr 456
22
+ {"success": true, "pr_number": 456, "comments": [...]}
23
+ """
24
+
25
+ import json
26
+ from typing import TypedDict
27
+
28
+ import click
29
+
30
+ from erk.cli.script_output import exit_with_error
31
+ from erk_shared.context.helpers import (
32
+ get_current_branch,
33
+ require_github,
34
+ require_repo_root,
35
+ )
36
+ from erk_shared.context.helpers import (
37
+ require_issues as require_github_issues,
38
+ )
39
+ from erk_shared.github.checks import GitHubChecks
40
+ from erk_shared.github.issues.types import IssueComment
41
+ from erk_shared.github.types import PRDetails
42
+ from erk_shared.non_ideal_state import (
43
+ BranchDetectionFailed,
44
+ GitHubAPIFailed,
45
+ NoPRForBranch,
46
+ PRNotFoundError,
47
+ )
48
+
49
+
50
+ def _ensure_branch(branch_result: str | BranchDetectionFailed) -> str:
51
+ """Ensure branch was detected, exit with error if not."""
52
+ if isinstance(branch_result, BranchDetectionFailed):
53
+ exit_with_error(branch_result.error_type, branch_result.message)
54
+ assert not isinstance(branch_result, BranchDetectionFailed) # Type narrowing after NoReturn
55
+ return branch_result
56
+
57
+
58
+ def _ensure_pr_result_for_branch(
59
+ pr_result: PRDetails | NoPRForBranch,
60
+ ) -> PRDetails:
61
+ """Ensure PR lookup by branch succeeded, exit with appropriate error if not."""
62
+ if isinstance(pr_result, NoPRForBranch):
63
+ exit_with_error(pr_result.error_type, pr_result.message)
64
+ assert not isinstance(pr_result, NoPRForBranch) # Type narrowing after NoReturn
65
+ return pr_result
66
+
67
+
68
+ def _ensure_pr_result_by_number(
69
+ pr_result: PRDetails | PRNotFoundError,
70
+ ) -> PRDetails:
71
+ """Ensure PR lookup by number succeeded, exit with appropriate error if not."""
72
+ if isinstance(pr_result, PRNotFoundError):
73
+ exit_with_error(pr_result.error_type, pr_result.message)
74
+ assert not isinstance(pr_result, PRNotFoundError) # Type narrowing after NoReturn
75
+ return pr_result
76
+
77
+
78
+ def _ensure_comments(
79
+ comments_result: list[IssueComment] | GitHubAPIFailed,
80
+ ) -> list[IssueComment]:
81
+ """Ensure comments fetch succeeded, exit with error if not."""
82
+ if isinstance(comments_result, GitHubAPIFailed):
83
+ exit_with_error(comments_result.error_type, comments_result.message)
84
+ assert not isinstance(comments_result, GitHubAPIFailed) # Type narrowing after NoReturn
85
+ return comments_result
86
+
87
+
88
+ class DiscussionCommentDict(TypedDict):
89
+ """Typed dict for a single discussion comment in JSON output."""
90
+
91
+ id: int
92
+ author: str
93
+ body: str
94
+ url: str
95
+
96
+
97
+ @click.command(name="get-pr-discussion-comments")
98
+ @click.option("--pr", type=int, default=None, help="PR number (defaults to current branch's PR)")
99
+ @click.pass_context
100
+ def get_pr_discussion_comments(ctx: click.Context, pr: int | None) -> None:
101
+ """Fetch PR discussion comments for agent context injection.
102
+
103
+ Queries GitHub for discussion comments on a pull request's main
104
+ conversation thread (not inline code review comments) and outputs
105
+ structured JSON for agent processing.
106
+
107
+ If --pr is not specified, finds the PR for the current branch.
108
+ """
109
+ # Get dependencies from context
110
+ repo_root = require_repo_root(ctx)
111
+ github = require_github(ctx)
112
+ github_issues = require_github_issues(ctx)
113
+
114
+ # Get PR details - either from current branch or specified PR number
115
+ if pr is None:
116
+ branch = _ensure_branch(GitHubChecks.branch(get_current_branch(ctx)))
117
+ pr_details = _ensure_pr_result_for_branch(
118
+ GitHubChecks.pr_for_branch(github, repo_root, branch)
119
+ )
120
+ else:
121
+ pr_details = _ensure_pr_result_by_number(GitHubChecks.pr_by_number(github, repo_root, pr))
122
+
123
+ # Fetch discussion comments (exits on failure)
124
+ comments = _ensure_comments(
125
+ GitHubChecks.issue_comments(github_issues, repo_root, pr_details.number)
126
+ )
127
+
128
+ # Format comments for JSON output
129
+ formatted_comments: list[DiscussionCommentDict] = []
130
+ for comment in comments:
131
+ assert isinstance(comment, IssueComment) # Runtime verification for type safety
132
+ formatted_comments.append(
133
+ {
134
+ "id": comment.id,
135
+ "author": comment.author,
136
+ "body": comment.body,
137
+ "url": comment.url,
138
+ }
139
+ )
140
+
141
+ result = {
142
+ "success": True,
143
+ "pr_number": pr_details.number,
144
+ "pr_url": pr_details.url,
145
+ "pr_title": pr_details.title,
146
+ "comments": formatted_comments,
147
+ }
148
+ click.echo(json.dumps(result, indent=2))
149
+ raise SystemExit(0)
@@ -0,0 +1,155 @@
1
+ """Fetch PR review comments for agent context injection.
2
+
3
+ This exec command fetches unresolved (or all) PR review comments from GitHub
4
+ and outputs them as JSON for agent processing.
5
+
6
+ Usage:
7
+ erk exec get-pr-review-comments
8
+ erk exec get-pr-review-comments --pr 123
9
+ erk exec get-pr-review-comments --include-resolved
10
+
11
+ Output:
12
+ JSON with success status, PR info, and review threads
13
+
14
+ Exit Codes:
15
+ 0: Success (or graceful error with JSON output)
16
+ 1: Context not initialized
17
+
18
+ Examples:
19
+ $ erk exec get-pr-review-comments
20
+ {"success": true, "pr_number": 123, "threads": [...]}
21
+
22
+ $ erk exec get-pr-review-comments --pr 456
23
+ {"success": true, "pr_number": 456, "threads": [...]}
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from dataclasses import asdict, dataclass
30
+ from typing import TypedDict
31
+
32
+ import click
33
+
34
+ from erk.cli.script_output import exit_with_error
35
+ from erk_shared.context.helpers import get_current_branch, require_github, require_repo_root
36
+ from erk_shared.github.types import PRDetails, PRNotFound, PRReviewThread
37
+
38
+
39
+ class ReviewCommentDict(TypedDict):
40
+ """Typed dict for a single review comment in JSON output."""
41
+
42
+ author: str
43
+ body: str
44
+ created_at: str
45
+
46
+
47
+ class ReviewThreadDict(TypedDict):
48
+ """Typed dict for a review thread in JSON output."""
49
+
50
+ id: str
51
+ path: str
52
+ line: int | None
53
+ is_outdated: bool
54
+ comments: list[ReviewCommentDict]
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ReviewCommentSuccess:
59
+ """Success response for PR review comments."""
60
+
61
+ success: bool
62
+ pr_number: int
63
+ pr_url: str
64
+ pr_title: str
65
+ threads: list[ReviewThreadDict]
66
+
67
+
68
+ def _ensure_branch(branch: str | None) -> str:
69
+ """Ensure branch was detected, exit with error if not."""
70
+ if branch is None:
71
+ exit_with_error("branch-detection-failed", "Could not determine current branch")
72
+ assert branch is not None # Type narrowing after NoReturn
73
+ return branch
74
+
75
+
76
+ def _ensure_pr_result(
77
+ pr_result: PRDetails | PRNotFound,
78
+ *,
79
+ branch: str | None,
80
+ pr_number: int | None,
81
+ ) -> PRDetails:
82
+ """Ensure PR lookup succeeded, exit with appropriate error if not."""
83
+ if isinstance(pr_result, PRNotFound):
84
+ if branch is not None:
85
+ exit_with_error("no-pr-for-branch", f"No PR found for branch '{branch}'")
86
+ else:
87
+ exit_with_error("pr-not-found", f"PR #{pr_number} not found")
88
+ assert not isinstance(pr_result, PRNotFound) # Type narrowing after NoReturn
89
+ return pr_result
90
+
91
+
92
+ def _format_thread_for_json(thread: PRReviewThread) -> ReviewThreadDict:
93
+ """Format a PRReviewThread for JSON output."""
94
+ comments: list[ReviewCommentDict] = []
95
+ for comment in thread.comments:
96
+ comments.append(
97
+ {
98
+ "author": comment.author,
99
+ "body": comment.body,
100
+ "created_at": comment.created_at,
101
+ }
102
+ )
103
+
104
+ return {
105
+ "id": thread.id,
106
+ "path": thread.path,
107
+ "line": thread.line,
108
+ "is_outdated": thread.is_outdated,
109
+ "comments": comments,
110
+ }
111
+
112
+
113
+ @click.command(name="get-pr-review-comments")
114
+ @click.option("--pr", type=int, default=None, help="PR number (defaults to current branch's PR)")
115
+ @click.option("--include-resolved", is_flag=True, help="Include resolved threads")
116
+ @click.pass_context
117
+ def get_pr_review_comments(ctx: click.Context, pr: int | None, include_resolved: bool) -> None:
118
+ """Fetch PR review comments for agent context injection.
119
+
120
+ Queries GitHub for review threads on a pull request and outputs
121
+ structured JSON for agent processing. By default, excludes resolved
122
+ threads.
123
+
124
+ If --pr is not specified, finds the PR for the current branch.
125
+ """
126
+ # Get dependencies from context
127
+ repo_root = require_repo_root(ctx)
128
+ github = require_github(ctx)
129
+
130
+ # Get PR details - either from current branch or specified PR number
131
+ if pr is None:
132
+ branch = _ensure_branch(get_current_branch(ctx))
133
+ pr_result = _ensure_pr_result(
134
+ github.get_pr_for_branch(repo_root, branch), branch=branch, pr_number=None
135
+ )
136
+ else:
137
+ pr_result = _ensure_pr_result(github.get_pr(repo_root, pr), branch=None, pr_number=pr)
138
+
139
+ # Fetch review threads
140
+ try:
141
+ threads = github.get_pr_review_threads(
142
+ repo_root, pr_result.number, include_resolved=include_resolved
143
+ )
144
+ except RuntimeError as e:
145
+ exit_with_error("github-api-failed", str(e))
146
+
147
+ result_success = ReviewCommentSuccess(
148
+ success=True,
149
+ pr_number=pr_result.number,
150
+ pr_url=pr_result.url,
151
+ pr_title=pr_result.title,
152
+ threads=[_format_thread_for_json(t) for t in threads],
153
+ )
154
+ click.echo(json.dumps(asdict(result_success), indent=2))
155
+ raise SystemExit(0)
@@ -0,0 +1,158 @@
1
+ """Initialize implementation by validating .impl/ folder.
2
+
3
+ This exec command validates .impl/ folder for /erk:plan-implement:
4
+ - Validates .impl/ folder structure (plan.md exists)
5
+ - Checks for GitHub issue tracking (issue.json)
6
+ - Parses "Related Documentation" section for skills and docs
7
+
8
+ Usage:
9
+ erk exec impl-init --json
10
+
11
+ Output:
12
+ JSON with validation status and related docs
13
+ Always outputs JSON (for machine parsing by slash command)
14
+
15
+ Exit Codes:
16
+ 0: Success
17
+ 1: Validation error
18
+
19
+ Examples:
20
+ $ erk exec impl-init --json
21
+ {"valid": true, "impl_type": "impl", "has_issue_tracking": true, ...}
22
+ """
23
+
24
+ import json
25
+ import re
26
+ from pathlib import Path
27
+ from typing import NoReturn
28
+
29
+ import click
30
+
31
+ from erk_shared.impl_folder import read_issue_reference
32
+
33
+
34
+ def _error_json(error_type: str, message: str) -> NoReturn:
35
+ """Output error as JSON and exit with code 1."""
36
+ result = {"valid": False, "error_type": error_type, "message": message}
37
+ click.echo(json.dumps(result))
38
+ raise SystemExit(1)
39
+
40
+
41
+ def _validate_impl_folder() -> tuple[Path, str]:
42
+ """Validate .impl/ or .worker-impl/ folder exists and has required files.
43
+
44
+ Returns:
45
+ Tuple of (impl_dir Path, impl_type string)
46
+
47
+ Raises:
48
+ SystemExit: If validation fails
49
+ """
50
+ cwd = Path.cwd()
51
+
52
+ # Check .impl/ first, then .worker-impl/
53
+ impl_dir = cwd / ".impl"
54
+ impl_type = "impl"
55
+
56
+ if not impl_dir.exists():
57
+ impl_dir = cwd / ".worker-impl"
58
+ impl_type = "worker-impl"
59
+
60
+ if not impl_dir.exists():
61
+ _error_json(
62
+ "no_impl_folder",
63
+ "No .impl/ or .worker-impl/ folder found in current directory",
64
+ )
65
+
66
+ plan_file = impl_dir / "plan.md"
67
+ if not plan_file.exists():
68
+ _error_json("no_plan_file", f"No plan.md found in {impl_dir.name}/ folder")
69
+
70
+ return impl_dir, impl_type
71
+
72
+
73
+ def _extract_related_docs(plan_content: str) -> dict[str, list[str]]:
74
+ """Extract Related Documentation section from plan content.
75
+
76
+ Parses markdown like:
77
+ ## Related Documentation
78
+
79
+ **Skills:**
80
+ - `dignified-python-313`
81
+
82
+ **Docs:**
83
+ - [Kit CLI Testing](docs/agent/testing/kit-cli-testing.md)
84
+
85
+ Args:
86
+ plan_content: Full plan markdown content
87
+
88
+ Returns:
89
+ Dict with 'skills' and 'docs' lists
90
+ """
91
+ result: dict[str, list[str]] = {"skills": [], "docs": []}
92
+
93
+ # Find Related Documentation section
94
+ related_docs_pattern = re.compile(
95
+ r"##\s+Related Documentation\s*\n(.*?)(?=\n##|\Z)",
96
+ re.DOTALL | re.IGNORECASE,
97
+ )
98
+ match = related_docs_pattern.search(plan_content)
99
+
100
+ if match is None:
101
+ return result
102
+
103
+ section = match.group(1)
104
+
105
+ # Extract skills (backtick-enclosed names after bullet points)
106
+ skills_section = re.search(r"\*\*Skills:\*\*\s*\n(.*?)(?=\*\*|\Z)", section, re.DOTALL)
107
+ if skills_section:
108
+ skill_pattern = re.compile(r"-\s*`([^`]+)`")
109
+ result["skills"] = skill_pattern.findall(skills_section.group(1))
110
+
111
+ # Extract docs (markdown links or plain paths)
112
+ docs_section = re.search(r"\*\*Docs:\*\*\s*\n(.*?)(?=\*\*|\Z)", section, re.DOTALL)
113
+ if docs_section:
114
+ # Match markdown links [text](path) or backtick paths `path`
115
+ link_pattern = re.compile(r"-\s*(?:\[[^\]]*\]\(([^)]+)\)|`([^`]+)`)")
116
+ for m in link_pattern.finditer(docs_section.group(1)):
117
+ doc_path = m.group(1) or m.group(2)
118
+ if doc_path:
119
+ result["docs"].append(doc_path)
120
+
121
+ return result
122
+
123
+
124
+ @click.command(name="impl-init")
125
+ @click.option("--json", "json_output", is_flag=True, default=True, help="Output JSON (default)")
126
+ def impl_init(json_output: bool) -> None:
127
+ """Initialize implementation by validating .impl/ folder.
128
+
129
+ Validates .impl/ folder for /erk:plan-implement.
130
+ Returns structured JSON with validation status and related documentation.
131
+ """
132
+ # Validate folder structure
133
+ impl_dir, impl_type = _validate_impl_folder()
134
+
135
+ # Get issue reference info
136
+ issue_ref = read_issue_reference(impl_dir)
137
+ has_issue_tracking = issue_ref is not None
138
+ issue_number = issue_ref.issue_number if issue_ref else None
139
+
140
+ # Read plan content
141
+ plan_file = impl_dir / "plan.md"
142
+ plan_content = plan_file.read_text(encoding="utf-8")
143
+
144
+ # Extract related documentation
145
+ related_docs = _extract_related_docs(plan_content)
146
+
147
+ # Build result
148
+ result: dict = {
149
+ "valid": True,
150
+ "impl_type": impl_type,
151
+ "has_issue_tracking": has_issue_tracking,
152
+ "related_docs": related_docs,
153
+ }
154
+
155
+ if issue_number is not None:
156
+ result["issue_number"] = issue_number
157
+
158
+ click.echo(json.dumps(result))