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,89 @@
1
+ import click
2
+
3
+ from erk.core.context import ErkContext
4
+ from erk_shared.output.output import machine_output
5
+
6
+
7
+ @click.group("completion")
8
+ def completion_group() -> None:
9
+ """Generate shell completion scripts."""
10
+
11
+
12
+ @completion_group.command("bash")
13
+ @click.pass_obj
14
+ def completion_bash(ctx: ErkContext) -> None:
15
+ """Generate bash completion script.
16
+
17
+ \b
18
+ For automatic setup of both completion and auto-activation:
19
+ erk init --shell
20
+
21
+ \b
22
+ To load completions in your current shell session:
23
+ source <(erk completion bash)
24
+
25
+ \b
26
+ To load completions permanently, add to your ~/.bashrc:
27
+ echo 'source <(erk completion bash)' >> ~/.bashrc
28
+
29
+ \b
30
+ Alternatively, you can save the completion script to bash_completion.d:
31
+ erk completion bash > /usr/local/etc/bash_completion.d/erk
32
+
33
+ \b
34
+ You will need to start a new shell for this setup to take effect.
35
+ """
36
+ script = ctx.completion.generate_bash()
37
+ machine_output(script, nl=False)
38
+
39
+
40
+ @completion_group.command("zsh")
41
+ @click.pass_obj
42
+ def completion_zsh(ctx: ErkContext) -> None:
43
+ """Generate zsh completion script.
44
+
45
+ \b
46
+ For automatic setup of both completion and auto-activation:
47
+ erk init --shell
48
+
49
+ \b
50
+ To load completions in your current shell session:
51
+ source <(erk completion zsh)
52
+
53
+ \b
54
+ To load completions permanently, add to your ~/.zshrc:
55
+ echo 'source <(erk completion zsh)' >> ~/.zshrc
56
+
57
+ \b
58
+ Note: Make sure compinit is called in your ~/.zshrc after loading completions.
59
+
60
+ \b
61
+ You will need to start a new shell for this setup to take effect.
62
+ """
63
+ script = ctx.completion.generate_zsh()
64
+ machine_output(script, nl=False)
65
+
66
+
67
+ @completion_group.command("fish")
68
+ @click.pass_obj
69
+ def completion_fish(ctx: ErkContext) -> None:
70
+ """Generate fish completion script.
71
+
72
+ \b
73
+ For automatic setup of both completion and auto-activation:
74
+ erk init --shell
75
+
76
+ \b
77
+ To load completions in your current shell session:
78
+ erk completion fish | source
79
+
80
+ \b
81
+ To load completions permanently:
82
+ mkdir -p ~/.config/fish/completions && \\
83
+ erk completion fish > ~/.config/fish/completions/erk.fish
84
+
85
+ \b
86
+ You will need to start a new shell for this setup to take effect.
87
+ """
88
+ script = ctx.completion.generate_fish()
89
+ machine_output(script, nl=False)
@@ -0,0 +1,165 @@
1
+ """Shell completion functions for CLI commands.
2
+
3
+ Separated from navigation_helpers to avoid circular imports.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from collections.abc import Generator
10
+ from contextlib import contextmanager
11
+ from typing import TYPE_CHECKING
12
+
13
+ import click
14
+
15
+ from erk.cli.core import discover_repo_context
16
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ if TYPE_CHECKING:
21
+ from erk.core.context import ErkContext
22
+
23
+
24
+ @contextmanager
25
+ def shell_completion_context(ctx: click.Context) -> Generator[ErkContext]:
26
+ """Context manager for shell completion that provides ErkContext with error handling.
27
+
28
+ Combines context extraction with graceful error handling for shell completion.
29
+ Suppresses all exceptions for graceful degradation.
30
+
31
+ Why this is needed:
32
+ - Shell completion runs in the user's interactive shell session
33
+ - Any uncaught exception would break the shell experience with a Python traceback
34
+ - Click's shell completion protocol expects functions to return empty lists on error
35
+ - This allows tab-completion to fail gracefully without disrupting the user
36
+
37
+ Why we create ErkContext if ctx.obj is None:
38
+ - Click's shell completion runs with resilient_parsing=True
39
+ - This mode skips command callbacks, so the cli() callback that creates ctx.obj never runs
40
+ - We must create a fresh ErkContext to provide completion data
41
+
42
+ Usage:
43
+ with shell_completion_context(ctx) as erk_ctx:
44
+ # ... completion logic
45
+ return completion_candidates
46
+ return [] # Fallback if exception
47
+
48
+ Reference:
49
+ Click's shell completion protocol:
50
+ https://click.palletsprojects.com/en/stable/shell-completion/
51
+ """
52
+ try:
53
+ root_ctx = ctx.find_root()
54
+ erk_ctx = root_ctx.obj
55
+
56
+ # Click's resilient_parsing mode skips callbacks, so ctx.obj may be None
57
+ if erk_ctx is None:
58
+ from erk.core.context import create_context
59
+
60
+ erk_ctx = create_context(dry_run=False)
61
+
62
+ yield erk_ctx
63
+ except Exception:
64
+ # Suppress exceptions for graceful degradation, but log for debugging
65
+ # Shell completion should never break the user's shell experience
66
+ logger.debug("Shell completion error", exc_info=True)
67
+
68
+
69
+ def complete_worktree_names(
70
+ ctx: click.Context, param: click.Parameter | None, incomplete: str
71
+ ) -> list[str]:
72
+ """Shell completion for worktree names. Includes 'root' for the repository root.
73
+
74
+ Args:
75
+ ctx: Click context
76
+ param: Click parameter (unused, but required by Click's completion protocol)
77
+ incomplete: Partial input string to complete
78
+ """
79
+ with shell_completion_context(ctx) as erk_ctx:
80
+ repo = discover_repo_context(erk_ctx, erk_ctx.cwd)
81
+ ensure_erk_metadata_dir(repo)
82
+
83
+ names = ["root"] if "root".startswith(incomplete) else []
84
+
85
+ # Get worktree names from git_ops instead of filesystem iteration
86
+ worktrees = erk_ctx.git.list_worktrees(repo.root)
87
+ for wt in worktrees:
88
+ if wt.is_root:
89
+ continue # Skip root worktree (already added as "root")
90
+ worktree_name = wt.path.name
91
+ if worktree_name.startswith(incomplete):
92
+ names.append(worktree_name)
93
+
94
+ return names
95
+ return []
96
+
97
+
98
+ def complete_branch_names(
99
+ ctx: click.Context, param: click.Parameter | None, incomplete: str
100
+ ) -> list[str]:
101
+ """Shell completion for branch names. Includes both local and remote branches.
102
+
103
+ Remote branch names have their remote prefix stripped
104
+ (e.g., 'origin/feature' becomes 'feature').
105
+ Duplicates are removed if a branch exists both locally and remotely.
106
+
107
+ Args:
108
+ ctx: Click context
109
+ param: Click parameter (unused, but required by Click's completion protocol)
110
+ incomplete: Partial input string to complete
111
+ """
112
+ with shell_completion_context(ctx) as erk_ctx:
113
+ repo = discover_repo_context(erk_ctx, erk_ctx.cwd)
114
+ ensure_erk_metadata_dir(repo)
115
+
116
+ # Collect all branch names in a set for deduplication
117
+ branch_names = set()
118
+
119
+ # Add local branches
120
+ local_branches = erk_ctx.git.list_local_branches(repo.root)
121
+ branch_names.update(local_branches)
122
+
123
+ # Add remote branches with prefix stripped
124
+ remote_branches = erk_ctx.git.list_remote_branches(repo.root)
125
+ for remote_branch in remote_branches:
126
+ # Strip remote prefix (e.g., 'origin/feature' -> 'feature')
127
+ if "/" in remote_branch:
128
+ _, branch_name = remote_branch.split("/", 1)
129
+ branch_names.add(branch_name)
130
+ else:
131
+ # Fallback: if no slash, use as-is
132
+ branch_names.add(remote_branch)
133
+
134
+ # Filter by incomplete prefix and return sorted list
135
+ matching_branches = [name for name in branch_names if name.startswith(incomplete)]
136
+ return sorted(matching_branches)
137
+ return []
138
+
139
+
140
+ def complete_plan_files(
141
+ ctx: click.Context, param: click.Parameter | None, incomplete: str
142
+ ) -> list[str]:
143
+ """Shell completion for plan files (markdown files in current directory).
144
+
145
+ Args:
146
+ ctx: Click context
147
+ param: Click parameter (unused, but required by Click's completion protocol)
148
+ incomplete: Partial input string to complete
149
+
150
+ Returns:
151
+ List of completion candidates (filenames matching incomplete text)
152
+ """
153
+ with shell_completion_context(ctx) as erk_ctx:
154
+ # Get current working directory from erk context
155
+ cwd = erk_ctx.cwd
156
+
157
+ # Find all .md files in current directory
158
+ candidates = []
159
+ for md_file in cwd.glob("*.md"):
160
+ # Filter by incomplete prefix if provided
161
+ if md_file.name.startswith(incomplete):
162
+ candidates.append(md_file.name)
163
+
164
+ return sorted(candidates)
165
+ return []
@@ -0,0 +1,327 @@
1
+ import subprocess
2
+ from collections.abc import MutableMapping
3
+ from dataclasses import replace
4
+ from functools import cache
5
+ from pathlib import Path
6
+ from typing import Any, cast
7
+
8
+ import click
9
+ import tomlkit
10
+
11
+ from erk.cli.commands.slot.common import DEFAULT_POOL_SIZE
12
+ from erk.cli.config import LoadedConfig
13
+ from erk.cli.core import discover_repo_context
14
+ from erk.cli.ensure import Ensure
15
+ from erk.core.context import ErkContext, write_trunk_to_pyproject
16
+ from erk_shared.output.output import machine_output, user_output
17
+
18
+
19
+ @cache
20
+ def get_global_config_keys() -> dict[str, str]:
21
+ """Get user-exposed global config keys with descriptions.
22
+
23
+ Order determines display order in 'erk config list'.
24
+ shell_setup_complete is internal and not exposed.
25
+ """
26
+ return {
27
+ "erk_root": "Root directory for erk data (~/.erk by default)",
28
+ "use_graphite": "Enable Graphite integration for stack management",
29
+ "show_pr_info": "Show PR status in branch listings",
30
+ "github_planning": "Enable GitHub issues integration for planning",
31
+ "fix_conflicts_require_dangerous_flag": "Require --dangerous flag for fix-conflicts",
32
+ "show_hidden_commands": "Show deprecated/hidden commands in help output",
33
+ }
34
+
35
+
36
+ def _get_env_value(cfg: LoadedConfig, parts: list[str], key: str) -> None:
37
+ """Handle env.* configuration keys.
38
+
39
+ Prints the value or exits with error if key not found.
40
+ """
41
+ Ensure.invariant(len(parts) == 2, f"Invalid key: {key}")
42
+ Ensure.invariant(parts[1] in cfg.env, f"Key not found: {key}")
43
+
44
+ machine_output(cfg.env[parts[1]])
45
+
46
+
47
+ def _get_post_create_value(cfg: LoadedConfig, parts: list[str], key: str) -> None:
48
+ """Handle post_create.* configuration keys.
49
+
50
+ Prints the value or exits with error if key not found.
51
+ """
52
+ Ensure.invariant(len(parts) == 2, f"Invalid key: {key}")
53
+
54
+ # Handle shell subkey
55
+ if parts[1] == "shell":
56
+ Ensure.truthy(cfg.post_create_shell, f"Key not found: {key}")
57
+ machine_output(cfg.post_create_shell)
58
+ return
59
+
60
+ # Handle commands subkey
61
+ if parts[1] == "commands":
62
+ for cmd in cfg.post_create_commands:
63
+ machine_output(cmd)
64
+ return
65
+
66
+ # Unknown subkey
67
+ Ensure.invariant(False, f"Key not found: {key}")
68
+
69
+
70
+ def _write_pool_max_slots(repo_root: Path, max_slots: int) -> None:
71
+ """Write pool.max_slots to .erk/config.toml.
72
+
73
+ Creates or updates the [pool] section with max_slots setting.
74
+ Preserves existing formatting and comments using tomlkit.
75
+
76
+ Args:
77
+ repo_root: Path to the repository root directory
78
+ max_slots: Maximum number of pool slots to configure
79
+ """
80
+ config_dir = repo_root / ".erk"
81
+ config_path = config_dir / "config.toml"
82
+
83
+ # Ensure .erk directory exists
84
+ if not config_dir.exists():
85
+ config_dir.mkdir(parents=True)
86
+
87
+ # Load existing file or create new document
88
+ if config_path.exists():
89
+ with config_path.open("r", encoding="utf-8") as f:
90
+ doc = tomlkit.load(f)
91
+ else:
92
+ doc = tomlkit.document()
93
+
94
+ # Ensure [pool] section exists
95
+ if "pool" not in doc:
96
+ assert isinstance(doc, MutableMapping), f"Expected MutableMapping, got {type(doc)}"
97
+ cast(dict[str, Any], doc)["pool"] = tomlkit.table()
98
+
99
+ # Set max_slots value
100
+ pool_section = doc["pool"]
101
+ assert isinstance(pool_section, MutableMapping), type(pool_section)
102
+ cast(dict[str, Any], pool_section)["max_slots"] = max_slots
103
+
104
+ # Write back to file
105
+ with config_path.open("w", encoding="utf-8") as f:
106
+ tomlkit.dump(doc, f)
107
+
108
+
109
+ @click.group("config")
110
+ def config_group() -> None:
111
+ """Manage erk configuration."""
112
+
113
+
114
+ @config_group.command("keys")
115
+ def config_keys() -> None:
116
+ """List all available configuration keys with descriptions."""
117
+ formatter = click.HelpFormatter()
118
+
119
+ # Global config section
120
+ user_output(click.style("Global configuration keys:", bold=True))
121
+ rows = list(get_global_config_keys().items())
122
+ formatter.write_dl(rows)
123
+ user_output(formatter.getvalue().rstrip())
124
+
125
+ user_output("")
126
+
127
+ # Repository config section
128
+ user_output(click.style("Repository configuration keys:", bold=True))
129
+ formatter = click.HelpFormatter()
130
+ repo_keys = [
131
+ ("trunk-branch", "The main/master branch name for the repository"),
132
+ ("pool.max_slots", "Maximum number of pool slots for worktree pool"),
133
+ ("env.<name>", "Environment variables to set in worktrees"),
134
+ ("post_create.shell", "Shell to use for post-create commands"),
135
+ ("post_create.commands", "Commands to run after creating a worktree"),
136
+ ]
137
+ formatter.write_dl(repo_keys)
138
+ user_output(formatter.getvalue().rstrip())
139
+
140
+
141
+ def _format_config_value(value: object) -> str:
142
+ """Format a config value for display."""
143
+ if isinstance(value, bool):
144
+ return str(value).lower()
145
+ return str(value)
146
+
147
+
148
+ @config_group.command("list")
149
+ @click.pass_obj
150
+ def config_list(ctx: ErkContext) -> None:
151
+ """Print a list of configuration keys and values."""
152
+ # Display global config
153
+ user_output(click.style("Global configuration:", bold=True))
154
+ if ctx.global_config:
155
+ for key in get_global_config_keys():
156
+ value = getattr(ctx.global_config, key)
157
+ user_output(f" {key}={_format_config_value(value)}")
158
+ else:
159
+ user_output(" (not configured - run 'erk init' to create)")
160
+
161
+ # Display local config
162
+ user_output(click.style("\nRepository configuration:", bold=True))
163
+ from erk.core.repo_discovery import NoRepoSentinel
164
+
165
+ if isinstance(ctx.repo, NoRepoSentinel):
166
+ user_output(" (not in a git repository)")
167
+ else:
168
+ trunk_branch = ctx.trunk_branch
169
+ cfg = ctx.local_config
170
+ if trunk_branch:
171
+ user_output(f" trunk-branch={trunk_branch}")
172
+ if cfg.pool_size is not None:
173
+ user_output(f" pool.max_slots={cfg.pool_size}")
174
+ else:
175
+ user_output(f" pool.max_slots={DEFAULT_POOL_SIZE} (default)")
176
+ if cfg.env:
177
+ for key, value in cfg.env.items():
178
+ user_output(f" env.{key}={value}")
179
+ if cfg.post_create_shell:
180
+ user_output(f" post_create.shell={cfg.post_create_shell}")
181
+ if cfg.post_create_commands:
182
+ user_output(f" post_create.commands={cfg.post_create_commands}")
183
+
184
+ has_no_custom_config = (
185
+ not trunk_branch
186
+ and cfg.pool_size is None
187
+ and not cfg.env
188
+ and not cfg.post_create_shell
189
+ and not cfg.post_create_commands
190
+ )
191
+ if has_no_custom_config:
192
+ user_output(" (no custom configuration - run 'erk init' to create)")
193
+
194
+
195
+ @config_group.command("get")
196
+ @click.argument("key", metavar="KEY")
197
+ @click.pass_obj
198
+ def config_get(ctx: ErkContext, key: str) -> None:
199
+ """Print the value of a given configuration key."""
200
+ parts = key.split(".")
201
+
202
+ # Handle global config keys
203
+ if parts[0] in get_global_config_keys():
204
+ global_config = Ensure.not_none(
205
+ ctx.global_config, f"Global config not found at {ctx.erk_installation.config_path()}"
206
+ )
207
+ value = getattr(global_config, parts[0])
208
+ machine_output(_format_config_value(value))
209
+ return
210
+
211
+ # Handle repo config keys
212
+ from erk.core.repo_discovery import NoRepoSentinel
213
+
214
+ if isinstance(ctx.repo, NoRepoSentinel):
215
+ user_output("Not in a git repository")
216
+ raise SystemExit(1)
217
+
218
+ if parts[0] == "trunk-branch":
219
+ trunk_branch = ctx.trunk_branch
220
+ if trunk_branch:
221
+ machine_output(trunk_branch)
222
+ else:
223
+ user_output("not configured (will auto-detect)")
224
+ return
225
+
226
+ cfg = ctx.local_config
227
+
228
+ if parts[0] == "env":
229
+ _get_env_value(cfg, parts, key)
230
+ return
231
+
232
+ if parts[0] == "post_create":
233
+ _get_post_create_value(cfg, parts, key)
234
+ return
235
+
236
+ if parts[0] == "pool" and len(parts) == 2 and parts[1] == "max_slots":
237
+ if cfg.pool_size is not None:
238
+ machine_output(str(cfg.pool_size))
239
+ else:
240
+ machine_output(f"{DEFAULT_POOL_SIZE} (default)")
241
+ return
242
+
243
+ user_output(f"Invalid key: {key}")
244
+ raise SystemExit(1)
245
+
246
+
247
+ def _parse_config_value(key: str, value: str, current_type: type) -> object:
248
+ """Parse a string value to the appropriate type for a config key."""
249
+ if current_type is bool:
250
+ if value.lower() not in ("true", "false"):
251
+ user_output(f"Invalid boolean value: {value}")
252
+ raise SystemExit(1)
253
+ return value.lower() == "true"
254
+ if current_type is Path or key == "erk_root":
255
+ return Path(value).expanduser().resolve()
256
+ return value
257
+
258
+
259
+ @config_group.command("set")
260
+ @click.argument("key", metavar="KEY")
261
+ @click.argument("value", metavar="VALUE")
262
+ @click.pass_obj
263
+ def config_set(ctx: ErkContext, key: str, value: str) -> None:
264
+ """Update configuration with a value for the given key."""
265
+ # Parse key into parts
266
+ parts = key.split(".")
267
+
268
+ # Handle global config keys
269
+ if parts[0] in get_global_config_keys():
270
+ config_path = ctx.erk_installation.config_path()
271
+ global_config = Ensure.not_none(
272
+ ctx.global_config,
273
+ f"Global config not found at {config_path}. Run 'erk init' to create it.",
274
+ )
275
+
276
+ # Get current value's type and parse new value
277
+ current_value = getattr(global_config, parts[0])
278
+ parsed_value = _parse_config_value(parts[0], value, type(current_value))
279
+
280
+ # Create new config with updated value using dataclasses.replace
281
+ new_config = replace(global_config, **{parts[0]: parsed_value})
282
+
283
+ ctx.erk_installation.save_config(new_config)
284
+ user_output(f"Set {key}={value}")
285
+ return
286
+
287
+ # Handle repo config keys
288
+ if parts[0] == "trunk-branch":
289
+ # discover_repo_context checks for git repository and raises FileNotFoundError
290
+ repo = discover_repo_context(ctx, Path.cwd())
291
+
292
+ # Validate that the branch exists before writing
293
+ result = subprocess.run(
294
+ ["git", "rev-parse", "--verify", value],
295
+ cwd=repo.root,
296
+ capture_output=True,
297
+ text=True,
298
+ check=False,
299
+ )
300
+ Ensure.invariant(
301
+ result.returncode == 0,
302
+ f"Branch '{value}' doesn't exist in repository.\n"
303
+ f"Create the branch first before configuring it as trunk.",
304
+ )
305
+
306
+ # Write configuration
307
+ write_trunk_to_pyproject(repo.root, value)
308
+ user_output(f"Set trunk-branch={value}")
309
+ return
310
+
311
+ # Handle pool.max_slots
312
+ if parts[0] == "pool" and len(parts) == 2 and parts[1] == "max_slots":
313
+ repo = discover_repo_context(ctx, Path.cwd())
314
+
315
+ # Validate value is a positive integer
316
+ if not value.isdigit() or int(value) < 1:
317
+ user_output(f"Invalid value: {value}. pool.max_slots must be a positive integer.")
318
+ raise SystemExit(1)
319
+
320
+ pool_size = int(value)
321
+ _write_pool_max_slots(repo.root, pool_size)
322
+ user_output(f"Set pool.max_slots={pool_size}")
323
+ return
324
+
325
+ # Other repo config keys not implemented yet
326
+ user_output(f"Invalid key: {key}")
327
+ raise SystemExit(1)
@@ -0,0 +1 @@
1
+ """Agent documentation command group."""
@@ -0,0 +1,16 @@
1
+ """Agent documentation command group."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.docs.sync import sync_command
6
+ from erk.cli.commands.docs.validate import validate_command
7
+
8
+
9
+ @click.group(name="docs")
10
+ def docs_group() -> None:
11
+ """Manage and validate agent documentation."""
12
+
13
+
14
+ # Register commands
15
+ docs_group.add_command(sync_command)
16
+ docs_group.add_command(validate_command)