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,251 @@
1
+ name: erk-impl
2
+ run-name: "${{ inputs.issue_number }}:${{ inputs.distinct_id }}"
3
+
4
+ on:
5
+ workflow_dispatch:
6
+ inputs:
7
+ issue_number:
8
+ description: "GitHub issue number to implement"
9
+ required: true
10
+ type: string
11
+ submitted_by:
12
+ description: "GitHub username of the person who submitted the issue"
13
+ required: true
14
+ type: string
15
+ distinct_id:
16
+ description: "Unique identifier for run discovery (base36)"
17
+ required: true
18
+ type: string
19
+ issue_title:
20
+ description: "Issue title for workflow run display"
21
+ required: true
22
+ type: string
23
+ branch_name:
24
+ description: "Branch name for implementation (created by submit command)"
25
+ required: true
26
+ type: string
27
+ pr_number:
28
+ description: "PR number for implementation (created by submit command)"
29
+ required: true
30
+ type: string
31
+ model_name:
32
+ description: "Claude model to use for implementation"
33
+ required: false
34
+ type: string
35
+ default: "claude-sonnet-4-5"
36
+
37
+ concurrency:
38
+ group: implement-issue-${{ github.event.inputs.issue_number }}
39
+ cancel-in-progress: true
40
+
41
+ jobs:
42
+ implement:
43
+ runs-on: ubuntu-latest
44
+ timeout-minutes: 180
45
+ permissions:
46
+ contents: write
47
+ pull-requests: write
48
+ issues: write
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ with:
52
+ token: ${{ secrets.ERK_QUEUE_GH_PAT }}
53
+ fetch-depth: 0
54
+
55
+ - name: Install uv
56
+ uses: astral-sh/setup-uv@v5
57
+ with:
58
+ python-version: "3.14"
59
+
60
+ - name: Install Claude Code
61
+ uses: ./.github/actions/setup-claude-code
62
+
63
+ - name: Setup remaining tools
64
+ run: |
65
+ npm install -g prettier
66
+ cd $GITHUB_WORKSPACE
67
+ uv tool install -e . --with-editable ./packages/erk-shared
68
+
69
+ - name: Configure git
70
+ env:
71
+ SUBMITTED_BY: ${{ inputs.submitted_by }}
72
+ run: |
73
+ git config user.name "$SUBMITTED_BY"
74
+ git config user.email "${SUBMITTED_BY}@users.noreply.github.com"
75
+
76
+ - name: Detect trunk branch
77
+ id: trunk
78
+ run: |
79
+ result=$(erk exec detect-trunk-branch)
80
+ echo "trunk_branch=$(echo "$result" | jq -r '.trunk_branch')" >> $GITHUB_OUTPUT
81
+
82
+ - name: Set branch and PR from inputs
83
+ id: find_pr
84
+ run: |
85
+ echo "branch_name=${{ inputs.branch_name }}" >> $GITHUB_OUTPUT
86
+ echo "pr_number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
87
+ echo "pr_exists=true" >> $GITHUB_OUTPUT
88
+ echo "Using branch: ${{ inputs.branch_name }}"
89
+ echo "Using PR: #${{ inputs.pr_number }}"
90
+
91
+ - name: Checkout implementation branch
92
+ env:
93
+ BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
94
+ ISSUE_NUMBER: ${{ inputs.issue_number }}
95
+ SUBMITTED_BY: ${{ inputs.submitted_by }}
96
+ GH_TOKEN: ${{ github.token }}
97
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
98
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
99
+ run: |
100
+ git fetch origin "$BRANCH_NAME"
101
+ git checkout "$BRANCH_NAME"
102
+ rm -rf .worker-impl
103
+ erk exec create-worker-impl-from-issue "$ISSUE_NUMBER"
104
+ if [ $? -ne 0 ]; then
105
+ gh issue comment "$ISSUE_NUMBER" --body "Could not fetch plan content for issue #$ISSUE_NUMBER."
106
+ exit 1
107
+ fi
108
+ git config user.name "$SUBMITTED_BY"
109
+ git config user.email "$SUBMITTED_BY@users.noreply.github.com"
110
+ git add .worker-impl
111
+ if [[ -n $(git status --porcelain) ]]; then
112
+ git commit -m "Update plan for issue #$ISSUE_NUMBER (rerun)"
113
+ git push origin "$BRANCH_NAME"
114
+ fi
115
+ echo "Checked out branch: $BRANCH_NAME"
116
+
117
+ - name: Post workflow started comment
118
+ env:
119
+ ISSUE_NUMBER: ${{ inputs.issue_number }}
120
+ BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
121
+ PR_NUMBER: ${{ inputs.pr_number }}
122
+ GH_TOKEN: ${{ github.token }}
123
+ run: |
124
+ erk exec post-workflow-started-comment \
125
+ --issue-number "$ISSUE_NUMBER" \
126
+ --branch-name "$BRANCH_NAME" \
127
+ --pr-number "$PR_NUMBER" \
128
+ --run-id "${{ github.run_id }}" \
129
+ --run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
130
+ --repository "${{ github.repository }}"
131
+
132
+ - name: Add remote execution note to PR
133
+ env:
134
+ PR_NUMBER: ${{ inputs.pr_number }}
135
+ GH_TOKEN: ${{ github.token }}
136
+ WORKFLOW_RUN_ID: ${{ github.run_id }}
137
+ WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
138
+ run: |
139
+ erk exec add-remote-execution-note \
140
+ --pr-number "$PR_NUMBER" \
141
+ --run-id "$WORKFLOW_RUN_ID" \
142
+ --run-url "$WORKFLOW_RUN_URL"
143
+
144
+ - name: Set up implementation folder
145
+ run: |
146
+ cp -r .worker-impl .impl
147
+ echo "Copied .worker-impl/ to .impl/"
148
+ cat > .impl/run-info.json <<EOF
149
+ {
150
+ "run_id": "${{ github.run_id }}",
151
+ "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
152
+ }
153
+ EOF
154
+ echo "Created .impl/run-info.json"
155
+
156
+ - name: Run implementation
157
+ id: implement
158
+ run: |
159
+ set +e
160
+ claude --print \
161
+ --model ${{ inputs.model_name }} \
162
+ --output-format stream-json \
163
+ --dangerously-skip-permissions \
164
+ --verbose \
165
+ "/erk:plan-implement"
166
+ EXIT_CODE=$?
167
+ echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
168
+ if [ $EXIT_CODE -eq 0 ]; then
169
+ echo "implementation_success=true" >> $GITHUB_OUTPUT
170
+ else
171
+ echo "implementation_success=false" >> $GITHUB_OUTPUT
172
+ fi
173
+ if [ -d .worker-impl/ ]; then
174
+ rm -rf .worker-impl/
175
+ echo "Removed .worker-impl/ before submission"
176
+ fi
177
+ exit 0
178
+ env:
179
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
180
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
181
+ GH_TOKEN: ${{ github.token }}
182
+ PR_NUMBER: ${{ inputs.pr_number }}
183
+
184
+ - name: Submit branch with proper commit message
185
+ id: submit
186
+ continue-on-error: true
187
+ if: steps.implement.outputs.implementation_success == 'true'
188
+ env:
189
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
190
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
191
+ GH_TOKEN: ${{ github.token }}
192
+ ISSUE_NUMBER: ${{ inputs.issue_number }}
193
+ BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
194
+ SUBMITTED_BY: ${{ inputs.submitted_by }}
195
+ run: |
196
+ git config user.name "$SUBMITTED_BY"
197
+ git config user.email "$SUBMITTED_BY@users.noreply.github.com"
198
+ claude --print \
199
+ --model ${{ inputs.model_name }} \
200
+ --output-format stream-json \
201
+ --dangerously-skip-permissions \
202
+ --verbose \
203
+ "/erk:git-pr-push Implement issue #$ISSUE_NUMBER"
204
+ echo "impl_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
205
+
206
+ - name: Handle merge conflicts if push failed
207
+ id: handle_conflicts
208
+ if: steps.submit.outcome == 'failure'
209
+ env:
210
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
211
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
212
+ GH_TOKEN: ${{ github.token }}
213
+ run: |
214
+ erk exec rebase-with-conflict-resolution \
215
+ --trunk-branch "${{ steps.trunk.outputs.trunk_branch }}" \
216
+ --branch-name "${{ steps.find_pr.outputs.branch_name }}" \
217
+ --model "${{ inputs.model_name }}"
218
+
219
+ - name: Mark PR ready for review
220
+ if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
221
+ env:
222
+ GH_TOKEN: ${{ github.token }}
223
+ BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
224
+ run: |
225
+ gh pr ready "$BRANCH_NAME"
226
+ echo "PR marked ready for review"
227
+
228
+ - name: Update PR body with implementation summary
229
+ continue-on-error: true
230
+ if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
231
+ env:
232
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
233
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
234
+ GH_TOKEN: ${{ github.token }}
235
+ run: |
236
+ erk exec ci-update-pr-body \
237
+ --issue-number "${{ inputs.issue_number }}" \
238
+ --run-id "${{ github.run_id }}" \
239
+ --run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
240
+
241
+ - name: Trigger CI workflows
242
+ if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
243
+ env:
244
+ BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
245
+ SUBMITTED_BY: ${{ inputs.submitted_by }}
246
+ run: |
247
+ git config user.name "$SUBMITTED_BY"
248
+ git config user.email "$SUBMITTED_BY@users.noreply.github.com"
249
+ git commit --allow-empty -m "Trigger CI workflows"
250
+ git push origin "$BRANCH_NAME"
251
+ echo "CI workflows triggered via push event"
erk/hooks/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Hook utilities for erk CLI."""
@@ -0,0 +1,319 @@
1
+ """Decorators for hook commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import inspect
7
+ import io
8
+ import json
9
+ import os
10
+ import sys
11
+ import traceback
12
+ from collections.abc import Callable
13
+ from contextlib import redirect_stderr, redirect_stdout
14
+ from dataclasses import dataclass
15
+ from datetime import UTC, datetime
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, TypeVar, cast
18
+
19
+ if TYPE_CHECKING:
20
+ import click
21
+
22
+ from erk_shared.context.types import NoRepoSentinel
23
+ from erk_shared.hooks.logging import (
24
+ MAX_STDERR_BYTES,
25
+ MAX_STDIN_BYTES,
26
+ MAX_STDOUT_BYTES,
27
+ truncate_string,
28
+ write_hook_log,
29
+ )
30
+ from erk_shared.hooks.types import HookExecutionLog, HookExitStatus, classify_exit_code
31
+ from erk_shared.scratch.scratch import get_scratch_dir
32
+
33
+ F = TypeVar("F", bound=Callable[..., None])
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class HookContext:
38
+ """Context injected into hooks by the @logged_hook decorator.
39
+
40
+ This dataclass consolidates common derived values that hooks need,
41
+ eliminating duplicated code across hook implementations.
42
+
43
+ Attributes:
44
+ session_id: Claude session ID from stdin JSON, or None if not available.
45
+ repo_root: Path to the git repository root.
46
+ scratch_dir: Session-scoped scratch directory, or None if no session_id.
47
+ is_erk_project: True if repo_root/.erk directory exists.
48
+ """
49
+
50
+ session_id: str | None
51
+ repo_root: Path
52
+ scratch_dir: Path | None
53
+ is_erk_project: bool
54
+
55
+
56
+ def _read_stdin_once() -> str:
57
+ """Read stdin if available, returning empty string if not.
58
+
59
+ This is a one-time read - stdin cannot be read again after this.
60
+ """
61
+ if sys.stdin.isatty():
62
+ return ""
63
+ return sys.stdin.read()
64
+
65
+
66
+ def _extract_session_id(stdin_data: str) -> str | None:
67
+ """Extract session_id from stdin JSON if present.
68
+
69
+ Args:
70
+ stdin_data: Raw stdin content
71
+
72
+ Returns:
73
+ session_id if found in JSON, None otherwise
74
+ """
75
+ if not stdin_data.strip():
76
+ return None
77
+ data = json.loads(stdin_data)
78
+ return data.get("session_id")
79
+
80
+
81
+ def _build_hook_context(
82
+ session_id: str | None,
83
+ repo_root: Path,
84
+ ) -> HookContext:
85
+ """Build HookContext with derived values.
86
+
87
+ Args:
88
+ session_id: Claude session ID from stdin JSON, or None.
89
+ repo_root: Path to the git repository root.
90
+
91
+ Returns:
92
+ HookContext with all derived values computed.
93
+ """
94
+ is_erk_project = (repo_root / ".erk").is_dir()
95
+
96
+ scratch_dir: Path | None = None
97
+ if session_id is not None:
98
+ scratch_dir = get_scratch_dir(session_id, repo_root=repo_root)
99
+
100
+ return HookContext(
101
+ session_id=session_id,
102
+ repo_root=repo_root,
103
+ scratch_dir=scratch_dir,
104
+ is_erk_project=is_erk_project,
105
+ )
106
+
107
+
108
+ def _extract_repo_root_from_click_context(args: tuple) -> Path | None:
109
+ """Extract repo_root from Click context if available.
110
+
111
+ Looks for ctx.obj with a repo attribute that has a root path.
112
+ Handles NoRepoSentinel by returning None.
113
+
114
+ Args:
115
+ args: Positional arguments passed to the wrapped function.
116
+
117
+ Returns:
118
+ Path to repo root if found, None otherwise.
119
+ """
120
+ # First arg should be Click context if @click.pass_context was used
121
+ if not args:
122
+ return None
123
+
124
+ ctx = args[0]
125
+
126
+ # Check if it's a Click context with our ErkContext in obj
127
+ if not hasattr(ctx, "obj"):
128
+ return None
129
+
130
+ obj = ctx.obj
131
+ if obj is None:
132
+ return None
133
+
134
+ if not hasattr(obj, "repo"):
135
+ return None
136
+
137
+ repo = obj.repo
138
+ if isinstance(repo, NoRepoSentinel):
139
+ return None
140
+
141
+ if not hasattr(repo, "root"):
142
+ return None
143
+
144
+ return repo.root
145
+
146
+
147
+ def _function_accepts_hook_ctx(func: Callable) -> bool:
148
+ """Check if a function signature accepts hook_ctx parameter.
149
+
150
+ Args:
151
+ func: The function to inspect.
152
+
153
+ Returns:
154
+ True if function has a hook_ctx parameter.
155
+ """
156
+ sig = inspect.signature(func)
157
+ return "hook_ctx" in sig.parameters
158
+
159
+
160
+ def logged_hook(func: F) -> F:
161
+ """Decorator that logs hook execution for health monitoring.
162
+
163
+ This decorator MUST be applied BEFORE @project_scoped so that logging
164
+ happens even when the hook exits early due to project scope.
165
+
166
+ The decorator:
167
+ 1. Reads ERK_HOOK_ID from environment
168
+ 2. Captures stdin (contains session_id in JSON from Claude Code)
169
+ 3. Redirects stdout/stderr to capture output
170
+ 4. Records timing and exit status
171
+ 5. Writes log on exit (success or failure)
172
+ 6. Re-emits captured output to real stdout/stderr
173
+ 7. Injects HookContext if function signature accepts hook_ctx parameter
174
+
175
+ Environment variables:
176
+ ERK_HOOK_ID: Hook identifier (e.g., "session-id-injector-hook")
177
+
178
+ Usage:
179
+ @click.command()
180
+ @click.pass_context
181
+ @logged_hook
182
+ def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
183
+ if not hook_ctx.is_erk_project:
184
+ return
185
+ click.echo(f"Session: {hook_ctx.session_id}")
186
+ """
187
+ # Check once at decoration time whether function accepts hook_ctx
188
+ accepts_hook_ctx = _function_accepts_hook_ctx(func)
189
+
190
+ @functools.wraps(func)
191
+ def wrapper(*args, **kwargs):
192
+ # Read environment variables
193
+ hook_id = os.environ.get("ERK_HOOK_ID", "unknown")
194
+
195
+ # Capture stdin before hook reads it
196
+ stdin_data = _read_stdin_once()
197
+ session_id: str | None = None
198
+ try:
199
+ session_id = _extract_session_id(stdin_data)
200
+ except (json.JSONDecodeError, KeyError, TypeError):
201
+ pass
202
+
203
+ # Replace stdin with a StringIO containing the captured data
204
+ # so the hook can still read it
205
+ original_stdin = sys.stdin
206
+ sys.stdin = io.StringIO(stdin_data)
207
+
208
+ # Build HookContext if function accepts it and we can extract repo_root
209
+ if accepts_hook_ctx:
210
+ repo_root = _extract_repo_root_from_click_context(args)
211
+ if repo_root is not None:
212
+ hook_ctx = _build_hook_context(session_id, repo_root)
213
+ kwargs["hook_ctx"] = hook_ctx
214
+
215
+ # Capture stdout/stderr
216
+ stdout_buffer = io.StringIO()
217
+ stderr_buffer = io.StringIO()
218
+
219
+ # Record start time
220
+ started_at = datetime.now(UTC)
221
+ exit_code = 0
222
+ exit_status = HookExitStatus.SUCCESS
223
+ error_message: str | None = None
224
+
225
+ try:
226
+ with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
227
+ func(*args, **kwargs)
228
+ except SystemExit as e:
229
+ # Click raises SystemExit on exit
230
+ exit_code = e.code if isinstance(e.code, int) else 1
231
+ exit_status = classify_exit_code(exit_code)
232
+ except Exception as e:
233
+ # Uncaught exception
234
+ exit_code = 1
235
+ exit_status = HookExitStatus.EXCEPTION
236
+ error_message = f"{type(e).__name__}: {e}"
237
+ # Write traceback to stderr buffer
238
+ stderr_buffer.write(traceback.format_exc())
239
+ finally:
240
+ # Restore stdin
241
+ sys.stdin = original_stdin
242
+
243
+ # Record end time
244
+ ended_at = datetime.now(UTC)
245
+ duration_ms = int((ended_at - started_at).total_seconds() * 1000)
246
+
247
+ # Get captured output
248
+ stdout_content = stdout_buffer.getvalue()
249
+ stderr_content = stderr_buffer.getvalue()
250
+
251
+ # Create log entry
252
+ log = HookExecutionLog(
253
+ kit_id="erk", # All hooks are now in erk
254
+ hook_id=hook_id,
255
+ session_id=session_id,
256
+ started_at=started_at.isoformat(),
257
+ ended_at=ended_at.isoformat(),
258
+ duration_ms=duration_ms,
259
+ exit_code=exit_code,
260
+ exit_status=exit_status,
261
+ stdout=truncate_string(stdout_content, MAX_STDOUT_BYTES),
262
+ stderr=truncate_string(stderr_content, MAX_STDERR_BYTES),
263
+ stdin_context=truncate_string(stdin_data, MAX_STDIN_BYTES),
264
+ error_message=error_message,
265
+ )
266
+
267
+ # Write log (only if we have a session_id)
268
+ write_hook_log(log)
269
+
270
+ # Re-emit captured output
271
+ sys.stdout.write(stdout_content)
272
+ sys.stderr.write(stderr_content)
273
+
274
+ # Re-raise SystemExit if hook exited with non-zero
275
+ if exit_code != 0:
276
+ raise SystemExit(exit_code)
277
+
278
+ # Cast wrapper to F - functools.wraps preserves the signature semantics
279
+ return cast(F, wrapper)
280
+
281
+
282
+ def hook_command(name: str | None = None) -> Callable[[Callable[..., None]], click.Command]:
283
+ """Combined decorator for hook commands.
284
+
285
+ This decorator combines @click.command, @click.pass_context, and @logged_hook
286
+ into a single decorator, reducing boilerplate in hook implementations.
287
+
288
+ Args:
289
+ name: Optional command name. If not provided, Click will infer from function name.
290
+
291
+ Usage:
292
+ @hook_command(name="my-hook")
293
+ def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
294
+ if not hook_ctx.is_erk_project:
295
+ return
296
+ click.echo(f"Session: {hook_ctx.session_id}")
297
+
298
+ Equivalent to:
299
+ @click.command(name="my-hook")
300
+ @click.pass_context
301
+ @logged_hook
302
+ def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
303
+ ...
304
+ """
305
+ # Inline import to avoid circular dependency at module load time
306
+ import click
307
+
308
+ def decorator(func: Callable[..., None]) -> click.Command:
309
+ # Apply decorators in reverse order (innermost first)
310
+ # 1. @logged_hook (innermost - applied first)
311
+ wrapped = logged_hook(func)
312
+ # 2. @click.pass_context
313
+ wrapped = click.pass_context(wrapped)
314
+ # 3. @click.command (outermost - applied last)
315
+ if name is not None:
316
+ return click.command(name=name)(wrapped)
317
+ return click.command()(wrapped)
318
+
319
+ return decorator
erk/status/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Status command implementation.
2
+
3
+ Import from submodules:
4
+ - orchestrator: StatusOrchestrator
5
+ - models.status_data: StatusData, GitStatus, etc.
6
+ - collectors: StatusCollector, GitStatusCollector, etc.
7
+ - renderers.simple: SimpleRenderer
8
+ """
@@ -0,0 +1,9 @@
1
+ """Status information collectors.
2
+
3
+ Import from submodules:
4
+ - base: StatusCollector
5
+ - git: GitStatusCollector
6
+ - github: GitHubPRCollector
7
+ - graphite: GraphiteStackCollector
8
+ - impl: PlanFileCollector
9
+ """
@@ -0,0 +1,52 @@
1
+ """Base class for status information collectors."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from erk.core.context import ErkContext
8
+
9
+
10
+ class StatusCollector(ABC):
11
+ """Base class for status information collectors.
12
+
13
+ Each collector is responsible for gathering a specific type of status
14
+ information (git, PR, dependencies, etc.) and returning it in a structured
15
+ format.
16
+
17
+ Collectors should handle their own errors gracefully and return None
18
+ if information cannot be collected.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def name(self) -> str:
24
+ """Name identifier for this collector."""
25
+ ...
26
+
27
+ @abstractmethod
28
+ def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
29
+ """Check if this collector can run in the given worktree.
30
+
31
+ Args:
32
+ ctx: Erk context with operations
33
+ worktree_path: Path to the worktree
34
+
35
+ Returns:
36
+ True if collector can gather information, False otherwise
37
+ """
38
+ ...
39
+
40
+ @abstractmethod
41
+ def collect(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> Any:
42
+ """Collect status information from worktree.
43
+
44
+ Args:
45
+ ctx: Erk context with operations
46
+ worktree_path: Path to the worktree
47
+ repo_root: Path to repository root
48
+
49
+ Returns:
50
+ Collected status data or None if collection fails
51
+ """
52
+ ...