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,16 @@
1
+ """Project management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.project.init_cmd import init_project
6
+ from erk.cli.help_formatter import ErkCommandGroup
7
+
8
+
9
+ @click.group("project", cls=ErkCommandGroup, grouped=False)
10
+ def project_group() -> None:
11
+ """Manage project or projects (within a monorepo)."""
12
+ pass
13
+
14
+
15
+ # Register subcommands
16
+ project_group.add_command(init_project)
@@ -0,0 +1,91 @@
1
+ """Project init command - initialize a project in the current directory."""
2
+
3
+ import click
4
+
5
+ from erk.cli.core import discover_repo_context
6
+ from erk.core.context import ErkContext
7
+ from erk_shared.output.output import user_output
8
+
9
+ # Template for project.toml
10
+ PROJECT_TOML_TEMPLATE = """\
11
+ # Project configuration for erk
12
+ # This file identifies this directory as an erk project within a monorepo.
13
+
14
+ # Optional: custom project name (defaults to directory name)
15
+ # name = "{project_name}"
16
+
17
+ [env]
18
+ # Project-specific environment variables (merged with repo-level config)
19
+ # These variables are available in .env files created for worktrees
20
+ # Example:
21
+ # DAGSTER_HOME = "{{project_root}}"
22
+
23
+ [post_create]
24
+ # Commands to run after worktree creation, FROM the project directory
25
+ # These run AFTER repo-level post_create commands
26
+ # shell = "bash"
27
+ # commands = [
28
+ # "source .venv/bin/activate",
29
+ # ]
30
+ """
31
+
32
+
33
+ @click.command("init")
34
+ @click.pass_obj
35
+ def init_project(ctx: ErkContext) -> None:
36
+ """Initialize a project in the current directory.
37
+
38
+ Creates a .erk/project.toml file that identifies this directory as
39
+ a project within a monorepo. When worktrees are created from this
40
+ project context, erk will:
41
+
42
+ - Record the project path in worktrees.toml
43
+ - Navigate to the project subdirectory on `erk wt co`
44
+ - Merge project-level config with repo-level config
45
+ - Run project-specific post_create commands
46
+
47
+ Example:
48
+ cd /code/internal/python_modules/my-project
49
+ erk project init
50
+ """
51
+ # Validate we're in a git repo
52
+ repo = discover_repo_context(ctx, ctx.cwd)
53
+
54
+ # Don't allow init at repo root (check before project.toml to give clearer error)
55
+ if ctx.cwd.resolve() == repo.root.resolve():
56
+ user_output(
57
+ click.style("Error: ", fg="red") + "Cannot initialize project at repository root.\n"
58
+ "Projects are subdirectories within a repo. "
59
+ "Use `erk init` for repository-level configuration."
60
+ )
61
+ raise SystemExit(1)
62
+
63
+ # Check if project.toml already exists
64
+ project_toml_path = ctx.cwd / ".erk" / "project.toml"
65
+ if ctx.git.path_exists(project_toml_path):
66
+ user_output(
67
+ click.style("Error: ", fg="red") + f"Project already initialized: {project_toml_path}"
68
+ )
69
+ raise SystemExit(1)
70
+
71
+ # Create .erk directory and project.toml
72
+ erk_dir = ctx.cwd / ".erk"
73
+ erk_dir.mkdir(parents=True, exist_ok=True)
74
+
75
+ project_name = ctx.cwd.name
76
+ content = PROJECT_TOML_TEMPLATE.format(project_name=project_name)
77
+ project_toml_path.write_text(content, encoding="utf-8")
78
+
79
+ # Calculate path from repo root for display
80
+ path_from_repo = ctx.cwd.relative_to(repo.root)
81
+
82
+ user_output(
83
+ click.style("✓ ", fg="green")
84
+ + f"Initialized project: {click.style(project_name, fg='cyan', bold=True)}"
85
+ )
86
+ user_output(f" Location: {path_from_repo}")
87
+ user_output(f" Config: {project_toml_path}")
88
+ user_output("")
89
+ user_output("Next steps:")
90
+ user_output(f" 1. Edit {project_toml_path} to configure project settings")
91
+ user_output(" 2. Create a worktree from this project: erk wt create <name>")
@@ -0,0 +1,17 @@
1
+ """Run management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.run.list_cmd import list_runs
6
+ from erk.cli.commands.run.logs_cmd import logs_run
7
+
8
+
9
+ @click.group("run")
10
+ def run_group() -> None:
11
+ """View GitHub Actions workflow runs for plan implementations."""
12
+ pass
13
+
14
+
15
+ # Register subcommands
16
+ run_group.add_command(list_runs)
17
+ run_group.add_command(logs_run)
@@ -0,0 +1,189 @@
1
+ """List workflow runs command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from erk.cli.commands.plan.list_cmd import format_pr_cell, select_display_pr
8
+ from erk.cli.commands.run.shared import extract_issue_number
9
+ from erk.cli.constants import DISPATCH_WORKFLOW_NAME
10
+ from erk.cli.core import discover_repo_context
11
+ from erk.cli.ensure import Ensure
12
+ from erk.core.context import ErkContext
13
+ from erk.core.display_utils import (
14
+ format_submission_time,
15
+ format_workflow_outcome,
16
+ format_workflow_run_id,
17
+ )
18
+ from erk_shared.github.emoji import format_checks_cell
19
+ from erk_shared.github.parsing import github_repo_location_from_url
20
+ from erk_shared.github.types import GitHubRepoId
21
+ from erk_shared.output.output import user_output
22
+
23
+
24
+ def _list_runs(ctx: ErkContext, show_all: bool = False) -> None:
25
+ """List workflow runs in a run-centric table view."""
26
+ # Validate preconditions upfront (LBYL)
27
+ Ensure.gh_authenticated(ctx)
28
+
29
+ # Discover repository context
30
+ repo = discover_repo_context(ctx, ctx.cwd)
31
+
32
+ # 1. Fetch workflow runs from dispatch workflow
33
+ runs = ctx.github.list_workflow_runs(repo.root, DISPATCH_WORKFLOW_NAME)
34
+
35
+ # Handle empty state
36
+ if not runs:
37
+ user_output("No workflow runs found")
38
+ return
39
+
40
+ # Filter out runs without plans unless --show-legacy flag is set
41
+ if not show_all:
42
+ runs = [run for run in runs if extract_issue_number(run.display_title) is not None]
43
+ if not runs:
44
+ user_output("No runs with plans found. Use --show-legacy to see all runs.")
45
+ return
46
+
47
+ # 2. Extract issue numbers from display_title (format: "123:abc456")
48
+ issue_numbers: list[int] = []
49
+ for run in runs:
50
+ issue_num = extract_issue_number(run.display_title)
51
+ if issue_num is not None:
52
+ issue_numbers.append(issue_num)
53
+
54
+ # 3. Fetch issues for titles (using issues interface)
55
+ issues = ctx.issues.list_issues(repo.root, labels=["erk-plan"])
56
+ issue_map = {issue.number: issue for issue in issues}
57
+
58
+ # Second filtering pass - remove runs where we can't display title
59
+ if not show_all:
60
+ filtered_runs = []
61
+ for run in runs:
62
+ issue_num = extract_issue_number(run.display_title)
63
+ if issue_num is None:
64
+ continue # Already filtered, but defensive check
65
+
66
+ # Filter if issue not found
67
+ if issue_num not in issue_map:
68
+ continue
69
+
70
+ # Filter if title is empty
71
+ issue = issue_map[issue_num]
72
+ if not issue.title or not issue.title.strip():
73
+ continue
74
+
75
+ filtered_runs.append(run)
76
+
77
+ runs = filtered_runs
78
+
79
+ # Show message if ALL runs filtered
80
+ if not runs:
81
+ user_output("No runs with plans found. Use --show-legacy to see all runs.")
82
+ return
83
+
84
+ # Extract location from first issue URL (needed for API calls and links)
85
+ location = None
86
+ if issues:
87
+ location = github_repo_location_from_url(repo.root, issues[0].url)
88
+
89
+ # 4. Batch fetch PRs linked to issues
90
+ pr_linkages: dict[int, list] = {}
91
+ if issue_numbers and location is not None:
92
+ pr_linkages = ctx.github.get_prs_linked_to_issues(location, issue_numbers)
93
+
94
+ # Determine use_graphite for URL selection
95
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
96
+
97
+ # 5. Build table
98
+ table = Table(show_header=True, header_style="bold")
99
+ table.add_column("run-id", style="cyan", no_wrap=True)
100
+ table.add_column("status", no_wrap=True, width=14)
101
+ table.add_column("submitted", no_wrap=True, width=11)
102
+ table.add_column("plan", no_wrap=True)
103
+ table.add_column("title", no_wrap=True)
104
+ table.add_column("pr", no_wrap=True)
105
+ table.add_column("chks", no_wrap=True)
106
+
107
+ for run in runs:
108
+ issue_num = extract_issue_number(run.display_title)
109
+
110
+ # Format run-id with link
111
+ workflow_url = None
112
+ if location is not None:
113
+ workflow_url = f"https://github.com/{location.repo_id.owner}/{location.repo_id.repo}/actions/runs/{run.run_id}"
114
+ run_id_cell = format_workflow_run_id(run, workflow_url)
115
+
116
+ # Format status
117
+ status_cell = format_workflow_outcome(run)
118
+
119
+ # Format submission time
120
+ submitted_cell = format_submission_time(run.created_at)
121
+
122
+ # Handle legacy runs where we can't parse the issue number
123
+ # Show "X" to indicate "can't parse" vs "-" for "no data"
124
+ if issue_num is None:
125
+ # Legacy format - can't extract issue linkage
126
+ plan_cell = "[dim]X[/dim]"
127
+ title_cell = "[dim]X[/dim]"
128
+ pr_cell = "[dim]X[/dim]"
129
+ checks_cell = "[dim]X[/dim]"
130
+ else:
131
+ # New format - have issue number, try to get data
132
+ issue_url = None
133
+ if location is not None:
134
+ issue_url = f"https://github.com/{location.repo_id.owner}/{location.repo_id.repo}/issues/{issue_num}"
135
+ # Make plan number clickable
136
+ if issue_url:
137
+ plan_cell = f"[link={issue_url}][cyan]#{issue_num}[/cyan][/link]"
138
+ else:
139
+ plan_cell = f"[cyan]#{issue_num}[/cyan]"
140
+
141
+ # Get title from issue map
142
+ if issue_num in issue_map:
143
+ issue = issue_map[issue_num]
144
+
145
+ title = issue.title
146
+ # Truncate to 50 characters
147
+ if len(title) > 50:
148
+ title = title[:47] + "..."
149
+ title_cell = title
150
+ else:
151
+ title_cell = "[dim]-[/dim]"
152
+
153
+ # Format PR column
154
+ pr_cell = "-"
155
+ checks_cell = "-"
156
+ if issue_num in pr_linkages:
157
+ prs = pr_linkages[issue_num]
158
+ selected_pr = select_display_pr(prs)
159
+ if selected_pr is not None:
160
+ graphite_url = ctx.graphite.get_graphite_url(
161
+ GitHubRepoId(selected_pr.owner, selected_pr.repo), selected_pr.number
162
+ )
163
+ pr_cell = format_pr_cell(
164
+ selected_pr, use_graphite=use_graphite, graphite_url=graphite_url
165
+ )
166
+ checks_cell = format_checks_cell(selected_pr)
167
+
168
+ table.add_row(
169
+ run_id_cell,
170
+ status_cell,
171
+ submitted_cell,
172
+ plan_cell,
173
+ title_cell,
174
+ pr_cell,
175
+ checks_cell,
176
+ )
177
+
178
+ # Output table to stderr (consistent with user_output convention)
179
+ console = Console(stderr=True, width=200, force_terminal=True)
180
+ console.print(table)
181
+ console.print() # Add blank line after table
182
+
183
+
184
+ @click.command("list")
185
+ @click.option("--show-legacy", is_flag=True, help="Show all runs including legacy runs.")
186
+ @click.pass_obj
187
+ def list_runs(ctx: ErkContext, show_legacy: bool) -> None:
188
+ """List GitHub Actions workflow runs for plan implementations."""
189
+ _list_runs(ctx, show_legacy)
@@ -0,0 +1,54 @@
1
+ """View workflow run logs command."""
2
+
3
+ import click
4
+
5
+ from erk.cli.core import discover_repo_context
6
+ from erk.cli.ensure import Ensure
7
+ from erk.core.context import ErkContext
8
+ from erk_shared.output.output import user_output
9
+
10
+
11
+ @click.command("logs")
12
+ @click.argument("run_id", required=False)
13
+ @click.pass_obj
14
+ def logs_run(ctx: ErkContext, run_id: str | None) -> None:
15
+ """View logs for a workflow run.
16
+
17
+ If RUN_ID is not provided, shows logs for the most recent run
18
+ on the current branch.
19
+ """
20
+ # Validate preconditions upfront (LBYL)
21
+ Ensure.gh_authenticated(ctx)
22
+
23
+ # Discover repository context
24
+ repo = discover_repo_context(ctx, ctx.cwd)
25
+
26
+ if run_id is None:
27
+ # Auto-detect: find most recent run for current branch
28
+ current_branch = Ensure.not_none(
29
+ ctx.git.get_current_branch(ctx.cwd), "Could not determine current branch"
30
+ )
31
+
32
+ runs = ctx.github.list_workflow_runs(repo.root, "implement-plan.yml", limit=50)
33
+ branch_runs = [r for r in runs if r.branch == current_branch]
34
+
35
+ if not branch_runs:
36
+ user_output(
37
+ f"No workflow runs found for branch: {click.style(current_branch, fg='yellow')}"
38
+ )
39
+ raise SystemExit(1)
40
+
41
+ # Most recent is first (list_workflow_runs returns newest first)
42
+ run_id = branch_runs[0].run_id
43
+ user_output(
44
+ f"Showing logs for run {click.style(run_id, fg='cyan')} "
45
+ f"on branch {click.style(current_branch, fg='yellow')}\n"
46
+ )
47
+
48
+ try:
49
+ log_output = ctx.github.get_run_logs(repo.root, run_id)
50
+ # Direct output - logs go to stdout for piping
51
+ click.echo(log_output)
52
+ except RuntimeError as e:
53
+ click.echo(click.style("Error: ", fg="red") + str(e), err=True)
54
+ raise SystemExit(1) from None
@@ -0,0 +1,19 @@
1
+ """Shared utilities for run commands."""
2
+
3
+
4
+ def extract_issue_number(display_title: str | None) -> int | None:
5
+ """Extract issue number from display_title format '123:abc456'.
6
+
7
+ Handles:
8
+ - New format: "123:abc456" → 123
9
+ - Old format: "Issue title [abc123]" → None (no colon at start)
10
+ - None or empty → None
11
+ """
12
+ if not display_title or ":" not in display_title:
13
+ return None
14
+ parts = display_title.split(":", 1)
15
+ # Validate that the first part is a number
16
+ first_part = parts[0].strip()
17
+ if not first_part.isdigit():
18
+ return None
19
+ return int(first_part)
@@ -0,0 +1,29 @@
1
+ import click
2
+
3
+ from erk.cli.shell_integration.handler import (
4
+ PASSTHROUGH_MARKER,
5
+ ShellIntegrationResult,
6
+ handle_shell_request,
7
+ )
8
+ from erk_shared.output.output import machine_output
9
+
10
+
11
+ @click.command(
12
+ "__shell",
13
+ hidden=True,
14
+ add_help_option=False,
15
+ context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
16
+ )
17
+ @click.argument("args", nargs=-1, type=click.UNPROCESSED)
18
+ def hidden_shell_cmd(args: tuple[str, ...]) -> None:
19
+ """Unified entry point for shell integration wrappers."""
20
+ result: ShellIntegrationResult = handle_shell_request(args)
21
+
22
+ if result.passthrough:
23
+ machine_output(PASSTHROUGH_MARKER)
24
+ raise SystemExit(result.exit_code)
25
+
26
+ if result.script:
27
+ machine_output(result.script, nl=False)
28
+
29
+ raise SystemExit(result.exit_code)
@@ -0,0 +1,23 @@
1
+ """Slot infrastructure management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.alias import register_with_aliases
6
+ from erk.cli.commands.slot.check_cmd import slot_check
7
+ from erk.cli.commands.slot.init_pool_cmd import slot_init_pool
8
+ from erk.cli.commands.slot.list_cmd import slot_list
9
+ from erk.cli.commands.slot.repair_cmd import slot_repair
10
+ from erk.cli.help_formatter import ErkCommandGroup
11
+
12
+
13
+ @click.group("slot", cls=ErkCommandGroup, grouped=False)
14
+ def slot_group() -> None:
15
+ """Manage worktree pool slots (infrastructure only)."""
16
+ pass
17
+
18
+
19
+ # Register subcommands
20
+ slot_group.add_command(slot_check)
21
+ slot_group.add_command(slot_init_pool)
22
+ slot_group.add_command(slot_repair)
23
+ register_with_aliases(slot_group, slot_list)