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/cli/core.py ADDED
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+
3
+ from erk.cli.ensure import Ensure
4
+ from erk.core.context import ErkContext
5
+ from erk.core.repo_discovery import RepoContext, discover_repo_or_sentinel
6
+
7
+
8
+ def discover_repo_context(ctx: ErkContext, start: Path) -> RepoContext:
9
+ """Walk up from `start` to find a directory containing `.git`.
10
+
11
+ Returns a RepoContext pointing to the repo root and the global worktrees directory
12
+ for this repository.
13
+ Raises FileNotFoundError if not inside a git repo.
14
+
15
+ Note: Properly handles git worktrees by finding the main repository root,
16
+ not the worktree's .git file.
17
+ """
18
+ if ctx.global_config is None:
19
+ raise FileNotFoundError("Global config not found. Run 'erk init' to create it.")
20
+
21
+ result = discover_repo_or_sentinel(start, ctx.global_config.erk_root, ctx.git)
22
+ if isinstance(result, RepoContext):
23
+ return result
24
+ raise FileNotFoundError(result.message)
25
+
26
+
27
+ def worktree_path_for(worktrees_dir: Path, name: str) -> Path:
28
+ """Return the absolute path for a named worktree within worktrees directory.
29
+
30
+ Note: Does not handle 'root' as a special case. Commands that support
31
+ 'root' must check for it explicitly and use repo.root directly.
32
+
33
+ Args:
34
+ worktrees_dir: The directory containing all worktrees for this repo
35
+ name: The worktree name (e.g., 'feature-a')
36
+
37
+ Returns:
38
+ Absolute path to the worktree (e.g., ~/.erk/repos/myrepo/worktrees/feature-a/)
39
+ """
40
+ return (worktrees_dir / name).resolve()
41
+
42
+
43
+ def validate_worktree_name_for_deletion(name: str) -> None:
44
+ """Validate that a worktree name is safe for deletion.
45
+
46
+ Rejects:
47
+ - Empty strings
48
+ - `.` or `..` (current/parent directory references)
49
+ - `root` (explicit root worktree name)
50
+ - Names starting with `/` (absolute paths)
51
+ - Names containing `/` (path separators)
52
+
53
+ Raises SystemExit(1) with error message if validation fails.
54
+ """
55
+ Ensure.not_empty(name.strip() if name else "", "Worktree name cannot be empty")
56
+ Ensure.invariant(
57
+ name not in (".", ".."),
58
+ f"Cannot delete '{name}' - directory references not allowed",
59
+ )
60
+ Ensure.invariant(name != "root", "Cannot delete 'root' - root worktree name not allowed")
61
+ Ensure.invariant(
62
+ not name.startswith("/"),
63
+ f"Cannot delete '{name}' - absolute paths not allowed",
64
+ )
65
+ Ensure.invariant("/" not in name, f"Cannot delete '{name}' - path separators not allowed")
erk/cli/debug.py ADDED
@@ -0,0 +1,9 @@
1
+ """Debug logging utilities for erk.
2
+
3
+ DEPRECATED: Import from erk_shared.debug instead.
4
+ This module is a re-export shim for backwards compatibility.
5
+ """
6
+
7
+ # Re-export for backwards compatibility (explicit re-export syntax per PEP 484)
8
+ from erk_shared.debug import debug_log as debug_log
9
+ from erk_shared.debug import is_debug as is_debug
@@ -0,0 +1,288 @@
1
+ # Ensure Class Conversion Tasks
2
+
3
+ **Purpose**: Track conversion of manual error checking to use the centralized Ensure class.
4
+ **Status**: IN_PROGRESS
5
+ **Last Updated**: 2025-11-26
6
+
7
+ ## Status Legend
8
+
9
+ - [ ] Not started
10
+ - [WIP] Work in progress
11
+ - [x] Completed
12
+
13
+ ---
14
+
15
+ ## Phase 1: New Ensure Methods
16
+
17
+ ### Priority 1: High-Value Methods (8+ uses)
18
+
19
+ #### Task 1.1: Implement Ensure.mutually_exclusive_flags()
20
+
21
+ - [ ] Write method implementation
22
+ - [ ] Add unit tests (success case, multiple flags set, custom message)
23
+ - [ ] Document in method docstring
24
+ - **Usage**: 8 occurrences (wt/create, stack/consolidate, stack/move, stack/split)
25
+ - **Signature**: `mutually_exclusive_flags(flags: dict[str, bool | Any], error_message: str | None = None) -> None`
26
+
27
+ #### Task 1.2: Implement Ensure.not_detached_head()
28
+
29
+ - [ ] Write method implementation with type narrowing
30
+ - [ ] Add unit tests (on branch, detached HEAD, custom operation name)
31
+ - [ ] Document in method docstring
32
+ - **Usage**: 4 occurrences (stack/consolidate, stack/move, wt/delete)
33
+ - **Signature**: `not_detached_head(ctx: ErkContext, branch: str | None, operation_name: str) -> str`
34
+ - **Returns**: Narrowed type `str` (from `str | None`)
35
+
36
+ #### Task 1.3: Implement Ensure.clean_working_tree()
37
+
38
+ - [ ] Write method implementation with allow_force parameter
39
+ - [ ] Add unit tests (clean tree, uncommitted changes, force override)
40
+ - [ ] Document in method docstring
41
+ - **Usage**: 6 occurrences (navigation_helpers, stack/move, stack/split)
42
+ - **Signature**: `clean_working_tree(ctx: ErkContext, path: Path, operation_name: str | None = None, allow_force: bool = False) -> None`
43
+
44
+ #### Task 1.4: Implement Ensure.graphite_enabled()
45
+
46
+ - [ ] Write method implementation
47
+ - [ ] Add unit tests (enabled, disabled)
48
+ - [ ] Document in method docstring
49
+ - **Usage**: 3 occurrences (navigation_helpers, wt/delete)
50
+ - **Signature**: `graphite_enabled(ctx: ErkContext) -> None`
51
+ - **Error**: "This command requires Graphite to be enabled. Run 'erk config set use_graphite true'"
52
+
53
+ ### Priority 2: Medium-Value Methods (2-4 uses)
54
+
55
+ #### Task 1.5: Implement Ensure.branch_tracked_by_graphite()
56
+
57
+ - [ ] Write method implementation with type narrowing
58
+ - [ ] Add unit tests (tracked branch, untracked branch)
59
+ - [ ] Document in method docstring
60
+ - **Usage**: 2 occurrences (stack/consolidate)
61
+ - **Signature**: `branch_tracked_by_graphite(ctx: ErkContext, repo_root: Path, branch: str) -> list[str]`
62
+ - **Returns**: Stack branches (narrows from `list[str] | None`)
63
+
64
+ #### Task 1.6: Implement Ensure.in_repo()
65
+
66
+ - [ ] Write method implementation
67
+ - [ ] Add unit tests (in repo, NoRepoSentinel)
68
+ - [ ] Document in method docstring
69
+ - **Usage**: 4 occurrences (context.py, pr/checkout, config)
70
+ - **Signature**: `in_repo(ctx: ErkContext) -> None`
71
+
72
+ #### Task 1.7: Implement Ensure.global_config_exists()
73
+
74
+ - [ ] Write method implementation with type narrowing
75
+ - [ ] Add unit tests (config exists, config None)
76
+ - [ ] Document in method docstring
77
+ - **Usage**: 4 occurrences (init, config, core)
78
+ - **Signature**: `global_config_exists(ctx: ErkContext) -> GlobalConfig`
79
+
80
+ #### Task 1.8: Implement Ensure.not_trunk_branch()
81
+
82
+ - [ ] Write method implementation
83
+ - [ ] Add unit tests (trunk branch, non-trunk branch)
84
+ - [ ] Document in method docstring
85
+ - **Usage**: 1-2 occurrences (wt/create)
86
+ - **Signature**: `not_trunk_branch(ctx: ErkContext, repo_root: Path, branch: str) -> None`
87
+
88
+ ---
89
+
90
+ ## Phase 2: File Conversions
91
+
92
+ ### High Priority (20+ patterns each)
93
+
94
+ #### Task 2.1: Convert wt/create_cmd.py (30+ patterns)
95
+
96
+ - [ ] Lines 556-561: Replace manual check with `Ensure.mutually_exclusive_flags()` for --from-\* flags
97
+ - [ ] Lines 564-566: Replace manual check with `Ensure.mutually_exclusive_flags()` for --json/--script
98
+ - [ ] Lines 574-582: Replace manual check with `Ensure.mutually_exclusive_flags()` for --copy-plan
99
+ - [ ] Lines 101-108: Replace manual check with `Ensure.not_trunk_branch()`
100
+ - [ ] Lines 276: Replace None check with `Ensure.not_detached_head()` (already uses Ensure.not_none at line 610)
101
+ - [ ] Lines 587-594: Replace .exists()/.is_dir() checks with `Ensure.path_is_dir()` for .impl directory
102
+ - [ ] Lines 610-612: Keep as-is (already uses `Ensure.not_none()` correctly)
103
+ - [ ] Run tests: `uv run pytest tests/commands/workspace/test_create.py`
104
+ - [ ] Verify no regressions
105
+
106
+ #### Task 2.2: Convert stack/consolidate_cmd.py (15+ patterns)
107
+
108
+ - [ ] Lines 153-159: Replace manual check with `Ensure.mutually_exclusive_flags({"--down": down, "BRANCH": branch is not None})`
109
+ - [ ] Lines 165-168: Replace None check with `Ensure.not_detached_head(ctx, current_branch, "consolidate")`
110
+ - [ ] Lines 176-181: Replace None check with `stack_branches = Ensure.branch_tracked_by_graphite(ctx, repo.root, current_branch)`
111
+ - [ ] Lines 184-193: Keep as-is (nice formatted stack display, use `Ensure.invariant()` wrapper)
112
+ - [ ] Lines 202-212: Keep as-is or use `Ensure.invariant(name not in existing_names, msg)`
113
+ - [ ] Lines 231-241: Consider refactoring to `Ensure.clean_working_tree()` for each worktree in loop
114
+ - [ ] Run tests: `uv run pytest tests/commands/stack/`
115
+ - [ ] Verify no regressions
116
+
117
+ #### Task 2.3: Convert implement.py (25+ patterns)
118
+
119
+ - [ ] Lines 373-375: Replace .exists() check with `Ensure.path_exists(ctx, plan_file, ...)`
120
+ - [ ] Lines 632: Use `Ensure.not_none()` for wt_path
121
+ - [ ] Lines 708: Use `Ensure.not_none()` for target_info.issue_number
122
+ - [ ] Lines 866: Use `Ensure.not_none()` for wt_path
123
+ - [ ] Audit all None checks for conversion opportunities
124
+ - [ ] Run tests: `uv run pytest tests/commands/test_implement.py` (if exists)
125
+ - [ ] Verify no regressions
126
+
127
+ ### Medium Priority (10-20 patterns each)
128
+
129
+ #### Task 2.4: Convert navigation_helpers.py
130
+
131
+ - [ ] Lines 14-28: Delete `ensure_graphite_enabled()` function after converting callers
132
+ - [ ] Lines 31-42: Delete `check_clean_working_tree()` function after converting callers
133
+ - [ ] Lines 45-68: Delete `verify_pr_merged()` function OR convert to `Ensure.pr_merged()` if implementing
134
+ - [ ] Lines 212-214: Use `Ensure.not_none()` in navigate_upstack
135
+ - [ ] Lines 246-254: Use `Ensure.not_none()` in navigate_downstack (multiple checks)
136
+ - [ ] Update up.py and down.py to call Ensure methods directly
137
+ - [ ] Run tests: `uv run pytest tests/commands/navigation/`
138
+ - [ ] Verify no regressions
139
+
140
+ #### Task 2.5: Convert stack/move_cmd.py
141
+
142
+ - [ ] Lines 59-61: Replace with `Ensure.mutually_exclusive_flags()`
143
+ - [ ] Lines 115: Replace with `Ensure.not_detached_head()`
144
+ - [ ] Lines 119-124: Replace with `Ensure.clean_working_tree(ctx, source_wt, "move", allow_force=force)`
145
+ - [ ] Lines 138: Replace with `Ensure.clean_working_tree(ctx, target_wt, "move", allow_force=force)`
146
+ - [ ] Lines 186: Replace with `Ensure.clean_working_tree()`
147
+ - [ ] Run tests: `uv run pytest tests/commands/stack/test_move.py`
148
+ - [ ] Verify no regressions
149
+
150
+ #### Task 2.6: Convert stack/split_old/command.py
151
+
152
+ - [ ] Lines 32-37: Replace with `Ensure.mutually_exclusive_flags({"--up": up, "--down": down})`
153
+ - [ ] Lines 64-67: Replace with `Ensure.clean_working_tree(ctx, current_worktree, "split")`
154
+ - [ ] Run tests: `uv run pytest tests/commands/stack/test_split.py`
155
+ - [ ] Verify no regressions
156
+
157
+ ### Lower Priority (5-10 patterns each)
158
+
159
+ #### Task 2.7: Convert wt/delete_cmd.py
160
+
161
+ - [ ] Lines 132-137: Replace with `Ensure.graphite_enabled(ctx)`
162
+ - [ ] Lines 143-147: Replace with `Ensure.not_detached_head()`
163
+ - [ ] Run tests: `uv run pytest tests/commands/workspace/test_delete.py`
164
+ - [ ] Verify no regressions
165
+
166
+ #### Task 2.8: Convert commands/config.py
167
+
168
+ - [ ] Lines 104-107: Replace with `Ensure.global_config_exists(ctx)`
169
+ - [ ] Lines 157-160: Replace with `Ensure.global_config_exists(ctx)`
170
+ - [ ] Run tests: `uv run pytest tests/commands/test_config.py`
171
+ - [ ] Verify no regressions
172
+
173
+ #### Task 2.9: Convert commands/init.py
174
+
175
+ - [ ] Lines 171-175: Replace with `Ensure.global_config_exists(ctx)`
176
+ - [ ] Lines 228-232: Replace with `Ensure.global_config_exists(ctx)`
177
+ - [ ] Lines 248-252: Replace with `Ensure.global_config_exists(ctx)`
178
+ - [ ] Lines 266-268: Replace with `Ensure.path_not_exists()` (adjust condition logic)
179
+ - [ ] Run tests: `uv run pytest tests/commands/test_init.py`
180
+ - [ ] Verify no regressions
181
+
182
+ #### Task 2.10: Convert commands/wt/current_cmd.py
183
+
184
+ - [ ] Lines 28-29: Add error message and replace with `Ensure.not_none(wt_info, "Not in a worktree")`
185
+ - [ ] Run tests: `uv run pytest tests/commands/workspace/test_current.py`
186
+ - [ ] Verify no regressions
187
+
188
+ #### Task 2.11: Convert commands/pr/checkout_cmd.py
189
+
190
+ - [ ] Line 43: Replace NoRepoSentinel check with `Ensure.in_repo(ctx)`
191
+ - [ ] Run tests: `uv run pytest tests/commands/pr/`
192
+ - [ ] Verify no regressions
193
+
194
+ #### Task 2.12: Convert cli/core.py
195
+
196
+ - [ ] Lines 18-19: Replace with `Ensure.global_config_exists(ctx)`
197
+ - [ ] Run tests: `uv run pytest tests/unit/cli/`
198
+ - [ ] Verify no regressions
199
+
200
+ ---
201
+
202
+ ## Phase 3: Final Cleanup
203
+
204
+ #### Task 3.1: Audit for remaining patterns
205
+
206
+ - [ ] Search for `raise SystemExit(1)` across CLI files
207
+ - [ ] Search for manual None checks followed by user_output
208
+ - [ ] Search for manual path existence checks
209
+ - [ ] Create follow-up tasks for any missed patterns
210
+
211
+ #### Task 3.2: Consistency pass
212
+
213
+ - [ ] Verify all error messages use red "Error: " prefix
214
+ - [ ] Verify all Ensure method docstrings are complete
215
+ - [ ] Verify all type narrowing methods work with ty
216
+ - [ ] Update any developer documentation
217
+
218
+ #### Task 3.3: Final validation
219
+
220
+ - [ ] Run full test suite: `uv run pytest`
221
+ - [ ] Run type checker: `uv run ty`
222
+ - [ ] Verify no regressions in any tests
223
+ - [ ] Mark project as COMPLETED
224
+
225
+ ---
226
+
227
+ ## Implementation Notes
228
+
229
+ ### Test Pattern to Follow
230
+
231
+ From `tests/unit/cli/test_ensure.py`:
232
+
233
+ ```python
234
+ def test_exits_when_condition_false(self) -> None:
235
+ """Ensure.method raises SystemExit when condition fails."""
236
+ with pytest.raises(SystemExit) as exc_info:
237
+ Ensure.method(...)
238
+ assert exc_info.value.code == 1
239
+
240
+ def test_error_message_output(self, capsys: pytest.CaptureFixture[str]) -> None:
241
+ """Ensure.method outputs error message to stderr."""
242
+ with pytest.raises(SystemExit):
243
+ Ensure.method(...)
244
+
245
+ captured = capsys.readouterr()
246
+ assert "Error:" in captured.err
247
+ assert "expected message" in captured.err
248
+ ```
249
+
250
+ ### Type Narrowing Example
251
+
252
+ Methods that return values should narrow types:
253
+
254
+ ```python
255
+ # Before: branch: str | None = ctx.git.get_current_branch(ctx.cwd)
256
+ # After: branch: str = Ensure.not_detached_head(ctx, ctx.git.get_current_branch(ctx.cwd), "operation")
257
+ ```
258
+
259
+ ### Mutually Exclusive Flags Example
260
+
261
+ ```python
262
+ # Before:
263
+ if flag_count > 1:
264
+ user_output("Error: Only one of --current, --branch, or --worktree can be specified")
265
+ raise SystemExit(1)
266
+
267
+ # After:
268
+ Ensure.mutually_exclusive_flags({
269
+ "--current": current,
270
+ "--branch": branch is not None,
271
+ "--worktree": worktree is not None
272
+ })
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Progress Tracking
278
+
279
+ | Phase | Tasks | Completed | Remaining |
280
+ | ------------------------- | ------ | --------- | --------- |
281
+ | Phase 1: New Methods | 8 | 0 | 8 |
282
+ | Phase 2: File Conversions | 12 | 0 | 12 |
283
+ | Phase 3: Final Cleanup | 3 | 0 | 3 |
284
+ | **TOTAL** | **23** | **0** | **23** |
285
+
286
+ **Last Updated**: 2025-11-26
287
+ **Current Focus**: Phase 1 - New Ensure Methods
288
+ **Next Up**: Task 1.1 - Implement Ensure.mutually_exclusive_flags()