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
erk/core/context.py ADDED
@@ -0,0 +1,570 @@
1
+ """Application context with dependency injection.
2
+
3
+ This module provides factory functions for erk CLI context creation.
4
+ The unified ErkContext dataclass is defined in erk_shared.context and
5
+ re-exported here for backwards compatibility.
6
+ """
7
+
8
+ import shutil
9
+ from collections.abc import MutableMapping
10
+ from pathlib import Path
11
+ from typing import Any, cast
12
+
13
+ import click
14
+ import tomlkit
15
+
16
+ from erk.cli.config import load_config
17
+ from erk.core.claude_executor import RealClaudeExecutor
18
+ from erk.core.completion import RealCompletion
19
+ from erk.core.implementation_queue.github.real import RealGitHubAdmin
20
+ from erk.core.planner.registry_real import RealPlannerRegistry
21
+ from erk.core.repo_discovery import discover_repo_or_sentinel, ensure_erk_metadata_dir
22
+ from erk.core.script_writer import RealScriptWriter
23
+ from erk.core.services.plan_list_service import RealPlanListService
24
+ from erk.core.shell import RealShell
25
+
26
+ # Re-export ErkContext from erk_shared for isinstance() compatibility
27
+ # This ensures that both erk CLI and kit commands use the same class identity
28
+ from erk_shared.context.context import ErkContext as ErkContext
29
+
30
+ # Re-export types from erk_shared.context
31
+ from erk_shared.context.types import GlobalConfig as GlobalConfig
32
+ from erk_shared.context.types import LoadedConfig as LoadedConfig
33
+ from erk_shared.context.types import NoRepoSentinel as NoRepoSentinel
34
+ from erk_shared.context.types import RepoContext as RepoContext
35
+
36
+ # Import ABCs and fakes from erk_shared.core
37
+ from erk_shared.core import (
38
+ ClaudeExecutor,
39
+ FakePlanListService,
40
+ PlanListService,
41
+ PlannerRegistry,
42
+ ScriptWriter,
43
+ )
44
+ from erk_shared.extraction.claude_installation import ClaudeInstallation
45
+ from erk_shared.gateway.claude_settings.abc import ClaudeSettingsStore
46
+ from erk_shared.gateway.claude_settings.real import RealClaudeSettingsStore
47
+
48
+ # Import erk-specific integrations
49
+ from erk_shared.gateway.completion import Completion
50
+ from erk_shared.gateway.erk_installation.abc import ErkInstallation
51
+ from erk_shared.gateway.erk_installation.real import RealErkInstallation
52
+ from erk_shared.gateway.feedback import InteractiveFeedback, SuppressedFeedback, UserFeedback
53
+ from erk_shared.gateway.graphite.abc import Graphite
54
+ from erk_shared.gateway.graphite.disabled import (
55
+ GraphiteDisabled,
56
+ GraphiteDisabledReason,
57
+ )
58
+ from erk_shared.gateway.graphite.dry_run import DryRunGraphite
59
+ from erk_shared.gateway.graphite.real import RealGraphite
60
+ from erk_shared.gateway.shell import Shell
61
+ from erk_shared.gateway.time.abc import Time
62
+ from erk_shared.gateway.time.real import RealTime
63
+ from erk_shared.git.abc import Git
64
+ from erk_shared.git.dry_run import DryRunGit
65
+ from erk_shared.git.real import RealGit
66
+ from erk_shared.github.abc import GitHub
67
+ from erk_shared.github.dry_run import DryRunGitHub
68
+ from erk_shared.github.issues import DryRunGitHubIssues, GitHubIssues, RealGitHubIssues
69
+ from erk_shared.github.parsing import parse_git_remote_url
70
+ from erk_shared.github.real import RealGitHub
71
+ from erk_shared.github.types import RepoInfo
72
+ from erk_shared.github_admin.abc import GitHubAdmin
73
+ from erk_shared.output.output import user_output
74
+ from erk_shared.plan_store.github import GitHubPlanStore
75
+ from erk_shared.plan_store.store import PlanStore
76
+ from erk_shared.prompt_executor import PromptExecutor
77
+ from erk_shared.prompt_executor.real import RealPromptExecutor
78
+
79
+
80
+ def minimal_context(git: Git, cwd: Path, dry_run: bool = False) -> ErkContext:
81
+ """Create minimal context with only git configured, rest are test defaults.
82
+
83
+ Useful for simple tests that only need git operations. Other integration
84
+ classes are initialized with their standard test defaults (fake implementations).
85
+
86
+ Args:
87
+ git: The Git implementation (usually FakeGit with test configuration)
88
+ cwd: Current working directory path for the context
89
+ dry_run: Whether to enable dry-run mode (default False)
90
+
91
+ Returns:
92
+ ErkContext with git configured and other dependencies using test defaults
93
+
94
+ Note:
95
+ For more complex test setup with custom configs or multiple integration classes,
96
+ use context_for_test() instead.
97
+ """
98
+ from tests.fakes.claude_executor import FakeClaudeExecutor
99
+ from tests.fakes.script_writer import FakeScriptWriter
100
+
101
+ from erk.core.planner.registry_fake import FakePlannerRegistry
102
+ from erk_shared.extraction.claude_installation import FakeClaudeInstallation
103
+ from erk_shared.gateway.claude_settings.fake import FakeClaudeSettingsStore
104
+ from erk_shared.gateway.completion import FakeCompletion
105
+ from erk_shared.gateway.erk_installation.fake import FakeErkInstallation
106
+ from erk_shared.gateway.feedback import FakeUserFeedback
107
+ from erk_shared.gateway.graphite.fake import FakeGraphite
108
+ from erk_shared.gateway.shell import FakeShell
109
+ from erk_shared.gateway.time.fake import FakeTime
110
+ from erk_shared.github.fake import FakeGitHub
111
+ from erk_shared.github.issues import FakeGitHubIssues
112
+ from erk_shared.github_admin.fake import FakeGitHubAdmin
113
+ from erk_shared.prompt_executor.fake import FakePromptExecutor
114
+
115
+ fake_github = FakeGitHub()
116
+ fake_issues = FakeGitHubIssues()
117
+ fake_graphite = FakeGraphite()
118
+ fake_time = FakeTime()
119
+ return ErkContext(
120
+ git=git,
121
+ github=fake_github,
122
+ github_admin=FakeGitHubAdmin(),
123
+ issues=fake_issues,
124
+ plan_store=GitHubPlanStore(fake_issues, fake_time),
125
+ graphite=fake_graphite,
126
+ shell=FakeShell(),
127
+ claude_executor=FakeClaudeExecutor(),
128
+ completion=FakeCompletion(),
129
+ time=fake_time,
130
+ erk_installation=FakeErkInstallation(),
131
+ script_writer=FakeScriptWriter(),
132
+ feedback=FakeUserFeedback(),
133
+ plan_list_service=FakePlanListService(),
134
+ planner_registry=FakePlannerRegistry(),
135
+ claude_installation=FakeClaudeInstallation.for_test(),
136
+ prompt_executor=FakePromptExecutor(),
137
+ claude_settings_store=FakeClaudeSettingsStore(),
138
+ cwd=cwd,
139
+ global_config=None,
140
+ local_config=LoadedConfig.test(),
141
+ repo=NoRepoSentinel(),
142
+ repo_info=None,
143
+ dry_run=dry_run,
144
+ debug=False,
145
+ )
146
+
147
+
148
+ def context_for_test(
149
+ git: Git | None = None,
150
+ github: GitHub | None = None,
151
+ github_admin: GitHubAdmin | None = None,
152
+ issues: GitHubIssues | None = None,
153
+ plan_store: PlanStore | None = None,
154
+ graphite: Graphite | None = None,
155
+ shell: Shell | None = None,
156
+ claude_executor: ClaudeExecutor | None = None,
157
+ completion: Completion | None = None,
158
+ time: Time | None = None,
159
+ erk_installation: ErkInstallation | None = None,
160
+ script_writer: ScriptWriter | None = None,
161
+ feedback: UserFeedback | None = None,
162
+ plan_list_service: PlanListService | None = None,
163
+ planner_registry: PlannerRegistry | None = None,
164
+ claude_installation: ClaudeInstallation | None = None,
165
+ prompt_executor: PromptExecutor | None = None,
166
+ cwd: Path | None = None,
167
+ global_config: GlobalConfig | None = None,
168
+ local_config: LoadedConfig | None = None,
169
+ repo: RepoContext | NoRepoSentinel | None = None,
170
+ repo_info: RepoInfo | None = None,
171
+ dry_run: bool = False,
172
+ debug: bool = False,
173
+ ) -> ErkContext:
174
+ """Create test context with optional pre-configured integration classes.
175
+
176
+ Provides full control over all context parameters with sensible test defaults
177
+ for any unspecified values. Use this for complex test scenarios that need
178
+ specific configurations for multiple integration classes.
179
+
180
+ Args:
181
+ git: Optional Git implementation. If None, creates empty FakeGit.
182
+ github: Optional GitHub implementation. If None, creates empty FakeGitHub.
183
+ issues: Optional GitHubIssues implementation.
184
+ If None, creates empty FakeGitHubIssues.
185
+ graphite: Optional Graphite implementation.
186
+ If None, creates empty FakeGraphite.
187
+ shell: Optional Shell implementation. If None, creates empty FakeShell.
188
+ completion: Optional Completion implementation.
189
+ If None, creates empty FakeCompletion.
190
+ erk_installation: Optional ErkInstallation implementation.
191
+ If None, creates FakeErkInstallation with test config.
192
+ script_writer: Optional ScriptWriter implementation.
193
+ If None, creates empty FakeScriptWriter.
194
+ feedback: Optional UserFeedback implementation.
195
+ If None, creates FakeUserFeedback.
196
+ prompt_executor: Optional PromptExecutor. If None, creates FakePromptExecutor.
197
+ cwd: Optional current working directory. If None, uses sentinel_path().
198
+ global_config: Optional GlobalConfig. If None, uses test defaults.
199
+ local_config: Optional LoadedConfig. If None, uses empty defaults.
200
+ repo: Optional RepoContext or NoRepoSentinel. If None, uses NoRepoSentinel().
201
+ repo_info: Optional RepoInfo. If None, stays None.
202
+ dry_run: Whether to enable dry-run mode (default False).
203
+ debug: Whether to enable debug mode (default False).
204
+
205
+ Returns:
206
+ ErkContext configured with provided values and test defaults
207
+ """
208
+ from tests.fakes.claude_executor import FakeClaudeExecutor
209
+ from tests.fakes.script_writer import FakeScriptWriter
210
+ from tests.test_utils.paths import sentinel_path
211
+
212
+ from erk.core.planner.registry_fake import FakePlannerRegistry
213
+ from erk_shared.extraction.claude_installation import FakeClaudeInstallation
214
+ from erk_shared.gateway.claude_settings.fake import FakeClaudeSettingsStore
215
+ from erk_shared.gateway.completion import FakeCompletion
216
+ from erk_shared.gateway.erk_installation.fake import FakeErkInstallation
217
+ from erk_shared.gateway.feedback import FakeUserFeedback
218
+ from erk_shared.gateway.graphite.dry_run import DryRunGraphite
219
+ from erk_shared.gateway.graphite.fake import FakeGraphite
220
+ from erk_shared.gateway.shell import FakeShell
221
+ from erk_shared.gateway.time.fake import FakeTime
222
+ from erk_shared.git.fake import FakeGit
223
+ from erk_shared.github.fake import FakeGitHub
224
+ from erk_shared.github.issues import FakeGitHubIssues
225
+ from erk_shared.github_admin.fake import FakeGitHubAdmin
226
+ from erk_shared.prompt_executor.fake import FakePromptExecutor
227
+
228
+ if git is None:
229
+ git = FakeGit()
230
+
231
+ if github is None:
232
+ github = FakeGitHub()
233
+
234
+ if github_admin is None:
235
+ github_admin = FakeGitHubAdmin()
236
+
237
+ if issues is None:
238
+ issues = FakeGitHubIssues()
239
+
240
+ if plan_store is None:
241
+ # Always compose from issues layer - no separate FakePlanStore
242
+ # This ensures tests use the same composition as production code
243
+ plan_store = GitHubPlanStore(issues)
244
+
245
+ if graphite is None:
246
+ graphite = FakeGraphite()
247
+
248
+ if shell is None:
249
+ shell = FakeShell()
250
+
251
+ if claude_executor is None:
252
+ claude_executor = FakeClaudeExecutor()
253
+
254
+ if completion is None:
255
+ completion = FakeCompletion()
256
+
257
+ if time is None:
258
+ time = FakeTime()
259
+
260
+ if script_writer is None:
261
+ script_writer = FakeScriptWriter()
262
+
263
+ if feedback is None:
264
+ feedback = FakeUserFeedback()
265
+
266
+ if plan_list_service is None:
267
+ # If github and issues were provided, wire them up via RealPlanListService
268
+ # so that tests get realistic behavior when testing plan list functionality
269
+ plan_list_service = RealPlanListService(github, issues)
270
+
271
+ if planner_registry is None:
272
+ planner_registry = FakePlannerRegistry()
273
+
274
+ if claude_installation is None:
275
+ claude_installation = FakeClaudeInstallation.for_test()
276
+
277
+ if prompt_executor is None:
278
+ prompt_executor = FakePromptExecutor()
279
+
280
+ if global_config is None:
281
+ global_config = GlobalConfig(
282
+ erk_root=Path("/test/erks"),
283
+ use_graphite=False,
284
+ shell_setup_complete=False,
285
+ show_pr_info=True,
286
+ github_planning=True,
287
+ )
288
+
289
+ if erk_installation is None:
290
+ erk_installation = FakeErkInstallation(config=global_config)
291
+
292
+ if local_config is None:
293
+ local_config = LoadedConfig.test()
294
+
295
+ if repo is None:
296
+ repo = NoRepoSentinel()
297
+
298
+ # Apply dry-run wrappers if needed (matching production behavior)
299
+ if dry_run:
300
+ git = DryRunGit(git)
301
+ graphite = DryRunGraphite(graphite)
302
+ github = DryRunGitHub(github)
303
+ issues = DryRunGitHubIssues(issues)
304
+
305
+ return ErkContext(
306
+ git=git,
307
+ github=github,
308
+ github_admin=github_admin,
309
+ issues=issues,
310
+ plan_store=plan_store,
311
+ graphite=graphite,
312
+ shell=shell,
313
+ claude_executor=claude_executor,
314
+ completion=completion,
315
+ time=time,
316
+ erk_installation=erk_installation,
317
+ script_writer=script_writer,
318
+ feedback=feedback,
319
+ plan_list_service=plan_list_service,
320
+ planner_registry=planner_registry,
321
+ claude_installation=claude_installation,
322
+ prompt_executor=prompt_executor,
323
+ claude_settings_store=FakeClaudeSettingsStore(),
324
+ cwd=cwd or sentinel_path(),
325
+ global_config=global_config,
326
+ local_config=local_config,
327
+ repo=repo,
328
+ repo_info=repo_info,
329
+ dry_run=dry_run,
330
+ debug=debug,
331
+ )
332
+
333
+
334
+ def write_trunk_to_pyproject(repo_root: Path, trunk: str, git: Git | None = None) -> None:
335
+ """Write trunk branch configuration to pyproject.toml.
336
+
337
+ Creates or updates the [tool.erk] section with trunk_branch setting.
338
+ Preserves existing formatting and comments using tomlkit.
339
+
340
+ Args:
341
+ repo_root: Path to the repository root directory
342
+ trunk: Trunk branch name to configure
343
+ git: Optional Git interface for path checking (uses .exists() if None)
344
+ """
345
+ pyproject_path = repo_root / "pyproject.toml"
346
+
347
+ # Check existence using git if available (for test compatibility)
348
+ if git is not None:
349
+ path_exists = git.path_exists(pyproject_path)
350
+ else:
351
+ path_exists = pyproject_path.exists()
352
+
353
+ # Load existing file or create new document
354
+ if path_exists:
355
+ with pyproject_path.open("r", encoding="utf-8") as f:
356
+ doc = tomlkit.load(f)
357
+ else:
358
+ doc = tomlkit.document()
359
+
360
+ # Ensure [tool] section exists
361
+ if "tool" not in doc:
362
+ assert isinstance(doc, MutableMapping), f"Expected MutableMapping, got {type(doc)}"
363
+ cast(dict[str, Any], doc)["tool"] = tomlkit.table()
364
+
365
+ # Ensure [tool.erk] section exists
366
+ tool_section = cast(dict[str, Any], doc["tool"])
367
+ if "erk" not in tool_section:
368
+ tool_section["erk"] = tomlkit.table()
369
+
370
+ # Set trunk_branch value
371
+ cast(dict[str, Any], tool_section["erk"])["trunk_branch"] = trunk
372
+
373
+ # Write back to file
374
+ with pyproject_path.open("w", encoding="utf-8") as f:
375
+ tomlkit.dump(doc, f)
376
+
377
+
378
+ def safe_cwd() -> tuple[Path | None, str | None]:
379
+ """Get current working directory, detecting if it no longer exists.
380
+
381
+ Uses LBYL approach: checks if the operation will succeed before attempting it.
382
+
383
+ Returns:
384
+ tuple[Path | None, str | None]: (path, error_message)
385
+ - If successful: (Path, None)
386
+ - If directory deleted: (None, error_message)
387
+
388
+ Note:
389
+ This is an acceptable use of try/except since we're wrapping a third-party
390
+ API (Path.cwd()) that provides no way to check the condition first.
391
+ """
392
+ try:
393
+ cwd_path = Path.cwd()
394
+ return (cwd_path, None)
395
+ except (FileNotFoundError, OSError):
396
+ return (
397
+ None,
398
+ "Current working directory no longer exists",
399
+ )
400
+
401
+
402
+ def create_context(*, dry_run: bool, script: bool = False, debug: bool = False) -> ErkContext:
403
+ """Create production context with real implementations.
404
+
405
+ Called at CLI entry point to create the context for the entire
406
+ command execution.
407
+
408
+ Args:
409
+ dry_run: If True, wrap all dependencies with dry-run wrappers that
410
+ print intended actions without executing them
411
+ script: If True, use SuppressedFeedback to suppress diagnostic output
412
+ for shell integration mode (default False)
413
+ debug: If True, enable debug mode for error handling (default False)
414
+
415
+ Returns:
416
+ ErkContext with real implementations, wrapped in dry-run
417
+ wrappers if dry_run=True
418
+
419
+ Example:
420
+ >>> ctx = create_context(dry_run=False, script=False)
421
+ >>> worktrees = ctx.git.list_worktrees(Path("/repo"))
422
+ >>> erk_root = ctx.global_config.erk_root
423
+ """
424
+ # 1. Capture cwd (no deps)
425
+ cwd_result, error_msg = safe_cwd()
426
+ if cwd_result is None:
427
+ assert error_msg is not None
428
+ # Emit clear error and exit
429
+ user_output(click.style("Error: ", fg="red") + error_msg)
430
+ user_output("\nThe directory you're running from has been deleted.")
431
+ user_output("Please change to a valid directory and try again.")
432
+ raise SystemExit(1)
433
+
434
+ cwd = cwd_result
435
+
436
+ # 2. Create erk installation gateway
437
+ erk_installation = RealErkInstallation()
438
+
439
+ # 3. Load global config (no deps) - None if not exists (for init command)
440
+ global_config: GlobalConfig | None
441
+ if erk_installation.config_exists():
442
+ global_config = erk_installation.load_config()
443
+ else:
444
+ # For init command only: config doesn't exist yet
445
+ global_config = None
446
+
447
+ # 4. Create integration classes (need git for repo discovery)
448
+ # Create time first so it can be injected into other classes
449
+ time: Time = RealTime()
450
+ git: Git = RealGit()
451
+
452
+ # Create Graphite based on config and availability
453
+ graphite: Graphite
454
+ if global_config is not None and global_config.use_graphite:
455
+ # Config says use Graphite - check if gt is installed
456
+ if shutil.which("gt") is None:
457
+ graphite = GraphiteDisabled(GraphiteDisabledReason.NOT_INSTALLED)
458
+ else:
459
+ graphite = RealGraphite()
460
+ else:
461
+ # Graphite disabled by config (or config doesn't exist yet)
462
+ graphite = GraphiteDisabled(GraphiteDisabledReason.CONFIG_DISABLED)
463
+
464
+ # 5. Discover repo (only needs cwd, erk_root, git)
465
+ # If global_config is None, use placeholder path for repo discovery
466
+ erk_root = global_config.erk_root if global_config else erk_installation.root() / "worktrees"
467
+ repo = discover_repo_or_sentinel(cwd, erk_root, git)
468
+
469
+ # 6. Fetch repo_info (if in a repo with origin remote)
470
+ # Note: try-except is acceptable at CLI entry point boundary per LBYL conventions
471
+ repo_info: RepoInfo | None = None
472
+ if not isinstance(repo, NoRepoSentinel):
473
+ try:
474
+ remote_url = git.get_remote_url(repo.root)
475
+ owner, name = parse_git_remote_url(remote_url)
476
+ repo_info = RepoInfo(owner=owner, name=name)
477
+ except ValueError:
478
+ # No origin remote configured - repo_info stays None
479
+ pass
480
+
481
+ # 7. Load local config (or defaults if no repo)
482
+ # Loaded early so plans_repo can be used for GitHubIssues
483
+ if isinstance(repo, NoRepoSentinel):
484
+ local_config = LoadedConfig.test()
485
+ else:
486
+ # Ensure metadata directories exist (needed for worktrees)
487
+ ensure_erk_metadata_dir(repo)
488
+ # Load config from primary location (.erk/config.toml)
489
+ # Legacy locations are detected by 'erk doctor' only
490
+ local_config = load_config(repo.root)
491
+
492
+ # 8. Create GitHub-related classes (need repo_info, local_config)
493
+ github: GitHub = RealGitHub(time, repo_info)
494
+ # Use plans_repo for cross-repo plan issue management if configured
495
+ issues: GitHubIssues = RealGitHubIssues(target_repo=local_config.plans_repo)
496
+ plan_store: PlanStore = GitHubPlanStore(issues)
497
+ plan_list_service: PlanListService = RealPlanListService(github, issues)
498
+
499
+ # 9. Choose feedback implementation based on mode
500
+ feedback: UserFeedback
501
+ if script:
502
+ feedback = SuppressedFeedback() # Suppress diagnostics
503
+ else:
504
+ feedback = InteractiveFeedback() # Show all messages
505
+
506
+ # 10. Apply dry-run wrappers if needed
507
+ if dry_run:
508
+ git = DryRunGit(git)
509
+ graphite = DryRunGraphite(graphite)
510
+ github = DryRunGitHub(github)
511
+ issues = DryRunGitHubIssues(issues)
512
+
513
+ # 11. Create claude installation, prompt executor, and claude settings store
514
+ from erk_shared.extraction.claude_installation import RealClaudeInstallation
515
+
516
+ real_claude_installation: ClaudeInstallation = RealClaudeInstallation()
517
+ prompt_executor: PromptExecutor = RealPromptExecutor(time)
518
+ claude_settings_store: ClaudeSettingsStore = RealClaudeSettingsStore()
519
+
520
+ # 13. Create context with all values
521
+ return ErkContext(
522
+ git=git,
523
+ github=github,
524
+ github_admin=RealGitHubAdmin(),
525
+ issues=issues,
526
+ plan_store=plan_store,
527
+ graphite=graphite,
528
+ shell=RealShell(),
529
+ claude_executor=RealClaudeExecutor(),
530
+ completion=RealCompletion(),
531
+ time=time,
532
+ erk_installation=erk_installation,
533
+ script_writer=RealScriptWriter(),
534
+ feedback=feedback,
535
+ plan_list_service=plan_list_service,
536
+ planner_registry=RealPlannerRegistry(erk_installation.get_planners_config_path()),
537
+ claude_installation=real_claude_installation,
538
+ prompt_executor=prompt_executor,
539
+ claude_settings_store=claude_settings_store,
540
+ cwd=cwd,
541
+ global_config=global_config,
542
+ local_config=local_config,
543
+ repo=repo,
544
+ repo_info=repo_info,
545
+ dry_run=dry_run,
546
+ debug=debug,
547
+ )
548
+
549
+
550
+ def regenerate_context(existing_ctx: ErkContext) -> ErkContext:
551
+ """Regenerate context with fresh cwd.
552
+
553
+ Creates a new ErkContext with:
554
+ - Current working directory (Path.cwd())
555
+ - Preserved dry_run state and operation instances
556
+
557
+ Use this after mutations like os.chdir() or worktree removal
558
+ to ensure ctx.cwd reflects actual current directory.
559
+
560
+ Args:
561
+ existing_ctx: Current context to preserve settings from
562
+
563
+ Returns:
564
+ New ErkContext with regenerated state
565
+
566
+ Example:
567
+ # After os.chdir() or worktree removal
568
+ ctx = regenerate_context(ctx)
569
+ """
570
+ return create_context(dry_run=existing_ctx.dry_run, debug=existing_ctx.debug)
@@ -0,0 +1,4 @@
1
+ """Display abstractions for live terminal updates."""
2
+
3
+ from erk.core.display.abc import LiveDisplay as LiveDisplay
4
+ from erk.core.display.real import RealLiveDisplay as RealLiveDisplay
@@ -0,0 +1,24 @@
1
+ """Abstract interface for live-updating terminal display."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from rich.console import RenderableType
6
+
7
+
8
+ class LiveDisplay(ABC):
9
+ """Abstract interface for live-updating terminal display."""
10
+
11
+ @abstractmethod
12
+ def start(self) -> None:
13
+ """Start live display mode."""
14
+ ...
15
+
16
+ @abstractmethod
17
+ def update(self, renderable: RenderableType) -> None:
18
+ """Update the display with new content."""
19
+ ...
20
+
21
+ @abstractmethod
22
+ def stop(self) -> None:
23
+ """Stop live display mode."""
24
+ ...
@@ -0,0 +1,30 @@
1
+ """Real implementation of LiveDisplay using Rich Live."""
2
+
3
+ from rich.console import Console, RenderableType
4
+ from rich.live import Live
5
+
6
+ from erk.core.display.abc import LiveDisplay
7
+
8
+
9
+ class RealLiveDisplay(LiveDisplay):
10
+ """Real implementation using Rich Live."""
11
+
12
+ def __init__(self, console: Console | None = None) -> None:
13
+ self._console = console or Console(stderr=True, width=200, force_terminal=True)
14
+ self._live: Live | None = None
15
+
16
+ def start(self) -> None:
17
+ """Start live display mode."""
18
+ self._live = Live(console=self._console, refresh_per_second=4)
19
+ self._live.start()
20
+
21
+ def update(self, renderable: RenderableType) -> None:
22
+ """Update the display with new content."""
23
+ if self._live is not None:
24
+ self._live.update(renderable)
25
+
26
+ def stop(self) -> None:
27
+ """Stop live display mode."""
28
+ if self._live is not None:
29
+ self._live.stop()
30
+ self._live = None