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,237 @@
1
+ """Post extraction workflow status comments to GitHub issues.
2
+
3
+ Usage:
4
+ erk exec post-extraction-comment \
5
+ --issue-number 123 \
6
+ --status started \
7
+ --workflow-run-url "https://..."
8
+
9
+ Status options:
10
+ - started: Extraction workflow has begun
11
+ - failed: Extraction failed with error
12
+ - complete: Extraction succeeded with PR
13
+ - no-changes: No documentation changes needed
14
+
15
+ Output:
16
+ JSON with success status and comment_url
17
+ """
18
+
19
+ import json
20
+ from datetime import UTC, datetime
21
+
22
+ import click
23
+
24
+ from erk_shared.context.helpers import require_issues as require_github_issues
25
+ from erk_shared.context.helpers import require_repo_root
26
+
27
+
28
+ def _format_started_comment(workflow_run_url: str | None) -> str:
29
+ """Format the started status comment."""
30
+ started_at = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+ lines = [
33
+ "⚙️ **Documentation extraction started**",
34
+ "",
35
+ "<details>",
36
+ "<summary>📋 Metadata</summary>",
37
+ "",
38
+ "```yaml",
39
+ "status: started",
40
+ f"started_at: {started_at}",
41
+ ]
42
+
43
+ if workflow_run_url:
44
+ lines.append(f"workflow_run_url: {workflow_run_url}")
45
+
46
+ lines.extend(
47
+ [
48
+ "```",
49
+ "",
50
+ "</details>",
51
+ "",
52
+ "---",
53
+ "",
54
+ "Extracting session data from issue comments...",
55
+ ]
56
+ )
57
+
58
+ if workflow_run_url:
59
+ lines.extend(["", f"[View workflow run]({workflow_run_url})"])
60
+
61
+ return "\n".join(lines)
62
+
63
+
64
+ def _format_failed_comment(
65
+ workflow_run_url: str | None,
66
+ error_message: str | None,
67
+ ) -> str:
68
+ """Format the failed status comment."""
69
+ lines = ["❌ **Documentation extraction failed**", ""]
70
+
71
+ if error_message:
72
+ lines.extend([f"**Error:** {error_message}", ""])
73
+
74
+ lines.extend(
75
+ [
76
+ "No session content was found in the issue comments. This may happen if:",
77
+ "- The raw extraction issue was created without session XML",
78
+ "- The session content blocks are malformed",
79
+ ]
80
+ )
81
+
82
+ if workflow_run_url:
83
+ lines.extend(["", f"[View workflow run]({workflow_run_url})"])
84
+
85
+ lines.extend(
86
+ [
87
+ "",
88
+ "To retry, run the extraction manually:",
89
+ "```",
90
+ "/erk:create-extraction-plan",
91
+ "```",
92
+ ]
93
+ )
94
+
95
+ return "\n".join(lines)
96
+
97
+
98
+ def _format_complete_comment(pr_url: str | None) -> str:
99
+ """Format the complete status comment."""
100
+ completed_at = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
101
+
102
+ lines = [
103
+ "✅ **Documentation extraction complete**",
104
+ "",
105
+ ]
106
+
107
+ if pr_url:
108
+ lines.extend([f"**PR:** {pr_url}", ""])
109
+
110
+ lines.extend(
111
+ [
112
+ "<details>",
113
+ "<summary>📋 Metadata</summary>",
114
+ "",
115
+ "```yaml",
116
+ "status: complete",
117
+ f"completed_at: {completed_at}",
118
+ ]
119
+ )
120
+
121
+ if pr_url:
122
+ lines.append(f"pr_url: {pr_url}")
123
+
124
+ lines.extend(
125
+ [
126
+ "```",
127
+ "",
128
+ "</details>",
129
+ "",
130
+ "---",
131
+ "",
132
+ "The extraction has created documentation improvements. Please review the PR.",
133
+ ]
134
+ )
135
+
136
+ return "\n".join(lines)
137
+
138
+
139
+ def _format_no_changes_comment() -> str:
140
+ """Format the no-changes status comment."""
141
+ return "\n".join(
142
+ [
143
+ "ℹ️ **No documentation changes needed**",
144
+ "",
145
+ "The extraction analysis did not produce any documentation changes.",
146
+ "This may happen if the session did not contain extractable patterns.",
147
+ ]
148
+ )
149
+
150
+
151
+ @click.command(name="post-extraction-comment")
152
+ @click.option(
153
+ "--issue-number",
154
+ type=int,
155
+ required=True,
156
+ help="GitHub issue number",
157
+ )
158
+ @click.option(
159
+ "--status",
160
+ type=click.Choice(["started", "failed", "complete", "no-changes"]),
161
+ required=True,
162
+ help="Extraction status",
163
+ )
164
+ @click.option(
165
+ "--workflow-run-url",
166
+ type=str,
167
+ default=None,
168
+ help="URL to the workflow run (for started/failed)",
169
+ )
170
+ @click.option(
171
+ "--error-message",
172
+ type=str,
173
+ default=None,
174
+ help="Error message (for failed status)",
175
+ )
176
+ @click.option(
177
+ "--pr-url",
178
+ type=str,
179
+ default=None,
180
+ help="PR URL (for complete status)",
181
+ )
182
+ @click.pass_context
183
+ def post_extraction_comment(
184
+ ctx: click.Context,
185
+ issue_number: int,
186
+ status: str,
187
+ workflow_run_url: str | None,
188
+ error_message: str | None,
189
+ pr_url: str | None,
190
+ ) -> None:
191
+ """Post extraction workflow status comment to a GitHub issue.
192
+
193
+ Posts a formatted status comment based on the extraction workflow stage.
194
+ """
195
+ github = require_github_issues(ctx)
196
+ repo_root = require_repo_root(ctx)
197
+
198
+ # Format the comment based on status
199
+ if status == "started":
200
+ comment_body = _format_started_comment(workflow_run_url)
201
+ elif status == "failed":
202
+ comment_body = _format_failed_comment(workflow_run_url, error_message)
203
+ elif status == "complete":
204
+ comment_body = _format_complete_comment(pr_url)
205
+ elif status == "no-changes":
206
+ comment_body = _format_no_changes_comment()
207
+ else:
208
+ click.echo(json.dumps({"success": False, "error": f"Unknown status: {status}"}))
209
+ raise SystemExit(1)
210
+
211
+ # Post the comment
212
+ try:
213
+ github.add_comment(repo_root, issue_number, comment_body)
214
+ except RuntimeError as e:
215
+ click.echo(
216
+ json.dumps(
217
+ {
218
+ "success": False,
219
+ "error": f"Failed to post comment: {e}",
220
+ }
221
+ )
222
+ )
223
+ raise SystemExit(1) from e
224
+
225
+ click.echo(
226
+ json.dumps(
227
+ {
228
+ "success": True,
229
+ "issue_number": issue_number,
230
+ "status": status,
231
+ }
232
+ )
233
+ )
234
+
235
+
236
+ if __name__ == "__main__":
237
+ post_extraction_comment()
@@ -0,0 +1,133 @@
1
+ """Post or update a PR summary comment with a unique marker.
2
+
3
+ This exec command finds an existing PR comment containing a marker,
4
+ updates it if found, or creates a new comment if not found.
5
+
6
+ Usage:
7
+ erk exec post-or-update-pr-summary --pr-number 123 \\
8
+ --marker "<!-- my-marker -->" --body "Summary text"
9
+
10
+ Output:
11
+ JSON with success status, action taken (created/updated), and comment ID
12
+
13
+ Exit Codes:
14
+ 0: Always (even on error, to support || true pattern)
15
+
16
+ Examples:
17
+ $ erk exec post-or-update-pr-summary --pr-number 123 \\
18
+ --marker "<!-- review -->" --body "# Review"
19
+ {"success": true, "action": "created", "comment_id": 12345}
20
+
21
+ $ erk exec post-or-update-pr-summary --pr-number 123 \\
22
+ --marker "<!-- review -->" --body "# Updated"
23
+ {"success": true, "action": "updated", "comment_id": 12345}
24
+ """
25
+
26
+ import json
27
+ from dataclasses import asdict, dataclass
28
+
29
+ import click
30
+
31
+ from erk_shared.context.helpers import require_github, require_repo_root
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class SummaryCommentSuccess:
36
+ """Success response for summary comment posting."""
37
+
38
+ success: bool
39
+ action: str # "created" or "updated"
40
+ comment_id: int
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class SummaryCommentError:
45
+ """Error response for summary comment posting."""
46
+
47
+ success: bool
48
+ error_type: str
49
+ message: str
50
+
51
+
52
+ @click.command(name="post-or-update-pr-summary")
53
+ @click.option("--pr-number", required=True, type=int, help="PR number to comment on")
54
+ @click.option("--marker", required=True, help="HTML marker to identify the comment")
55
+ @click.option("--body", required=True, help="Comment body text (must include marker)")
56
+ @click.pass_context
57
+ def post_or_update_pr_summary(
58
+ ctx: click.Context,
59
+ pr_number: int,
60
+ marker: str,
61
+ body: str,
62
+ ) -> None:
63
+ """Post or update a PR summary comment.
64
+
65
+ Finds an existing comment containing the marker and updates it,
66
+ or creates a new comment if none found. The body should include
67
+ the marker for future lookups.
68
+
69
+ PR_NUMBER: The PR to comment on
70
+ MARKER: HTML marker to identify the comment (e.g., <!-- my-review -->)
71
+ BODY: Comment text (should include the marker for future updates)
72
+ """
73
+ repo_root = require_repo_root(ctx)
74
+ github = require_github(ctx)
75
+
76
+ # Ensure body contains the marker for future lookups
77
+ if marker not in body:
78
+ result = SummaryCommentError(
79
+ success=False,
80
+ error_type="marker_not_in_body",
81
+ message=f"The body must contain the marker '{marker}' for future lookups",
82
+ )
83
+ click.echo(json.dumps(asdict(result), indent=2))
84
+ raise SystemExit(0)
85
+
86
+ # Try to find existing comment with marker
87
+ try:
88
+ existing_id = github.find_pr_comment_by_marker(repo_root, pr_number, marker)
89
+ except RuntimeError as e:
90
+ result = SummaryCommentError(
91
+ success=False,
92
+ error_type="find_failed",
93
+ message=f"Failed to search for existing comment: {e}",
94
+ )
95
+ click.echo(json.dumps(asdict(result), indent=2))
96
+ raise SystemExit(0) from None
97
+
98
+ if existing_id is not None:
99
+ # Update existing comment
100
+ try:
101
+ github.update_pr_comment(repo_root, existing_id, body)
102
+ result_success = SummaryCommentSuccess(
103
+ success=True,
104
+ action="updated",
105
+ comment_id=existing_id,
106
+ )
107
+ click.echo(json.dumps(asdict(result_success), indent=2))
108
+ except RuntimeError as e:
109
+ result = SummaryCommentError(
110
+ success=False,
111
+ error_type="update_failed",
112
+ message=str(e),
113
+ )
114
+ click.echo(json.dumps(asdict(result), indent=2))
115
+ else:
116
+ # Create new comment
117
+ try:
118
+ comment_id = github.create_pr_comment(repo_root, pr_number, body)
119
+ result_success = SummaryCommentSuccess(
120
+ success=True,
121
+ action="created",
122
+ comment_id=comment_id,
123
+ )
124
+ click.echo(json.dumps(asdict(result_success), indent=2))
125
+ except RuntimeError as e:
126
+ result = SummaryCommentError(
127
+ success=False,
128
+ error_type="create_failed",
129
+ message=str(e),
130
+ )
131
+ click.echo(json.dumps(asdict(result), indent=2))
132
+
133
+ raise SystemExit(0)
@@ -0,0 +1,143 @@
1
+ """Post an inline review comment on a specific line of a PR.
2
+
3
+ This exec command creates a pull request review comment attached to a
4
+ specific line of a file in the PR diff.
5
+
6
+ Usage:
7
+ erk exec post-pr-inline-comment --pr-number 123 \\
8
+ --path "src/foo.py" --line 42 --body "Comment text"
9
+
10
+ Output:
11
+ JSON with success status and comment ID
12
+
13
+ Exit Codes:
14
+ 0: Always (even on error, to support || true pattern)
15
+
16
+ Examples:
17
+ $ erk exec post-pr-inline-comment --pr-number 123 \\
18
+ --path "src/foo.py" --line 42 --body "Use LBYL"
19
+ {"success": true, "comment_id": 12345}
20
+
21
+ $ erk exec post-pr-inline-comment --pr-number 123 \\
22
+ --path "bad.py" --line 999 --body "Comment"
23
+ {"success": false, "error_type": "github_api_failed", "message": "..."}
24
+ """
25
+
26
+ import json
27
+ from dataclasses import asdict, dataclass
28
+ from pathlib import Path
29
+
30
+ import click
31
+
32
+ from erk_shared.context.helpers import require_github, require_repo_root
33
+ from erk_shared.github.parsing import execute_gh_command
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class InlineCommentSuccess:
38
+ """Success response for inline comment posting."""
39
+
40
+ success: bool
41
+ comment_id: int
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class InlineCommentError:
46
+ """Error response for inline comment posting."""
47
+
48
+ success: bool
49
+ error_type: str
50
+ message: str
51
+
52
+
53
+ def _get_pr_head_sha(repo_root: Path, pr_number: int) -> str:
54
+ """Get the head commit SHA for a PR.
55
+
56
+ Uses gh CLI REST API to fetch the PR head ref SHA.
57
+ Uses REST API instead of GraphQL (`gh pr view`) to avoid hitting
58
+ GraphQL rate limits. GraphQL and REST have separate quotas.
59
+
60
+ Args:
61
+ repo_root: Repository root directory
62
+ pr_number: PR number to query
63
+
64
+ Returns:
65
+ The head commit SHA as a string
66
+
67
+ Raises:
68
+ RuntimeError: If gh command fails
69
+ """
70
+ # GH-API-AUDIT: REST - GET pulls/{number}
71
+ cmd = [
72
+ "gh",
73
+ "api",
74
+ f"repos/{{owner}}/{{repo}}/pulls/{pr_number}",
75
+ "--jq",
76
+ ".head.sha",
77
+ ]
78
+ stdout = execute_gh_command(cmd, repo_root)
79
+ return stdout.strip()
80
+
81
+
82
+ @click.command(name="post-pr-inline-comment")
83
+ @click.option("--pr-number", required=True, type=int, help="PR number to comment on")
84
+ @click.option("--path", required=True, help="File path relative to repo root")
85
+ @click.option("--line", required=True, type=int, help="Line number in the diff")
86
+ @click.option("--body", required=True, help="Comment body text")
87
+ @click.pass_context
88
+ def post_pr_inline_comment(
89
+ ctx: click.Context,
90
+ pr_number: int,
91
+ path: str,
92
+ line: int,
93
+ body: str,
94
+ ) -> None:
95
+ """Post an inline review comment on a PR.
96
+
97
+ Creates a pull request review comment attached to a specific line
98
+ of a file in the PR diff. Automatically fetches the PR head commit SHA.
99
+
100
+ PR_NUMBER: The PR to comment on
101
+ PATH: File path relative to repository root
102
+ LINE: Line number in the diff to attach comment to
103
+ BODY: Comment text (markdown supported)
104
+ """
105
+ repo_root = require_repo_root(ctx)
106
+ github = require_github(ctx)
107
+
108
+ # Get the PR head commit SHA
109
+ try:
110
+ commit_sha = _get_pr_head_sha(repo_root, pr_number)
111
+ except RuntimeError as e:
112
+ result = InlineCommentError(
113
+ success=False,
114
+ error_type="pr-not-found",
115
+ message=f"Could not get PR head commit: {e}",
116
+ )
117
+ click.echo(json.dumps(asdict(result), indent=2))
118
+ raise SystemExit(0) from None
119
+
120
+ # Create the inline comment
121
+ try:
122
+ comment_id = github.create_pr_review_comment(
123
+ repo_root,
124
+ pr_number,
125
+ body,
126
+ commit_sha,
127
+ path,
128
+ line,
129
+ )
130
+ result_success = InlineCommentSuccess(
131
+ success=True,
132
+ comment_id=comment_id,
133
+ )
134
+ click.echo(json.dumps(asdict(result_success), indent=2))
135
+ except RuntimeError as e:
136
+ result = InlineCommentError(
137
+ success=False,
138
+ error_type="github-api-failed",
139
+ message=str(e),
140
+ )
141
+ click.echo(json.dumps(asdict(result), indent=2))
142
+
143
+ raise SystemExit(0)
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Post a "workflow started" comment to a GitHub issue with YAML metadata block.
3
+
4
+ This command posts a structured comment to a GitHub issue indicating that a
5
+ GitHub Actions workflow has started. The comment includes a YAML metadata block
6
+ that can be parsed programmatically.
7
+
8
+ This replaces ~40 lines of bash heredoc template assembly in GitHub Actions workflows.
9
+
10
+ Usage:
11
+ erk exec post-workflow-started-comment \\
12
+ --issue-number 123 \\
13
+ --branch-name my-feature-branch \\
14
+ --pr-number 456 \\
15
+ --run-id 12345678 \\
16
+ --run-url https://github.com/owner/repo/actions/runs/12345678 \\
17
+ --repository owner/repo
18
+
19
+ Output:
20
+ JSON object with success status
21
+
22
+ Exit Codes:
23
+ 0: Success (comment posted)
24
+ 1: Error (GitHub API failed)
25
+
26
+ Examples:
27
+ $ erk exec post-workflow-started-comment \\
28
+ --issue-number 123 \\
29
+ --branch-name feat-auth \\
30
+ --pr-number 456 \\
31
+ --run-id 99999 \\
32
+ --run-url https://github.com/acme/app/actions/runs/99999 \\
33
+ --repository acme/app
34
+ {
35
+ "success": true,
36
+ "issue_number": 123
37
+ }
38
+ """
39
+
40
+ import json
41
+ from dataclasses import asdict, dataclass
42
+ from datetime import UTC, datetime
43
+
44
+ import click
45
+
46
+ from erk_shared.context.helpers import require_issues as require_github_issues
47
+ from erk_shared.context.helpers import require_repo_root
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class PostSuccess:
52
+ """Success result when comment is posted."""
53
+
54
+ success: bool
55
+ issue_number: int
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class PostError:
60
+ """Error result when comment posting fails."""
61
+
62
+ success: bool
63
+ error: str
64
+ message: str
65
+
66
+
67
+ def _build_workflow_started_comment(
68
+ issue_number: int,
69
+ branch_name: str,
70
+ pr_number: int,
71
+ run_id: str,
72
+ run_url: str,
73
+ repository: str,
74
+ ) -> str:
75
+ """Build the workflow started comment body.
76
+
77
+ Args:
78
+ issue_number: GitHub issue number
79
+ branch_name: Git branch name
80
+ pr_number: Pull request number
81
+ run_id: GitHub Actions workflow run ID
82
+ run_url: Full URL to the workflow run
83
+ repository: Repository in owner/repo format
84
+
85
+ Returns:
86
+ Formatted markdown comment body
87
+ """
88
+ started_at = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
89
+
90
+ return f"""⚙️ GitHub Action Started
91
+
92
+ <details>
93
+ <summary>📋 Metadata</summary>
94
+
95
+ <!-- erk:metadata-block:workflow-started -->
96
+ ```yaml
97
+ schema: workflow-started
98
+ status: started
99
+ started_at: {started_at}
100
+ workflow_run_id: "{run_id}"
101
+ workflow_run_url: {run_url}
102
+ branch_name: {branch_name}
103
+ issue_number: {issue_number}
104
+ ```
105
+ <!-- /erk:metadata-block:workflow-started -->
106
+
107
+ </details>
108
+
109
+ ---
110
+
111
+ Setup completed successfully.
112
+
113
+ **Branch:** `{branch_name}`
114
+ **PR:** [#{pr_number}](https://github.com/{repository}/pull/{pr_number})
115
+ **Status:** Ready for implementation
116
+
117
+ [View workflow run]({run_url})
118
+ """
119
+
120
+
121
+ @click.command(name="post-workflow-started-comment")
122
+ @click.option("--issue-number", type=int, required=True, help="GitHub issue number")
123
+ @click.option("--branch-name", type=str, required=True, help="Git branch name")
124
+ @click.option("--pr-number", type=int, required=True, help="Pull request number")
125
+ @click.option("--run-id", type=str, required=True, help="GitHub Actions workflow run ID")
126
+ @click.option("--run-url", type=str, required=True, help="Full URL to workflow run")
127
+ @click.option("--repository", type=str, required=True, help="Repository in owner/repo format")
128
+ @click.pass_context
129
+ def post_workflow_started_comment(
130
+ ctx: click.Context,
131
+ issue_number: int,
132
+ branch_name: str,
133
+ pr_number: int,
134
+ run_id: str,
135
+ run_url: str,
136
+ repository: str,
137
+ ) -> None:
138
+ """Post a workflow started comment to a GitHub issue.
139
+
140
+ Posts a structured comment with YAML metadata block indicating that a
141
+ GitHub Actions workflow has started processing the issue.
142
+ """
143
+ github = require_github_issues(ctx)
144
+ repo_root = require_repo_root(ctx)
145
+
146
+ # Build comment body
147
+ comment_body = _build_workflow_started_comment(
148
+ issue_number=issue_number,
149
+ branch_name=branch_name,
150
+ pr_number=pr_number,
151
+ run_id=run_id,
152
+ run_url=run_url,
153
+ repository=repository,
154
+ )
155
+
156
+ # Post comment
157
+ try:
158
+ github.add_comment(repo_root, issue_number, comment_body)
159
+ result = PostSuccess(success=True, issue_number=issue_number)
160
+ click.echo(json.dumps(asdict(result), indent=2))
161
+ except RuntimeError as e:
162
+ result = PostError(
163
+ success=False,
164
+ error="github-api-failed",
165
+ message=str(e),
166
+ )
167
+ click.echo(json.dumps(asdict(result), indent=2))
168
+ raise SystemExit(1) from e