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,801 @@
1
+ import dataclasses
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.artifacts.sync import sync_artifacts
8
+ from erk.cli.core import discover_repo_context
9
+ from erk.core.claude_settings import (
10
+ ERK_PERMISSION,
11
+ NoBackupCreated,
12
+ StatuslineNotConfigured,
13
+ add_erk_hooks,
14
+ add_erk_permission,
15
+ add_erk_statusline,
16
+ get_erk_statusline_command,
17
+ get_repo_claude_settings_path,
18
+ get_statusline_config,
19
+ has_erk_permission,
20
+ has_erk_statusline,
21
+ has_exit_plan_hook,
22
+ has_user_prompt_hook,
23
+ read_claude_settings,
24
+ write_claude_settings,
25
+ )
26
+ from erk.core.context import ErkContext
27
+ from erk.core.init_utils import (
28
+ add_gitignore_entry,
29
+ discover_presets,
30
+ get_shell_wrapper_content,
31
+ has_shell_integration_in_rc,
32
+ is_repo_erk_ified,
33
+ is_repo_named,
34
+ render_config_template,
35
+ )
36
+ from erk.core.release_notes import get_current_version
37
+ from erk.core.repo_discovery import (
38
+ NoRepoSentinel,
39
+ discover_repo_or_sentinel,
40
+ ensure_erk_metadata_dir,
41
+ )
42
+ from erk.core.shell import Shell
43
+ from erk_shared.context.types import GlobalConfig
44
+ from erk_shared.extraction.claude_installation import RealClaudeInstallation
45
+ from erk_shared.git.real import RealGit
46
+ from erk_shared.github.issues.abc import GitHubIssues
47
+ from erk_shared.github.issues.real import RealGitHubIssues
48
+ from erk_shared.github.plan_issues import get_erk_label_definitions
49
+ from erk_shared.output.output import user_confirm, user_output
50
+
51
+
52
+ def detect_graphite(shell_ops: Shell) -> bool:
53
+ """Detect if Graphite (gt) is installed and available in PATH."""
54
+ return shell_ops.get_installed_tool_path("gt") is not None
55
+
56
+
57
+ def create_and_save_global_config(
58
+ ctx: ErkContext,
59
+ erk_root: Path,
60
+ shell_setup_complete: bool,
61
+ ) -> GlobalConfig:
62
+ """Create and save global config, returning the created config."""
63
+ use_graphite = detect_graphite(ctx.shell)
64
+ config = GlobalConfig(
65
+ erk_root=erk_root,
66
+ use_graphite=use_graphite,
67
+ shell_setup_complete=shell_setup_complete,
68
+ show_pr_info=True,
69
+ github_planning=True,
70
+ )
71
+ ctx.erk_installation.save_config(config)
72
+ return config
73
+
74
+
75
+ def _add_gitignore_entry_with_prompt(
76
+ content: str, entry: str, prompt_message: str
77
+ ) -> tuple[str, bool]:
78
+ """Add an entry to gitignore content if not present and user confirms.
79
+
80
+ This wrapper adds user interaction to the pure add_gitignore_entry function.
81
+
82
+ Args:
83
+ content: Current gitignore content
84
+ entry: Entry to add (e.g., ".env")
85
+ prompt_message: Message to show user when confirming
86
+
87
+ Returns:
88
+ Tuple of (updated_content, was_modified)
89
+ """
90
+ # Entry already present
91
+ if entry in content:
92
+ return (content, False)
93
+
94
+ # User declined
95
+ if not click.confirm(prompt_message, default=True):
96
+ return (content, False)
97
+
98
+ # Use pure function to add entry
99
+ new_content = add_gitignore_entry(content, entry)
100
+ return (new_content, True)
101
+
102
+
103
+ def _create_prompt_hooks_directory(repo_root: Path) -> None:
104
+ """Create .erk/prompt-hooks/ directory and install README.
105
+
106
+ Args:
107
+ repo_root: Path to the repository root
108
+ """
109
+ prompt_hooks_dir = repo_root / ".erk" / "prompt-hooks"
110
+ prompt_hooks_dir.mkdir(parents=True, exist_ok=True)
111
+
112
+ # Install README template
113
+ template_path = Path(__file__).parent.parent / "prompt_hooks_templates" / "README.md"
114
+ readme_path = prompt_hooks_dir / "README.md"
115
+
116
+ if template_path.exists():
117
+ readme_content = template_path.read_text(encoding="utf-8")
118
+ readme_path.write_text(readme_content, encoding="utf-8")
119
+ user_output(click.style("✓", fg="green") + " Created prompt hooks directory")
120
+ user_output(" See .erk/prompt-hooks/README.md for available hooks")
121
+ else:
122
+ # Fallback: create directory but warn about missing template
123
+ user_output(
124
+ click.style("⚠️", fg="yellow") + " Created .erk/prompt-hooks/ (template not found)"
125
+ )
126
+
127
+
128
+ def _run_gitignore_prompts(repo_root: Path) -> None:
129
+ """Run interactive prompts for .gitignore entries.
130
+
131
+ Offers to add .env, .erk/scratch/, and .impl/ to .gitignore.
132
+
133
+ Args:
134
+ repo_root: Path to the repository root
135
+ """
136
+ gitignore_path = repo_root / ".gitignore"
137
+ if not gitignore_path.exists():
138
+ return
139
+
140
+ gitignore_content = gitignore_path.read_text(encoding="utf-8")
141
+
142
+ # Add .env
143
+ gitignore_content, env_added = _add_gitignore_entry_with_prompt(
144
+ gitignore_content,
145
+ ".env",
146
+ "Add .env to .gitignore?",
147
+ )
148
+
149
+ # Add .erk/scratch/
150
+ gitignore_content, scratch_added = _add_gitignore_entry_with_prompt(
151
+ gitignore_content,
152
+ ".erk/scratch/",
153
+ "Add .erk/scratch/ to .gitignore (session-specific working files)?",
154
+ )
155
+
156
+ # Add .impl/
157
+ gitignore_content, impl_added = _add_gitignore_entry_with_prompt(
158
+ gitignore_content,
159
+ ".impl/",
160
+ "Add .impl/ to .gitignore (temporary implementation plans)?",
161
+ )
162
+
163
+ # Write if any entry was modified
164
+ if env_added or scratch_added or impl_added:
165
+ gitignore_path.write_text(gitignore_content, encoding="utf-8")
166
+ user_output(f"Updated {gitignore_path}")
167
+
168
+
169
+ def print_shell_setup_instructions(
170
+ shell: str, rc_file: Path, completion_line: str, wrapper_content: str
171
+ ) -> None:
172
+ """Print formatted shell integration setup instructions for manual installation.
173
+
174
+ Args:
175
+ shell: The shell type (e.g., "zsh", "bash", "fish")
176
+ rc_file: Path to the shell's rc file (e.g., ~/.zshrc)
177
+ completion_line: The completion command to add (e.g., "source <(erk completion zsh)")
178
+ wrapper_content: The full wrapper function content to add
179
+ """
180
+ user_output("\n" + "━" * 60)
181
+ user_output("Shell Integration Setup")
182
+ user_output("━" * 60)
183
+ user_output(f"\nDetected shell: {shell} ({rc_file})")
184
+ user_output("\nAdd the following to your rc file:\n")
185
+ user_output("# Erk completion")
186
+ user_output(f"{completion_line}\n")
187
+ user_output("# Erk shell integration")
188
+ user_output(wrapper_content)
189
+ user_output("\nThen reload your shell:")
190
+ user_output(f" source {rc_file}")
191
+ user_output("━" * 60)
192
+
193
+
194
+ def perform_shell_setup(shell_ops: Shell) -> bool:
195
+ """Print shell integration setup instructions for manual installation.
196
+
197
+ Returns True if instructions were printed, False if setup was skipped.
198
+ """
199
+ shell_info = shell_ops.detect_shell()
200
+ if not shell_info:
201
+ user_output("Unable to detect shell. Skipping shell integration setup.")
202
+ return False
203
+
204
+ shell, rc_file = shell_info
205
+
206
+ # Resolve symlinks to show the real file path in instructions
207
+ if rc_file.exists():
208
+ rc_file = rc_file.resolve()
209
+
210
+ user_output(f"\nDetected shell: {shell}")
211
+ user_output("Shell integration provides:")
212
+ user_output(" - Tab completion for erk commands")
213
+ user_output(" - Automatic worktree activation on 'erk br co'")
214
+
215
+ if not click.confirm("\nShow shell integration setup instructions?", default=True):
216
+ user_output("Skipping shell integration. You can run 'erk init --shell' later.")
217
+ return False
218
+
219
+ # Generate the instructions
220
+ completion_line = f"source <(erk completion {shell})"
221
+ shell_integration_dir = Path(__file__).parent.parent / "shell_integration"
222
+ wrapper_content = get_shell_wrapper_content(shell_integration_dir, shell)
223
+
224
+ # Print the formatted instructions
225
+ print_shell_setup_instructions(shell, rc_file, completion_line, wrapper_content)
226
+
227
+ return True
228
+
229
+
230
+ def _get_presets_dir() -> Path:
231
+ """Get the path to the presets directory."""
232
+ return Path(__file__).parent.parent / "presets"
233
+
234
+
235
+ def offer_claude_permission_setup(repo_root: Path) -> Path | NoBackupCreated:
236
+ """Offer to add erk permission to repo's Claude Code settings.
237
+
238
+ This checks if the repo's .claude/settings.json exists and whether the erk
239
+ permission is already configured. If the file exists but permission is missing,
240
+ it prompts the user to add it.
241
+
242
+ Args:
243
+ repo_root: Path to the repository root
244
+
245
+ Returns:
246
+ Path to backup file if one was created, NoBackupCreated sentinel otherwise.
247
+ """
248
+ settings_path = get_repo_claude_settings_path(repo_root)
249
+
250
+ try:
251
+ settings = read_claude_settings(settings_path)
252
+ except json.JSONDecodeError as e:
253
+ warning = click.style("⚠️ Warning: ", fg="yellow")
254
+ user_output(warning + "Invalid JSON in .claude/settings.json")
255
+ user_output(f" {e}")
256
+ return NoBackupCreated()
257
+
258
+ # No settings file - skip silently (repo may not have Claude settings)
259
+ if settings is None:
260
+ return NoBackupCreated()
261
+
262
+ # Permission already exists - skip silently
263
+ if has_erk_permission(settings):
264
+ return NoBackupCreated()
265
+
266
+ # Offer to add permission
267
+ user_output("\nClaude settings found. The erk permission allows Claude to run")
268
+ user_output("erk commands without prompting for approval each time.")
269
+
270
+ if not user_confirm(f"Add {ERK_PERMISSION} to .claude/settings.json?", default=True):
271
+ user_output("Skipped. You can add the permission manually to .claude/settings.json")
272
+ return NoBackupCreated()
273
+
274
+ # Add permission
275
+ new_settings = add_erk_permission(settings)
276
+
277
+ # Confirm before overwriting
278
+ user_output(f"\nThis will update: {settings_path}")
279
+ if not user_confirm("Proceed with writing changes?", default=False):
280
+ user_output("Skipped. No changes made to settings.json")
281
+ return NoBackupCreated()
282
+
283
+ backup_result = write_claude_settings(settings_path, new_settings)
284
+ user_output(click.style("✓", fg="green") + f" Added {ERK_PERMISSION} to {settings_path}")
285
+
286
+ # If backup was created, inform user (deletion offered at end of init)
287
+ if not isinstance(backup_result, NoBackupCreated):
288
+ user_output(f"\n📁 Backup created: {backup_result}")
289
+ user_output(f" To restore: cp {backup_result} {settings_path}")
290
+
291
+ return backup_result
292
+
293
+
294
+ def offer_backup_cleanup(backup_path: Path) -> None:
295
+ """Offer to delete a backup file.
296
+
297
+ Args:
298
+ backup_path: Path to the backup file to potentially delete
299
+ """
300
+ if click.confirm("Delete backup?", default=True):
301
+ backup_path.unlink()
302
+ user_output(click.style("✓", fg="green") + " Backup deleted")
303
+
304
+
305
+ def offer_claude_hook_setup(repo_root: Path) -> None:
306
+ """Offer to add erk hooks to repo's Claude Code settings.
307
+
308
+ This checks if the repo's .claude/settings.json exists and whether the erk
309
+ hooks are already configured. If the file exists but hooks are missing,
310
+ it prompts the user to add them.
311
+
312
+ Args:
313
+ repo_root: Path to the repository root
314
+ """
315
+ settings_path = get_repo_claude_settings_path(repo_root)
316
+
317
+ try:
318
+ settings = read_claude_settings(settings_path)
319
+ except json.JSONDecodeError as e:
320
+ warning = click.style("⚠️ Warning: ", fg="yellow")
321
+ user_output(warning + "Invalid JSON in .claude/settings.json")
322
+ user_output(f" {e}")
323
+ return
324
+
325
+ # No settings file - will create one
326
+ creating_new_file = settings is None
327
+ if creating_new_file:
328
+ settings = {}
329
+ user_output(f"\nNo .claude/settings.json found. Will create: {settings_path}")
330
+
331
+ assert settings is not None # Type narrowing: set to {} if was None above
332
+ if has_user_prompt_hook(settings) and has_exit_plan_hook(settings):
333
+ user_output(click.style("✓", fg="green") + " Hooks already configured")
334
+ return
335
+
336
+ # Explain what hooks do
337
+ user_output("\nErk uses Claude Code hooks for session management and plan tracking.")
338
+
339
+ if not user_confirm("Add erk hooks to .claude/settings.json?", default=False):
340
+ user_output("Skipped. You can add hooks later with: erk init --hooks")
341
+ return
342
+
343
+ new_settings = add_erk_hooks(settings)
344
+ write_claude_settings(settings_path, new_settings)
345
+ user_output(click.style("✓", fg="green") + " Added erk hooks")
346
+
347
+
348
+ def create_plans_repo_labels(
349
+ repo_root: Path,
350
+ plans_repo: str,
351
+ github_issues: GitHubIssues,
352
+ ) -> str | None:
353
+ """Create erk labels in the target issues repository.
354
+
355
+ Args:
356
+ repo_root: Path to the working repository root (used for gh CLI context)
357
+ plans_repo: Target repository in "owner/repo" format
358
+ github_issues: GitHubIssues interface for label operations
359
+
360
+ Returns:
361
+ None on success, error message string on failure
362
+ """
363
+ labels = get_erk_label_definitions()
364
+
365
+ for label in labels:
366
+ github_issues.ensure_label_exists(
367
+ repo_root=repo_root,
368
+ label=label.name,
369
+ description=label.description,
370
+ color=label.color,
371
+ )
372
+
373
+ return None
374
+
375
+
376
+ def offer_plans_repo_label_setup(repo_root: Path, plans_repo: str) -> None:
377
+ """Offer to set up erk labels in the target issues repository.
378
+
379
+ When a plans_repo is configured, issues are created in a separate repository
380
+ from the working repository. This function ensures all required erk labels
381
+ (erk-plan, erk-extraction, erk-objective) exist in that target repository.
382
+
383
+ Args:
384
+ repo_root: Path to the working repository root (used for gh CLI context)
385
+ plans_repo: Target repository in "owner/repo" format
386
+ """
387
+ user_output(f"\nPlans repo configured: {plans_repo}")
388
+ user_output("Erk uses labels (erk-plan, erk-extraction, erk-objective) to organize issues.")
389
+
390
+ if not user_confirm(f"Set up erk labels in {plans_repo}?", default=True):
391
+ user_output("Skipped. You can set up labels later with: erk doctor --fix")
392
+ return
393
+
394
+ github_issues = RealGitHubIssues(target_repo=plans_repo)
395
+
396
+ try:
397
+ create_plans_repo_labels(repo_root, plans_repo, github_issues)
398
+ user_output(click.style("✓", fg="green") + f" Labels configured in {plans_repo}")
399
+ except RuntimeError as e:
400
+ warning = click.style("⚠️ Warning: ", fg="yellow")
401
+ user_output(warning + f"Failed to set up labels in {plans_repo}")
402
+ user_output(f" {e}")
403
+ user_output(" You can try again with: erk doctor --fix")
404
+
405
+
406
+ def perform_statusline_setup(settings_path: Path | None) -> bool:
407
+ """Configure erk-statusline in global Claude Code settings.
408
+
409
+ Reads ~/.claude/settings.json, adds statusLine configuration if not present
410
+ or different, and writes back. Handles edge cases:
411
+ - File doesn't exist: creates it with just statusLine config
412
+ - Already configured with same command: skips
413
+ - Different statusLine command: warns and prompts to overwrite
414
+
415
+ Args:
416
+ settings_path: Path to settings.json. If None, uses ~/.claude/settings.json.
417
+
418
+ Returns:
419
+ True if status line was configured, False otherwise.
420
+ """
421
+ if settings_path is None:
422
+ # Use RealClaudeInstallation directly since this runs before ErkContext exists
423
+ installation = RealClaudeInstallation()
424
+ settings_path = installation.get_settings_path()
425
+
426
+ user_output("\n Configuring Claude Code status line...")
427
+
428
+ # Read existing settings (or None if file doesn't exist)
429
+ try:
430
+ settings = read_claude_settings(settings_path)
431
+ except json.JSONDecodeError as e:
432
+ warning = click.style("⚠️ Warning: ", fg="yellow")
433
+ user_output(warning + "Invalid JSON in ~/.claude/settings.json")
434
+ user_output(f" {e}")
435
+ return False
436
+
437
+ # No settings file - will create one
438
+ if settings is None:
439
+ settings = {}
440
+ user_output(f" Creating: {settings_path}")
441
+
442
+ # Check current statusline config
443
+ current_config = get_statusline_config(settings)
444
+
445
+ # Already configured with erk-statusline
446
+ if has_erk_statusline(settings):
447
+ user_output(click.style(" ✓", fg="green") + " Statusline already configured")
448
+ return True
449
+
450
+ # Different statusline configured - warn and prompt
451
+ if not isinstance(current_config, StatuslineNotConfigured):
452
+ user_output(f"\n Existing statusLine found: {current_config.command}")
453
+ if not user_confirm(f" Replace with {get_erk_statusline_command()}?", default=False):
454
+ user_output(" Skipped. Keeping existing statusLine configuration.")
455
+ return False
456
+
457
+ # Add statusline config
458
+ new_settings = add_erk_statusline(settings)
459
+ write_claude_settings(settings_path, new_settings)
460
+ statusline_msg = " Status line configured in ~/.claude/settings.json"
461
+ user_output(click.style(" ✓", fg="green") + statusline_msg)
462
+ user_output(" Note: Install erk-statusline with: uv tool install erk-statusline")
463
+
464
+ return True
465
+
466
+
467
+ @click.command("init")
468
+ @click.option("-f", "--force", is_flag=True, help="Overwrite existing repo config if present.")
469
+ @click.option(
470
+ "--preset",
471
+ type=str,
472
+ default="auto",
473
+ help=(
474
+ "Config template to use. 'auto' detects preset based on repo characteristics. "
475
+ f"Available: auto, {', '.join(discover_presets(_get_presets_dir()))}."
476
+ ),
477
+ )
478
+ @click.option(
479
+ "--list-presets",
480
+ is_flag=True,
481
+ help="List available presets and exit.",
482
+ )
483
+ @click.option(
484
+ "--shell",
485
+ is_flag=True,
486
+ help="Show shell integration setup instructions (completion + auto-activation wrapper).",
487
+ )
488
+ @click.option(
489
+ "--hooks",
490
+ "hooks_only",
491
+ is_flag=True,
492
+ help="Only set up Claude Code hooks.",
493
+ )
494
+ @click.option(
495
+ "--statusline",
496
+ "statusline_only",
497
+ is_flag=True,
498
+ help="Only configure erk-statusline in Claude Code.",
499
+ )
500
+ @click.option(
501
+ "--no-interactive",
502
+ "no_interactive",
503
+ is_flag=True,
504
+ help="Skip all interactive prompts (gitignore, permissions, hooks, shell setup).",
505
+ )
506
+ @click.option(
507
+ "--with-dignified-review",
508
+ "with_dignified_review",
509
+ is_flag=True,
510
+ help="Install dignified-python skill and review workflow.",
511
+ )
512
+ @click.pass_obj
513
+ def init_cmd(
514
+ ctx: ErkContext,
515
+ force: bool,
516
+ preset: str,
517
+ list_presets: bool,
518
+ shell: bool,
519
+ hooks_only: bool,
520
+ statusline_only: bool,
521
+ no_interactive: bool,
522
+ with_dignified_review: bool,
523
+ ) -> None:
524
+ """Initialize erk for this repo and scaffold config.toml.
525
+
526
+ Runs in three sequential steps:
527
+ 1. Repo verification - checks that you're in a git repository
528
+ 2. Project setup - erk-ifies the repo (if not already)
529
+ 3. User setup - configures shell integration and Claude Code status line
530
+ """
531
+ # Discover available presets on demand (needed for --list-presets)
532
+ presets_dir = _get_presets_dir()
533
+ available_presets = discover_presets(presets_dir)
534
+ valid_choices = ["auto"] + available_presets
535
+
536
+ # Handle --list-presets flag (doesn't require repo)
537
+ if list_presets:
538
+ user_output("Available presets:")
539
+ for p in available_presets:
540
+ user_output(f" - {p}")
541
+ return
542
+
543
+ # Handle --shell flag: only do shell setup (doesn't require repo)
544
+ if shell:
545
+ if ctx.global_config is None:
546
+ config_path = ctx.erk_installation.config_path()
547
+ user_output(f"Global config not found at {config_path}")
548
+ user_output("Run 'erk init' without --shell to create global config first.")
549
+ raise SystemExit(1)
550
+
551
+ setup_complete = perform_shell_setup(ctx.shell)
552
+ if setup_complete:
553
+ # Show what we're about to write
554
+ config_path = ctx.erk_installation.config_path()
555
+ user_output("\nTo remember that shell setup is complete, erk needs to update:")
556
+ user_output(f" {config_path}")
557
+
558
+ if not user_confirm("Proceed with updating global config?", default=False):
559
+ user_output("\nShell integration instructions shown above.")
560
+ user_output("Run 'erk init --shell' to save this preference.")
561
+ return
562
+
563
+ # Update global config with shell_setup_complete=True
564
+ new_config = GlobalConfig(
565
+ erk_root=ctx.global_config.erk_root,
566
+ use_graphite=ctx.global_config.use_graphite,
567
+ shell_setup_complete=True,
568
+ show_pr_info=ctx.global_config.show_pr_info,
569
+ github_planning=ctx.global_config.github_planning,
570
+ )
571
+ try:
572
+ ctx.erk_installation.save_config(new_config)
573
+ user_output(click.style("✓", fg="green") + " Global config updated")
574
+ except PermissionError as e:
575
+ user_output(click.style("\n❌ Error: ", fg="red") + "Could not save global config")
576
+ user_output(str(e))
577
+ user_output("\nShell integration instructions shown above.")
578
+ user_output("You can use them now - erk just couldn't save.")
579
+ raise SystemExit(1) from e
580
+ return
581
+
582
+ # =========================================================================
583
+ # STEP 1: Repo Verification
584
+ # =========================================================================
585
+ user_output("\nStep 1: Checking repository...")
586
+
587
+ # Check if we're in a git repo (before any other setup)
588
+ # Use a temporary erk_root for discovery - will be replaced after global config setup
589
+ temp_erk_root = Path.home() / ".erk"
590
+ repo_or_sentinel = discover_repo_or_sentinel(ctx.cwd, temp_erk_root, RealGit())
591
+
592
+ if isinstance(repo_or_sentinel, NoRepoSentinel):
593
+ user_output(click.style("Error: ", fg="red") + "Not in a git repository.")
594
+ user_output("Run 'erk init' from within a git repository.")
595
+ raise SystemExit(1)
596
+
597
+ # We have a valid repo - extract the root for display
598
+ repo_root = repo_or_sentinel.root
599
+ user_output(click.style("✓", fg="green") + f" Git repository detected: {repo_root.name}")
600
+
601
+ # Handle --hooks flag: only do hook setup
602
+ if hooks_only:
603
+ offer_claude_hook_setup(repo_root)
604
+ return
605
+
606
+ # Handle --statusline flag: only do statusline setup
607
+ if statusline_only:
608
+ perform_statusline_setup(settings_path=None)
609
+ return
610
+
611
+ # Validate preset choice
612
+ if preset not in valid_choices:
613
+ user_output(f"Invalid preset '{preset}'. Available options: {', '.join(valid_choices)}")
614
+ raise SystemExit(1)
615
+
616
+ # =========================================================================
617
+ # STEP 2: Project Configuration
618
+ # =========================================================================
619
+ user_output("\nStep 2: Project configuration...")
620
+
621
+ # Check if repo is already erk-ified
622
+ already_erkified = is_repo_erk_ified(repo_root)
623
+
624
+ if already_erkified and not force:
625
+ user_output(click.style("✓", fg="green") + " Repository already configured for erk")
626
+ else:
627
+ # Check for global config first
628
+ if not ctx.erk_installation.config_exists():
629
+ config_path = ctx.erk_installation.config_path()
630
+ user_output(f" Global config not found at {config_path}")
631
+ user_output(" Please provide the path for your .erk folder.")
632
+ user_output(" (This directory will contain worktrees for each repository)")
633
+ default_erk_root = Path.home() / ".erk"
634
+ erk_root = click.prompt(" .erk folder", type=Path, default=str(default_erk_root))
635
+ erk_root = erk_root.expanduser().resolve()
636
+ config = create_and_save_global_config(ctx, erk_root, shell_setup_complete=False)
637
+ # Update context with newly created config
638
+ ctx = dataclasses.replace(ctx, global_config=config)
639
+ user_output(f" Created global config at {config_path}")
640
+ # Show graphite status on first init
641
+ has_graphite = detect_graphite(ctx.shell)
642
+ if has_graphite:
643
+ user_output(" Graphite (gt) detected - will use 'gt create' for new branches")
644
+ else:
645
+ user_output(" Graphite (gt) not detected - will use 'git' for branch creation")
646
+
647
+ # Now re-discover repo with correct erk_root
648
+ if ctx.global_config is not None:
649
+ repo_context = discover_repo_context(ctx, ctx.cwd)
650
+ else:
651
+ # Fallback (shouldn't happen, but defensive)
652
+ repo_context = repo_or_sentinel
653
+
654
+ # Ensure .erk directory exists
655
+ erk_dir = repo_context.root / ".erk"
656
+ erk_dir.mkdir(parents=True, exist_ok=True)
657
+
658
+ # All repo config now goes to .erk/config.toml (consolidated location)
659
+ cfg_path = erk_dir / "config.toml"
660
+
661
+ # Also ensure metadata directory exists (needed for worktrees dir)
662
+ ensure_erk_metadata_dir(repo_context)
663
+
664
+ effective_preset: str | None
665
+ choice = preset.lower()
666
+ if choice == "auto":
667
+ is_dagster = is_repo_named(repo_context.root, "dagster")
668
+ effective_preset = "dagster" if is_dagster else "generic"
669
+ else:
670
+ effective_preset = choice
671
+
672
+ content = render_config_template(presets_dir, effective_preset)
673
+ cfg_path.write_text(content, encoding="utf-8")
674
+ user_output(f" Wrote {cfg_path}")
675
+
676
+ # Create required version file
677
+ version_file = erk_dir / "required-erk-uv-tool-version"
678
+ version_file.write_text(f"{get_current_version()}\n", encoding="utf-8")
679
+ user_output(f" Wrote {version_file}")
680
+
681
+ # Sync artifacts (skills, commands, agents, workflows, actions)
682
+ sync_result = sync_artifacts(repo_context.root, force=False)
683
+ if sync_result.success:
684
+ user_output(click.style(" ✓ ", fg="green") + sync_result.message)
685
+ else:
686
+ # Non-fatal: warn but continue init
687
+ warn_msg = f"Artifact sync failed: {sync_result.message}"
688
+ user_output(click.style(" ⚠ ", fg="yellow") + warn_msg)
689
+ user_output(" Run 'erk artifact sync' to retry")
690
+
691
+ # Sync optional dignified-review feature if requested
692
+ if with_dignified_review:
693
+ from erk.artifacts.sync import sync_dignified_review
694
+
695
+ dr_result = sync_dignified_review(repo_context.root)
696
+ if dr_result.success:
697
+ user_output(click.style(" ✓ ", fg="green") + dr_result.message)
698
+ else:
699
+ warn_msg = f"dignified-review sync failed: {dr_result.message}"
700
+ user_output(click.style(" ⚠ ", fg="yellow") + warn_msg)
701
+
702
+ # Create prompt hooks directory with README
703
+ _create_prompt_hooks_directory(repo_root=repo_context.root)
704
+
705
+ # Skip interactive prompts if requested
706
+ interactive = not no_interactive
707
+
708
+ # Track backup files for cleanup at end
709
+ pending_backup: Path | NoBackupCreated = NoBackupCreated()
710
+
711
+ if interactive:
712
+ _run_gitignore_prompts(repo_context.root)
713
+ pending_backup = offer_claude_permission_setup(repo_context.root)
714
+ offer_claude_hook_setup(repo_context.root)
715
+
716
+ # Check if plans_repo is configured and offer label setup
717
+ from erk.cli.config import load_config as load_repo_config
718
+
719
+ repo_config = load_repo_config(repo_context.root)
720
+ if repo_config.plans_repo is not None:
721
+ offer_plans_repo_label_setup(repo_context.root, repo_config.plans_repo)
722
+
723
+ # Offer to clean up any pending backup files (at end of project setup)
724
+ if not isinstance(pending_backup, NoBackupCreated):
725
+ offer_backup_cleanup(pending_backup)
726
+
727
+ # =========================================================================
728
+ # STEP 3: User Configuration (always runs)
729
+ # =========================================================================
730
+ user_output("\nStep 3: User configuration...")
731
+
732
+ # Skip interactive prompts if requested
733
+ interactive = not no_interactive
734
+
735
+ # 3a. Global config (if not exists) - already handled in step 2 if needed
736
+
737
+ # 3b. Shell integration
738
+ if interactive:
739
+ # Only check if global config exists
740
+ if ctx.global_config is not None or ctx.erk_installation.config_exists():
741
+ config_exists = ctx.erk_installation.config_exists()
742
+ fresh_config = ctx.erk_installation.load_config() if config_exists else None
743
+ if fresh_config is not None and not fresh_config.shell_setup_complete:
744
+ # Check if shell integration is already in the RC file
745
+ shell_info = ctx.shell.detect_shell()
746
+ already_in_rc = False
747
+ if shell_info is not None:
748
+ shell_name, rc_path = shell_info
749
+ already_in_rc = has_shell_integration_in_rc(rc_path)
750
+ if already_in_rc:
751
+ # Already configured - just show message and update config flag
752
+ msg = f" Shell integration already configured ({shell_name})"
753
+ user_output(click.style("✓", fg="green") + msg)
754
+ # Update global config to remember this
755
+ new_config = GlobalConfig(
756
+ erk_root=fresh_config.erk_root,
757
+ use_graphite=fresh_config.use_graphite,
758
+ shell_setup_complete=True,
759
+ show_pr_info=fresh_config.show_pr_info,
760
+ github_planning=fresh_config.github_planning,
761
+ )
762
+ ctx.erk_installation.save_config(new_config)
763
+
764
+ if not already_in_rc:
765
+ setup_complete = perform_shell_setup(ctx.shell)
766
+ if setup_complete:
767
+ # Show what we're about to write
768
+ config_path = ctx.erk_installation.config_path()
769
+ shell_msg = "To remember that shell setup is complete, erk needs to update:"
770
+ user_output(f"\n {shell_msg}")
771
+ user_output(f" {config_path}")
772
+
773
+ prompt = " Proceed with updating global config?"
774
+ if not user_confirm(prompt, default=False):
775
+ user_output("\n Shell integration instructions shown above.")
776
+ user_output(" Run 'erk init --shell' to save this preference.")
777
+ else:
778
+ # Update global config with shell_setup_complete=True
779
+ new_config = GlobalConfig(
780
+ erk_root=fresh_config.erk_root,
781
+ use_graphite=fresh_config.use_graphite,
782
+ shell_setup_complete=True,
783
+ show_pr_info=fresh_config.show_pr_info,
784
+ github_planning=fresh_config.github_planning,
785
+ )
786
+ try:
787
+ ctx.erk_installation.save_config(new_config)
788
+ msg = click.style(" ✓", fg="green") + " Global config updated"
789
+ user_output(msg)
790
+ except PermissionError as e:
791
+ error_msg = "Could not save global config"
792
+ user_output(click.style("\n ❌ Error: ", fg="red") + error_msg)
793
+ user_output(f" {e}")
794
+ user_output("\n Shell integration instructions shown above.")
795
+ user_output(" You can use them now - erk just couldn't save.")
796
+
797
+ # 3c. Status line configuration
798
+ if interactive:
799
+ perform_statusline_setup(settings_path=None)
800
+
801
+ user_output(click.style("\n✓", fg="green") + " Initialization complete!")