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,159 @@
1
+ """Set up .impl/ folder from GitHub issue in current worktree.
2
+
3
+ This exec command fetches a plan from a GitHub issue, creates a feature branch,
4
+ checks it out, and creates the .impl/ folder structure for implementation.
5
+
6
+ Usage:
7
+ erk exec setup-impl-from-issue <issue-number> [--session-id <id>]
8
+
9
+ Output:
10
+ Structured JSON output with success status and folder details
11
+
12
+ Exit Codes:
13
+ 0: Success (.impl/ folder created, branch checked out)
14
+ 1: Error (issue not found, plan fetch failed, git operations failed)
15
+
16
+ Examples:
17
+ $ erk exec setup-impl-from-issue 1028
18
+ {"success": true, "impl_path": "/path/to/.impl", "issue_number": 1028, "branch": "P1028-..."}
19
+ """
20
+
21
+ import json
22
+ from datetime import UTC, datetime
23
+ from pathlib import Path
24
+
25
+ import click
26
+
27
+ from erk_shared.context.helpers import require_cwd, require_git, require_repo_root
28
+ from erk_shared.gateway.time.real import RealTime
29
+ from erk_shared.git.abc import Git
30
+ from erk_shared.github.issues import RealGitHubIssues
31
+ from erk_shared.impl_folder import create_impl_folder, save_issue_reference
32
+ from erk_shared.naming import generate_issue_branch_name
33
+ from erk_shared.plan_store.github import GitHubPlanStore
34
+
35
+
36
+ def _get_current_branch(git: Git, cwd: Path) -> str:
37
+ """Get current branch via gateway, raising if detached HEAD."""
38
+ branch = git.get_current_branch(cwd)
39
+ if branch is None:
40
+ msg = "Cannot set up implementation from detached HEAD state"
41
+ raise click.ClickException(msg)
42
+ return branch
43
+
44
+
45
+ def _is_trunk_branch(branch: str) -> bool:
46
+ """Check if branch is a trunk branch (main/master)."""
47
+ return branch in ("main", "master")
48
+
49
+
50
+ @click.command(name="setup-impl-from-issue")
51
+ @click.argument("issue_number", type=int)
52
+ @click.option(
53
+ "--session-id",
54
+ default=None,
55
+ help="Claude session ID for marker creation",
56
+ )
57
+ @click.option(
58
+ "--no-impl",
59
+ is_flag=True,
60
+ help="Skip .impl/ folder creation (for local execution without file overhead)",
61
+ )
62
+ @click.pass_context
63
+ def setup_impl_from_issue(
64
+ ctx: click.Context,
65
+ issue_number: int,
66
+ session_id: str | None,
67
+ no_impl: bool,
68
+ ) -> None:
69
+ """Set up .impl/ folder from GitHub issue in current worktree.
70
+
71
+ Fetches plan content from GitHub issue, creates/checks out a feature branch,
72
+ and creates .impl/ folder structure with plan.md, progress.md, and issue.json.
73
+
74
+ ISSUE_NUMBER: GitHub issue number containing the plan
75
+
76
+ The command:
77
+ 1. Fetches the plan from the GitHub issue
78
+ 2. Creates a feature branch from current branch (stacked) or trunk
79
+ 3. Checks out the new branch in the current worktree
80
+ 4. Creates .impl/ folder with plan content
81
+ 5. Saves issue reference for PR linking
82
+ """
83
+ cwd = require_cwd(ctx)
84
+ repo_root = require_repo_root(ctx)
85
+ git = require_git(ctx)
86
+
87
+ # Direct instantiation of required dependencies
88
+ time = RealTime()
89
+ github_issues = RealGitHubIssues(target_repo=None)
90
+ plan_store = GitHubPlanStore(github_issues, time)
91
+
92
+ # Step 1: Fetch plan from GitHub
93
+ try:
94
+ plan = plan_store.get_plan(repo_root, str(issue_number))
95
+ except RuntimeError as e:
96
+ error_output = {
97
+ "success": False,
98
+ "error": "plan_not_found",
99
+ "message": f"Could not fetch plan for issue #{issue_number}: {e}. "
100
+ f"Ensure issue has erk-plan label and plan content.",
101
+ }
102
+ click.echo(json.dumps(error_output), err=True)
103
+ raise SystemExit(1) from e
104
+
105
+ # Step 2: Determine base branch and create feature branch
106
+ current_branch = _get_current_branch(git, cwd)
107
+
108
+ # Generate branch name from issue
109
+ timestamp = datetime.now(UTC)
110
+ branch_name = generate_issue_branch_name(issue_number, plan.title, timestamp)
111
+
112
+ # Check if branch already exists
113
+ local_branches = git.list_local_branches(repo_root)
114
+
115
+ if branch_name in local_branches:
116
+ # Branch exists - just check it out
117
+ click.echo(f"Branch '{branch_name}' already exists, checking out...", err=True)
118
+ git.checkout_branch(cwd, branch_name)
119
+ else:
120
+ # Determine base branch: stack on feature branch, or use trunk
121
+ if _is_trunk_branch(current_branch):
122
+ base_branch = current_branch
123
+ else:
124
+ # Stack on current feature branch
125
+ base_branch = current_branch
126
+
127
+ # Create and checkout branch
128
+ git.create_branch(repo_root, branch_name, base_branch)
129
+ git.checkout_branch(cwd, branch_name)
130
+ click.echo(f"Created branch '{branch_name}' from '{base_branch}'", err=True)
131
+
132
+ # Step 3: Create .impl/ folder with plan content (unless --no-impl)
133
+ impl_path_str: str | None = None
134
+
135
+ if not no_impl:
136
+ impl_path = cwd / ".impl"
137
+ impl_path_str = str(impl_path)
138
+
139
+ # Use overwrite=True since we may be re-running after a failed attempt
140
+ create_impl_folder(
141
+ worktree_path=cwd,
142
+ plan_content=plan.body,
143
+ overwrite=True,
144
+ )
145
+
146
+ # Step 4: Save issue reference for PR linking
147
+ save_issue_reference(impl_path, issue_number, plan.url, plan.title)
148
+
149
+ # Output structured success result
150
+ output: dict[str, str | int | bool | None] = {
151
+ "success": True,
152
+ "impl_path": impl_path_str,
153
+ "issue_number": issue_number,
154
+ "issue_url": plan.url,
155
+ "branch": branch_name,
156
+ "plan_title": plan.title,
157
+ "no_impl": no_impl,
158
+ }
159
+ click.echo(json.dumps(output))
@@ -0,0 +1,102 @@
1
+ """Get the last objective issue for the current slot.
2
+
3
+ This exec command looks up the current worktree's slot and returns
4
+ the last_objective_issue from pool.json.
5
+
6
+ Usage:
7
+ erk exec slot-objective
8
+
9
+ Output:
10
+ JSON with objective_issue (null if not found or not in a slot)
11
+
12
+ Exit Codes:
13
+ 0: Success (even if no objective found)
14
+
15
+ Examples:
16
+ $ erk exec slot-objective
17
+ {"objective_issue": 123, "slot_name": "erk-managed-wt-01"}
18
+
19
+ $ erk exec slot-objective # Not in a slot worktree
20
+ {"objective_issue": null, "slot_name": null}
21
+ """
22
+
23
+ import json
24
+ from pathlib import Path
25
+
26
+ import click
27
+
28
+ from erk.core.worktree_pool import load_pool_state
29
+ from erk_shared.context.helpers import require_cwd
30
+ from erk_shared.context.types import NoRepoSentinel
31
+
32
+
33
+ def _null_result() -> None:
34
+ """Output null result and return."""
35
+ click.echo(json.dumps({"objective_issue": None, "slot_name": None}))
36
+
37
+
38
+ @click.command(name="slot-objective")
39
+ @click.pass_context
40
+ def slot_objective(ctx: click.Context) -> None:
41
+ """Get the last objective issue for the current slot.
42
+
43
+ Looks up the current worktree in pool.json and returns
44
+ the slot's last_objective_issue if set.
45
+ """
46
+ if ctx.obj is None:
47
+ _null_result()
48
+ return
49
+
50
+ cwd = require_cwd(ctx)
51
+ repo = ctx.obj.repo
52
+
53
+ # Check if we're in a repo
54
+ if isinstance(repo, NoRepoSentinel):
55
+ _null_result()
56
+ return
57
+
58
+ # Load pool state
59
+ state = load_pool_state(repo.pool_json_path)
60
+ if state is None:
61
+ _null_result()
62
+ return
63
+
64
+ # Find assignment for current worktree
65
+ cwd_resolved = cwd.resolve()
66
+ slot_name: str | None = None
67
+
68
+ for assignment in state.assignments:
69
+ if not assignment.worktree_path.exists():
70
+ continue
71
+ if assignment.worktree_path.resolve() == cwd_resolved:
72
+ slot_name = assignment.slot_name
73
+ break
74
+
75
+ if slot_name is None:
76
+ # Check if cwd is within an assignment's worktree
77
+ for assignment in state.assignments:
78
+ if _is_path_within(cwd_resolved, assignment.worktree_path):
79
+ slot_name = assignment.slot_name
80
+ break
81
+
82
+ if slot_name is None:
83
+ _null_result()
84
+ return
85
+
86
+ # Find slot info to get last_objective_issue
87
+ objective_issue: int | None = None
88
+ for slot in state.slots:
89
+ if slot.name == slot_name:
90
+ objective_issue = slot.last_objective_issue
91
+ break
92
+
93
+ result = {"objective_issue": objective_issue, "slot_name": slot_name}
94
+ click.echo(json.dumps(result))
95
+
96
+
97
+ def _is_path_within(child: Path, parent: Path) -> bool:
98
+ """Check if child path is within parent path."""
99
+ if not parent.exists():
100
+ return False
101
+ parent_resolved = parent.resolve()
102
+ return child == parent_resolved or parent_resolved in child.parents
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Tripwires Reminder Hook."""
3
+
4
+ import click
5
+
6
+ from erk.hooks.decorators import HookContext, hook_command
7
+
8
+
9
+ @hook_command()
10
+ def tripwires_reminder_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
11
+ """Output tripwires reminder for UserPromptSubmit hook."""
12
+ # Scope check: only run in erk-managed projects
13
+ if not hook_ctx.is_erk_project:
14
+ return
15
+
16
+ click.echo("🚧 Ensure docs/learned/tripwires.md is loaded and follow its directives.")
17
+
18
+
19
+ if __name__ == "__main__":
20
+ tripwires_reminder_hook()
@@ -0,0 +1,116 @@
1
+ """Update dispatch info in GitHub issue plan-header metadata.
2
+
3
+ Usage:
4
+ erk exec update-dispatch-info <issue-number> <run-id> <node-id> <dispatched-at>
5
+
6
+ Output:
7
+ JSON with success status and issue_number
8
+
9
+ Exit Codes:
10
+ 0: Success
11
+ 1: Error (issue not found, invalid inputs, no plan-header block)
12
+ """
13
+
14
+ import json
15
+ from dataclasses import asdict, dataclass
16
+
17
+ import click
18
+
19
+ from erk_shared.context.helpers import require_issues as require_github_issues
20
+ from erk_shared.context.helpers import require_repo_root
21
+ from erk_shared.github.metadata.plan_header import update_plan_header_dispatch
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class UpdateSuccess:
26
+ """Success response for dispatch info update."""
27
+
28
+ success: bool
29
+ issue_number: int
30
+ run_id: str
31
+ node_id: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class UpdateError:
36
+ """Error response for dispatch info update."""
37
+
38
+ success: bool
39
+ error: str
40
+ message: str
41
+
42
+
43
+ @click.command(name="update-dispatch-info")
44
+ @click.argument("issue_number", type=int)
45
+ @click.argument("run_id")
46
+ @click.argument("node_id")
47
+ @click.argument("dispatched_at")
48
+ @click.pass_context
49
+ def update_dispatch_info(
50
+ ctx: click.Context,
51
+ issue_number: int,
52
+ run_id: str,
53
+ node_id: str,
54
+ dispatched_at: str,
55
+ ) -> None:
56
+ """Update dispatch info in GitHub issue plan-header metadata.
57
+
58
+ Fetches the issue, updates the plan-header block with last_dispatched_run_id,
59
+ last_dispatched_node_id, and last_dispatched_at, and posts the updated body
60
+ back to GitHub.
61
+
62
+ If issue uses old format (no plan-header block), exits with error code 1.
63
+ """
64
+ # Get dependencies from context
65
+ github_issues = require_github_issues(ctx)
66
+ repo_root = require_repo_root(ctx)
67
+
68
+ # Fetch current issue
69
+ try:
70
+ issue = github_issues.get_issue(repo_root, issue_number)
71
+ except RuntimeError as e:
72
+ result = UpdateError(
73
+ success=False,
74
+ error="issue-not-found",
75
+ message=f"Issue #{issue_number} not found: {e}",
76
+ )
77
+ click.echo(json.dumps(asdict(result)), err=True)
78
+ raise SystemExit(1) from None
79
+
80
+ # Update dispatch info
81
+ try:
82
+ updated_body = update_plan_header_dispatch(
83
+ issue_body=issue.body,
84
+ run_id=run_id,
85
+ node_id=node_id,
86
+ dispatched_at=dispatched_at,
87
+ )
88
+ except ValueError as e:
89
+ # plan-header block not found (old format issue)
90
+ result = UpdateError(
91
+ success=False,
92
+ error="no-plan-header-block",
93
+ message=str(e),
94
+ )
95
+ click.echo(json.dumps(asdict(result)), err=True)
96
+ raise SystemExit(1) from None
97
+
98
+ # Update issue body
99
+ try:
100
+ github_issues.update_issue_body(repo_root, issue_number, updated_body)
101
+ except RuntimeError as e:
102
+ result = UpdateError(
103
+ success=False,
104
+ error="github-api-failed",
105
+ message=f"Failed to update issue body: {e}",
106
+ )
107
+ click.echo(json.dumps(asdict(result)), err=True)
108
+ raise SystemExit(1) from None
109
+
110
+ result_success = UpdateSuccess(
111
+ success=True,
112
+ issue_number=issue_number,
113
+ run_id=run_id,
114
+ node_id=node_id,
115
+ )
116
+ click.echo(json.dumps(asdict(result_success)))
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit hook for erk.
3
+
4
+ Consolidates multiple hooks into a single script:
5
+ 1. Session ID injection + file persistence
6
+ 2. Coding standards reminders
7
+ 3. Tripwires reminder
8
+
9
+ Exit codes:
10
+ 0: All checks pass, stdout goes to Claude's context
11
+
12
+ This command is invoked via:
13
+ ERK_HOOK_ID=user-prompt-hook erk exec user-prompt-hook
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+ import click
19
+
20
+ from erk.hooks.decorators import HookContext, hook_command
21
+
22
+ # ============================================================================
23
+ # Pure Functions for Output Building
24
+ # ============================================================================
25
+
26
+
27
+ def build_session_context(session_id: str | None) -> str:
28
+ """Build the session ID context string.
29
+
30
+ Pure function - string building only.
31
+ """
32
+ if session_id is None:
33
+ return ""
34
+ return f"session: {session_id}"
35
+
36
+
37
+ def build_coding_standards_reminder() -> str:
38
+ """Return coding standards context.
39
+
40
+ Pure function - returns static string.
41
+ """
42
+ return """No direct Bash for: pytest/ty/ruff/prettier/make/gt
43
+ Use Task(subagent_type='devrun') instead.
44
+ dignified-python: CRITICAL RULES (examples - full skill has more):
45
+ NO try/except for control flow (use LBYL - check conditions first)
46
+ NO default parameter values (no `foo: bool = False`)
47
+ NO mutable/non-frozen dataclasses (always `@dataclass(frozen=True)`)
48
+ MANDATORY: Load and READ the full dignified-python skill documents.
49
+ These are examples only. You MUST strictly abide by ALL rules in the skill.
50
+ AFTER completing Python changes: Verify sufficient test coverage.
51
+ Behavior changes ALWAYS need tests."""
52
+
53
+
54
+ def build_tripwires_reminder() -> str:
55
+ """Return tripwires context.
56
+
57
+ Pure function - returns static string.
58
+ """
59
+ return "Ensure docs/learned/tripwires.md is loaded and follow its directives."
60
+
61
+
62
+ # ============================================================================
63
+ # I/O Helper Functions
64
+ # ============================================================================
65
+
66
+
67
+ def _persist_session_id(repo_root: Path, session_id: str | None) -> None:
68
+ """Write session ID to file.
69
+
70
+ Args:
71
+ repo_root: Path to the git repository root.
72
+ session_id: The current session ID, or None if not available.
73
+ """
74
+ if session_id is None:
75
+ return
76
+
77
+ session_file = repo_root / ".erk" / "scratch" / "current-session-id"
78
+ session_file.parent.mkdir(parents=True, exist_ok=True)
79
+ session_file.write_text(session_id, encoding="utf-8")
80
+
81
+
82
+ # ============================================================================
83
+ # Main Hook Entry Point
84
+ # ============================================================================
85
+
86
+
87
+ @hook_command(name="user-prompt-hook")
88
+ def user_prompt_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
89
+ """UserPromptSubmit hook for session persistence and coding reminders.
90
+
91
+ This hook runs on every user prompt submission in erk-managed projects.
92
+
93
+ Exit codes:
94
+ 0: Success - context emitted to stdout
95
+ """
96
+ # Scope check: only run in erk-managed projects
97
+ if not hook_ctx.is_erk_project:
98
+ return
99
+
100
+ # Persist session ID
101
+ _persist_session_id(hook_ctx.repo_root, hook_ctx.session_id)
102
+
103
+ # Build and emit context
104
+ context_parts = [
105
+ build_session_context(hook_ctx.session_id),
106
+ build_coding_standards_reminder(),
107
+ build_tripwires_reminder(),
108
+ ]
109
+ click.echo("\n".join(p for p in context_parts if p))
110
+
111
+
112
+ if __name__ == "__main__":
113
+ user_prompt_hook()
@@ -0,0 +1,98 @@
1
+ """Validate plan content structure and quality.
2
+
3
+ This exec command validates that plan content meets minimum requirements
4
+ for structure and length. It accepts plan content via stdin.
5
+
6
+ Usage:
7
+ echo "$plan" | erk exec validate-plan-content
8
+
9
+ Output:
10
+ JSON with validation status and details
11
+
12
+ Exit Codes:
13
+ 0: Success (always - check JSON for validation result)
14
+
15
+ Examples:
16
+ $ echo "# My Plan\n\n- Step 1\n- Step 2" | erk exec validate-plan-content
17
+ {"valid": true, "error": null, "details": {"length": 29, "has_headers": true,
18
+ "has_lists": true}}
19
+
20
+ $ echo "too short" | erk exec validate-plan-content
21
+ {"valid": false, "error": "Plan too short (9 characters, minimum 100)",
22
+ "details": {"length": 9, "has_headers": false, "has_lists": false}}
23
+ """
24
+
25
+ import json
26
+ import sys
27
+
28
+ import click
29
+
30
+
31
+ def _validate_plan_content(content: str) -> tuple[bool, str | None, dict[str, bool | int]]:
32
+ """Validate plan content meets minimum requirements.
33
+
34
+ Args:
35
+ content: Plan content as string
36
+
37
+ Returns:
38
+ Tuple of (valid, error_message, details_dict)
39
+ - valid: True if plan passes all checks
40
+ - error_message: None if valid, descriptive error if invalid
41
+ - details_dict: Dict with length, has_headers, has_lists
42
+ """
43
+ # Strip whitespace for validation
44
+ content_stripped = content.strip()
45
+ length = len(content_stripped)
46
+
47
+ # Check for structural elements
48
+ has_headers = any(line.startswith("#") for line in content_stripped.split("\n"))
49
+ has_lists = any(
50
+ line.strip().startswith(("-", "*", "+"))
51
+ or (line.strip() and line.strip()[0].isdigit() and ". " in line)
52
+ for line in content_stripped.split("\n")
53
+ )
54
+
55
+ details = {
56
+ "length": length,
57
+ "has_headers": has_headers,
58
+ "has_lists": has_lists,
59
+ }
60
+
61
+ # Validation checks
62
+ if not content_stripped:
63
+ return False, "Plan is empty or contains only whitespace", details
64
+
65
+ if length < 100:
66
+ return False, f"Plan too short ({length} characters, minimum 100)", details
67
+
68
+ if not has_headers and not has_lists:
69
+ return (
70
+ False,
71
+ "Plan lacks structure (no headers or lists found)",
72
+ details,
73
+ )
74
+
75
+ return True, None, details
76
+
77
+
78
+ @click.command(name="validate-plan-content")
79
+ def validate_plan_content() -> None:
80
+ """Validate plan content from stdin.
81
+
82
+ Reads plan content from stdin and validates:
83
+ - Minimum 100 characters
84
+ - Contains structural elements (headers OR lists)
85
+ - Not empty/whitespace only
86
+
87
+ Outputs JSON with validation result and details.
88
+ """
89
+ content = sys.stdin.read()
90
+ valid, error, details = _validate_plan_content(content)
91
+
92
+ result = {
93
+ "valid": valid,
94
+ "error": error,
95
+ "details": details,
96
+ }
97
+
98
+ click.echo(json.dumps(result))
@@ -0,0 +1,34 @@
1
+ """Wrap a plan in a collapsible GitHub metadata block."""
2
+
3
+ import sys
4
+
5
+ import click
6
+
7
+
8
+ @click.command(name="wrap-plan-in-metadata-block")
9
+ def wrap_plan_in_metadata_block() -> None:
10
+ """Return plan content for issue body.
11
+
12
+ Reads plan content from stdin and returns it as-is (stripped).
13
+ Formatting and workflow instructions will be added via a separate comment.
14
+
15
+ Usage:
16
+ echo "$plan" | erk exec wrap-plan-in-metadata-block
17
+
18
+ Exit Codes:
19
+ 0: Success
20
+ 1: Error (empty input)
21
+ """
22
+ # Read plan content from stdin
23
+ plan_content = sys.stdin.read()
24
+
25
+ # Validate input is not empty
26
+ if not plan_content or not plan_content.strip():
27
+ click.echo("Error: Empty plan content received", err=True)
28
+ raise SystemExit(1)
29
+
30
+ # Return plan content as-is (metadata wrapping delegated to separate comments)
31
+ result = plan_content.strip()
32
+
33
+ # Output the result
34
+ click.echo(result)