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,121 @@
1
+ """Sync agent documentation index files.
2
+
3
+ This command generates index.md files for docs/learned/ from frontmatter metadata.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from erk.agent_docs.operations import sync_agent_docs
11
+ from erk.cli.subprocess_utils import run_with_error_reporting
12
+
13
+
14
+ @click.command(name="sync")
15
+ @click.option(
16
+ "--dry-run",
17
+ is_flag=True,
18
+ help="Show what would be done without writing files.",
19
+ )
20
+ @click.option(
21
+ "--check",
22
+ is_flag=True,
23
+ help="Check if files are in sync without writing. Exit 1 if changes needed.",
24
+ )
25
+ def sync_command(*, dry_run: bool, check: bool) -> None:
26
+ """Regenerate index files from frontmatter.
27
+
28
+ Generates index.md files for:
29
+ - docs/learned/index.md (root index with categories and uncategorized docs)
30
+ - docs/learned/<category>/index.md (for categories with 2+ docs)
31
+
32
+ Index files are auto-generated and should not be manually edited.
33
+
34
+ Exit codes:
35
+ - 0: Sync completed successfully (or --check passes)
36
+ - 1: Error during sync (or --check finds files out of sync)
37
+ """
38
+ # --check implies dry-run behavior
39
+ effective_dry_run = dry_run or check
40
+
41
+ # Find repository root
42
+ result = run_with_error_reporting(
43
+ ["git", "rev-parse", "--show-toplevel"],
44
+ error_prefix="Failed to find repository root",
45
+ troubleshooting=["Ensure you're running from within a git repository"],
46
+ )
47
+ project_root = Path(result.stdout.strip())
48
+
49
+ if not project_root.exists():
50
+ click.echo(click.style("Error: Repository root not found", fg="red"), err=True)
51
+ raise SystemExit(1)
52
+
53
+ agent_docs_dir = project_root / "docs" / "learned"
54
+ if not agent_docs_dir.exists():
55
+ click.echo(click.style("No docs/learned/ directory found", fg="cyan"), err=True)
56
+ raise SystemExit(0)
57
+
58
+ # Sync index files
59
+ sync_result = sync_agent_docs(project_root, dry_run=effective_dry_run)
60
+
61
+ # Report results
62
+ if effective_dry_run:
63
+ click.echo(click.style("Dry run - no files written", fg="cyan", bold=True), err=True)
64
+ click.echo(err=True)
65
+
66
+ total_changes = len(sync_result.created) + len(sync_result.updated)
67
+
68
+ if sync_result.created:
69
+ action = "Would create" if effective_dry_run else "Created"
70
+ click.echo(f"{action} {len(sync_result.created)} file(s):", err=True)
71
+ for path in sync_result.created:
72
+ click.echo(f" + {path}", err=True)
73
+ click.echo(err=True)
74
+
75
+ if sync_result.updated:
76
+ action = "Would update" if effective_dry_run else "Updated"
77
+ click.echo(f"{action} {len(sync_result.updated)} file(s):", err=True)
78
+ for path in sync_result.updated:
79
+ click.echo(f" ~ {path}", err=True)
80
+ click.echo(err=True)
81
+
82
+ if sync_result.unchanged:
83
+ click.echo(f"Unchanged: {len(sync_result.unchanged)} file(s)", err=True)
84
+ click.echo(err=True)
85
+
86
+ # Report tripwires
87
+ if sync_result.tripwires_count > 0:
88
+ click.echo(f"Tripwires: {sync_result.tripwires_count} collected", err=True)
89
+ click.echo(err=True)
90
+
91
+ if sync_result.skipped_invalid > 0:
92
+ click.echo(
93
+ click.style(
94
+ f"Skipped {sync_result.skipped_invalid} doc(s) with invalid frontmatter",
95
+ fg="yellow",
96
+ ),
97
+ err=True,
98
+ )
99
+ click.echo(" Run 'erk docs validate' to see errors", err=True)
100
+ click.echo(err=True)
101
+
102
+ # Summary
103
+ if total_changes == 0 and sync_result.skipped_invalid == 0:
104
+ click.echo(click.style("All files are up to date", fg="green"), err=True)
105
+ elif total_changes > 0:
106
+ if check:
107
+ msg = f"Files out of sync: {total_changes} change(s) needed"
108
+ click.echo(click.style(msg, fg="red", bold=True), err=True)
109
+ click.echo(err=True)
110
+ click.echo("Run 'erk docs sync' to regenerate files from frontmatter.", err=True)
111
+ raise SystemExit(1)
112
+ elif effective_dry_run:
113
+ click.echo(
114
+ click.style(f"Would make {total_changes} change(s)", fg="cyan", bold=True),
115
+ err=True,
116
+ )
117
+ else:
118
+ click.echo(
119
+ click.style(f"Sync complete: {total_changes} change(s)", fg="green"),
120
+ err=True,
121
+ )
@@ -0,0 +1,102 @@
1
+ """Validate agent documentation frontmatter.
2
+
3
+ This command validates that all markdown files in docs/learned/ have valid
4
+ frontmatter with required fields: title and read_when.
5
+ """
6
+
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from erk.agent_docs.operations import validate_agent_docs
13
+
14
+
15
+ @click.command(name="validate")
16
+ @click.option(
17
+ "--verbose",
18
+ "-v",
19
+ is_flag=True,
20
+ help="Show details for all files, not just errors.",
21
+ )
22
+ def validate_command(*, verbose: bool) -> None:
23
+ """Validate agent documentation frontmatter.
24
+
25
+ Checks that all markdown files in docs/learned/ have valid frontmatter:
26
+ - title: Human-readable document title
27
+ - read_when: List of conditions when agent should read this doc
28
+
29
+ Index files (index.md) are skipped as they are auto-generated.
30
+
31
+ Exit codes:
32
+ - 0: All files are valid
33
+ - 1: Validation errors found
34
+ """
35
+ # Find repository root
36
+ result = subprocess.run(
37
+ ["git", "rev-parse", "--show-toplevel"],
38
+ check=True,
39
+ capture_output=True,
40
+ text=True,
41
+ )
42
+ project_root = Path(result.stdout.strip())
43
+
44
+ if not project_root.exists():
45
+ click.echo(click.style("Error: Repository root not found", fg="red"), err=True)
46
+ raise SystemExit(1)
47
+
48
+ agent_docs_dir = project_root / "docs" / "learned"
49
+ if not agent_docs_dir.exists():
50
+ click.echo(click.style("No docs/learned/ directory found", fg="cyan"), err=True)
51
+ raise SystemExit(0)
52
+
53
+ # Validate all files
54
+ results = validate_agent_docs(project_root)
55
+
56
+ if len(results) == 0:
57
+ click.echo(click.style("No agent documentation files found", fg="cyan"), err=True)
58
+ raise SystemExit(0)
59
+
60
+ valid_count = sum(1 for r in results if r.is_valid)
61
+ invalid_count = len(results) - valid_count
62
+
63
+ # Show results
64
+ if verbose or invalid_count > 0:
65
+ for validation_result in results:
66
+ if validation_result.is_valid:
67
+ if verbose:
68
+ status = click.style("OK", fg="green")
69
+ click.echo(f"{status} {validation_result.file_path}", err=True)
70
+ else:
71
+ status = click.style("FAIL", fg="red")
72
+ click.echo(f"{status} {validation_result.file_path}", err=True)
73
+ for error in validation_result.errors:
74
+ click.echo(f" {error}", err=True)
75
+
76
+ # Summary
77
+ click.echo(err=True)
78
+ if invalid_count == 0:
79
+ click.echo(
80
+ click.style("Agent docs validation: PASSED", fg="green", bold=True),
81
+ err=True,
82
+ )
83
+ click.echo(err=True)
84
+ click.echo(f"Files validated: {len(results)}", err=True)
85
+ click.echo("All files have valid frontmatter!", err=True)
86
+ else:
87
+ click.echo(
88
+ click.style("Agent docs validation: FAILED", fg="red", bold=True),
89
+ err=True,
90
+ )
91
+ click.echo(err=True)
92
+ click.echo(f"Files validated: {len(results)}", err=True)
93
+ click.echo(f" Valid: {valid_count}", err=True)
94
+ click.echo(f" Invalid: {invalid_count}", err=True)
95
+ click.echo(err=True)
96
+ click.echo("Required frontmatter format:", err=True)
97
+ click.echo(" ---", err=True)
98
+ click.echo(" title: Document Title", err=True)
99
+ click.echo(" read_when:", err=True)
100
+ click.echo(' - "when to read this doc"', err=True)
101
+ click.echo(" ---", err=True)
102
+ raise SystemExit(1)
@@ -0,0 +1,243 @@
1
+ """Doctor command for erk setup diagnostics.
2
+
3
+ Runs health checks on the erk setup to identify issues with
4
+ CLI availability, repository configuration, and Claude settings.
5
+ """
6
+
7
+ import click
8
+
9
+ from erk.core.context import ErkContext
10
+ from erk.core.health_checks import CheckResult, run_all_checks
11
+ from erk.core.health_checks_dogfooder import EARLY_DOGFOODER_CHECK_NAMES
12
+ from erk_shared.hooks.logging import clear_hook_logs
13
+
14
+ # Sub-group definitions for Repository Setup condensed display
15
+ REPO_SUBGROUPS: dict[str, set[str]] = {
16
+ "Git repository": {"repository", "gitignore"},
17
+ "Claude settings": {
18
+ "claude-erk-permission",
19
+ "claude-settings",
20
+ "user-prompt-hook",
21
+ "exit-plan-hook",
22
+ },
23
+ "Erk configuration": {
24
+ "required-version",
25
+ "legacy-prompt-hooks",
26
+ "legacy-config",
27
+ "managed-artifacts",
28
+ "post-plan-implement-ci-hook",
29
+ },
30
+ "GitHub": {"workflow-permissions"},
31
+ "Hooks": {"hooks"},
32
+ }
33
+
34
+ # Sub-group definitions for User Setup condensed display
35
+ USER_SUBGROUPS: dict[str, set[str]] = {
36
+ "User checks": {"github-auth", "claude-hooks", "statusline", "shell-integration"},
37
+ }
38
+
39
+
40
+ def _format_check_result(result: CheckResult, indent: str = "", verbose: bool = False) -> None:
41
+ """Format and display a single check result.
42
+
43
+ Args:
44
+ result: The check result to format
45
+ indent: Optional indentation prefix for nested display
46
+ verbose: If True and verbose_details exists, use it instead of details
47
+ """
48
+ if not result.passed:
49
+ icon = click.style("❌", fg="red")
50
+ elif result.warning:
51
+ icon = click.style("⚠️", fg="yellow")
52
+ elif result.info:
53
+ icon = click.style("ℹ️", fg="cyan")
54
+ else:
55
+ icon = click.style("✅", fg="green")
56
+
57
+ # Use verbose_details in verbose mode if available, otherwise use details
58
+ details = result.verbose_details if verbose and result.verbose_details else result.details
59
+
60
+ if details and "\n" not in details:
61
+ # Single-line details: show inline
62
+ styled_details = click.style(f" - {details}", dim=True)
63
+ click.echo(f"{indent}{icon} {result.message}{styled_details}")
64
+ else:
65
+ click.echo(f"{indent}{icon} {result.message}")
66
+ if details:
67
+ # Multi-line details: show with indentation
68
+ for line in details.split("\n"):
69
+ click.echo(click.style(f"{indent} {line}", dim=True))
70
+
71
+
72
+ def _format_subgroup(name: str, checks: list[CheckResult], verbose: bool, indent: str = "") -> None:
73
+ """Format a sub-group of checks (condensed or expanded).
74
+
75
+ Args:
76
+ name: Sub-group display name
77
+ checks: List of check results in this sub-group
78
+ verbose: If True, always show all individual checks
79
+ indent: Indentation prefix
80
+ """
81
+ if not checks:
82
+ return
83
+
84
+ passed = sum(1 for c in checks if c.passed)
85
+ total = len(checks)
86
+ all_passed = passed == total
87
+
88
+ if verbose:
89
+ # Always show all individual checks with sub-group header
90
+ click.echo(click.style(f"{indent} {name}", dim=True))
91
+ for result in checks:
92
+ _format_check_result(result, indent=f"{indent} ", verbose=True)
93
+ elif all_passed:
94
+ # Condensed: single line with count
95
+ icon = click.style("✅", fg="green")
96
+ click.echo(f"{indent}{icon} {name} ({total} checks)")
97
+ else:
98
+ # Failed: show summary line + expand failures
99
+ icon = click.style("❌", fg="red")
100
+ click.echo(f"{indent}{icon} {name} ({passed}/{total} checks)")
101
+ for result in checks:
102
+ if not result.passed:
103
+ _format_check_result(result, indent=f"{indent} ", verbose=False)
104
+
105
+
106
+ @click.command("doctor")
107
+ @click.option("-v", "--verbose", is_flag=True, help="Show all individual checks")
108
+ @click.option("--dogfooder", is_flag=True, help="Include early dogfooder migration checks")
109
+ @click.option(
110
+ "--clear-hook-logs", "clear_hook_logs_flag", is_flag=True, help="Clear all hook execution logs"
111
+ )
112
+ @click.pass_obj
113
+ def doctor_cmd(ctx: ErkContext, verbose: bool, dogfooder: bool, clear_hook_logs_flag: bool) -> None:
114
+ """Run diagnostic checks on erk setup.
115
+
116
+ Checks for:
117
+
118
+ \b
119
+ - Repository Setup: git config, Claude settings, erk config, hooks
120
+ - User Setup: prerequisites (erk, claude, gt, gh, uv), GitHub auth
121
+
122
+ Examples:
123
+
124
+ \b
125
+ # Run checks (condensed output)
126
+ erk doctor
127
+
128
+ # Show all individual checks
129
+ erk doctor --verbose
130
+
131
+ # Include early dogfooder migration checks
132
+ erk doctor --dogfooder
133
+
134
+ # Clear hook execution logs
135
+ erk doctor --clear-hook-logs
136
+ """
137
+ # Handle --clear-hook-logs flag (clears logs and returns early)
138
+ if clear_hook_logs_flag:
139
+ deleted_count = clear_hook_logs(ctx.repo_root)
140
+ click.echo(f"Cleared {deleted_count} hook log(s)")
141
+ return
142
+
143
+ click.echo(click.style("🔍 Checking erk setup...", bold=True))
144
+ click.echo("")
145
+
146
+ # Run all checks
147
+ results = run_all_checks(ctx)
148
+
149
+ # Group results by category
150
+ prerequisite_names = {"erk", "claude", "graphite", "github", "uv"}
151
+ user_check_names = {"github-auth", "claude-hooks", "statusline", "shell-integration"}
152
+ repo_check_names = {
153
+ "repository",
154
+ "claude-settings",
155
+ "user-prompt-hook",
156
+ "exit-plan-hook",
157
+ "gitignore",
158
+ "claude-erk-permission",
159
+ "legacy-config",
160
+ "required-version",
161
+ "legacy-prompt-hooks",
162
+ "managed-artifacts",
163
+ "post-plan-implement-ci-hook",
164
+ "workflow-permissions",
165
+ "hooks",
166
+ }
167
+
168
+ prerequisite_checks = [r for r in results if r.name in prerequisite_names]
169
+ user_checks = [r for r in results if r.name in user_check_names]
170
+ repo_checks = [r for r in results if r.name in repo_check_names]
171
+ early_dogfooder_checks = [r for r in results if r.name in EARLY_DOGFOODER_CHECK_NAMES]
172
+
173
+ # Track displayed check names to catch any uncategorized checks
174
+ displayed_names = (
175
+ prerequisite_names | user_check_names | repo_check_names | EARLY_DOGFOODER_CHECK_NAMES
176
+ )
177
+
178
+ # Display Repository Setup FIRST (with sub-groups)
179
+ click.echo(click.style("Repository Setup", bold=True))
180
+ if verbose:
181
+ # In verbose mode, show sub-groups with all individual checks
182
+ for subgroup_name, subgroup_check_names in REPO_SUBGROUPS.items():
183
+ subgroup_checks = [r for r in repo_checks if r.name in subgroup_check_names]
184
+ _format_subgroup(subgroup_name, subgroup_checks, verbose=True)
185
+ else:
186
+ # Condensed mode: show sub-group summaries
187
+ for subgroup_name, subgroup_check_names in REPO_SUBGROUPS.items():
188
+ subgroup_checks = [r for r in repo_checks if r.name in subgroup_check_names]
189
+ _format_subgroup(subgroup_name, subgroup_checks, verbose=False)
190
+ click.echo("")
191
+
192
+ # Display Early Dogfooder checks (only when --dogfooder flag is passed)
193
+ if dogfooder and early_dogfooder_checks:
194
+ click.echo(click.style("Early Dogfooder", bold=True))
195
+ for result in early_dogfooder_checks:
196
+ _format_check_result(result, verbose=verbose)
197
+ click.echo("")
198
+
199
+ # Display User Setup SECOND
200
+ click.echo(click.style("User Setup", bold=True))
201
+ # Prerequisites (always expanded)
202
+ for result in prerequisite_checks:
203
+ _format_check_result(result, verbose=verbose)
204
+ # User checks (condensable subgroup)
205
+ if verbose:
206
+ for subgroup_name, subgroup_check_names in USER_SUBGROUPS.items():
207
+ subgroup_checks = [r for r in user_checks if r.name in subgroup_check_names]
208
+ _format_subgroup(subgroup_name, subgroup_checks, verbose=True)
209
+ else:
210
+ for subgroup_name, subgroup_check_names in USER_SUBGROUPS.items():
211
+ subgroup_checks = [r for r in user_checks if r.name in subgroup_check_names]
212
+ _format_subgroup(subgroup_name, subgroup_checks, verbose=False)
213
+ click.echo("")
214
+
215
+ # Display any uncategorized checks (defensive - catches missing categorization)
216
+ other_checks = [r for r in results if r.name not in displayed_names]
217
+ if other_checks:
218
+ click.echo(click.style("Other Checks", bold=True))
219
+ for result in other_checks:
220
+ _format_check_result(result, verbose=verbose)
221
+ click.echo("")
222
+
223
+ # Collect and display consolidated remediations for failing checks
224
+ remediations = {r.remediation for r in results if r.remediation and not r.passed}
225
+ if remediations:
226
+ click.echo(click.style("Remediation", bold=True))
227
+ for remediation in sorted(remediations):
228
+ click.echo(f" {remediation}")
229
+ click.echo("")
230
+
231
+ # Calculate summary - exclude dogfooder checks from total if not showing them
232
+ checks_for_summary = [r for r in results if r.name not in EARLY_DOGFOODER_CHECK_NAMES]
233
+ if dogfooder:
234
+ checks_for_summary = results
235
+
236
+ passed = sum(1 for r in checks_for_summary if r.passed)
237
+ total = len(checks_for_summary)
238
+ failed = total - passed
239
+
240
+ if failed == 0:
241
+ click.echo(click.style("✨ All checks passed!", fg="green", bold=True))
242
+ else:
243
+ click.echo(click.style(f"⚠️ {failed} check(s) failed", fg="yellow", bold=True))
@@ -0,0 +1,171 @@
1
+ import click
2
+
3
+ from erk.cli.commands.navigation_helpers import (
4
+ activate_root_repo,
5
+ activate_worktree,
6
+ check_clean_working_tree,
7
+ check_pending_extraction_marker,
8
+ render_activation_script,
9
+ resolve_down_navigation,
10
+ unallocate_worktree_and_branch,
11
+ verify_pr_closed_or_merged,
12
+ )
13
+ from erk.cli.core import discover_repo_context
14
+ from erk.cli.ensure import Ensure
15
+ from erk.cli.graphite_command import GraphiteCommandWithHiddenOptions
16
+ from erk.cli.help_formatter import script_option
17
+ from erk.core.context import ErkContext
18
+ from erk.core.worktree_utils import compute_relative_path_in_worktree
19
+ from erk_shared.output.output import machine_output, user_output
20
+
21
+
22
+ @click.command("down", cls=GraphiteCommandWithHiddenOptions)
23
+ @script_option
24
+ @click.option(
25
+ "--delete-current",
26
+ is_flag=True,
27
+ help="Delete current branch and worktree after navigating down",
28
+ )
29
+ @click.option(
30
+ "-f",
31
+ "--force",
32
+ is_flag=True,
33
+ help="Force deletion even if marker exists or PR is open (prompts)",
34
+ )
35
+ @click.pass_obj
36
+ def down_cmd(ctx: ErkContext, script: bool, delete_current: bool, force: bool) -> None:
37
+ """Move to parent branch in worktree stack.
38
+
39
+ With shell integration (recommended):
40
+ erk down
41
+
42
+ The shell wrapper function automatically activates the worktree.
43
+ Run 'erk init --shell' to set up shell integration.
44
+
45
+ Without shell integration:
46
+ source <(erk down --script)
47
+
48
+ This will cd to the parent branch's worktree (or root repo if parent is trunk),
49
+ create/activate .venv, and load .env variables.
50
+ Requires Graphite to be enabled: 'erk config set use_graphite true'
51
+ """
52
+ # Validate preconditions upfront (LBYL)
53
+ Ensure.gh_authenticated(ctx)
54
+
55
+ repo = discover_repo_context(ctx, ctx.cwd)
56
+ trunk_branch = ctx.trunk_branch
57
+
58
+ # Get current branch
59
+ current_branch = Ensure.not_none(
60
+ ctx.git.get_current_branch(ctx.cwd), "Not currently on a branch (detached HEAD)"
61
+ )
62
+
63
+ # Store current worktree path for deletion (before navigation)
64
+ # Find the worktree for the current branch
65
+ current_worktree_path = None
66
+ if delete_current:
67
+ current_worktree_path = Ensure.not_none(
68
+ ctx.git.find_worktree_for_branch(repo.root, current_branch),
69
+ f"Cannot find worktree for current branch '{current_branch}'.",
70
+ )
71
+
72
+ # Safety checks before navigation (if --delete-current flag is set)
73
+ if delete_current and current_worktree_path is not None:
74
+ check_clean_working_tree(ctx)
75
+ verify_pr_closed_or_merged(ctx, repo.root, current_branch, force)
76
+ # Check for pending extraction marker
77
+ check_pending_extraction_marker(current_worktree_path, force)
78
+
79
+ # Get all worktrees for checking if target has a worktree
80
+ worktrees = ctx.git.list_worktrees(repo.root)
81
+
82
+ # Resolve navigation to get target branch or 'root' (may auto-create worktree)
83
+ target_name, was_created = resolve_down_navigation(
84
+ ctx, repo, current_branch, worktrees, trunk_branch
85
+ )
86
+
87
+ # Show creation message if worktree was just created
88
+ if was_created and not script:
89
+ user_output(
90
+ click.style("✓", fg="green")
91
+ + f" Created worktree for {click.style(target_name, fg='yellow')} and moved to it"
92
+ )
93
+
94
+ # Check if target_name refers to 'root' which means root repo
95
+ if target_name == "root":
96
+ if delete_current and current_worktree_path is not None:
97
+ # Handle activation inline so we can do cleanup before exiting
98
+ root_path = repo.root
99
+ if script:
100
+ script_content = render_activation_script(
101
+ worktree_path=root_path,
102
+ target_subpath=compute_relative_path_in_worktree(worktrees, ctx.cwd),
103
+ post_cd_commands=None,
104
+ final_message='echo "Went to root repo: $(pwd)"',
105
+ comment="work activate-script (root repo)",
106
+ )
107
+ result = ctx.script_writer.write_activation_script(
108
+ script_content,
109
+ command_name="down",
110
+ comment="activate root",
111
+ )
112
+ machine_output(str(result.path), nl=False)
113
+ else:
114
+ user_output(f"Went to root repo: {root_path}")
115
+
116
+ # Perform cleanup (no context regeneration needed - we haven't changed dirs)
117
+ unallocate_worktree_and_branch(ctx, repo, current_branch, current_worktree_path)
118
+
119
+ # Exit after cleanup
120
+ raise SystemExit(0)
121
+ else:
122
+ # No cleanup needed, use standard activation
123
+ activate_root_repo(ctx, repo, script, "down", post_cd_commands=None)
124
+
125
+ # Resolve target branch to actual worktree path
126
+ target_wt_path = Ensure.not_none(
127
+ ctx.git.find_worktree_for_branch(repo.root, target_name),
128
+ f"Branch '{target_name}' has no worktree. This should not happen.",
129
+ )
130
+
131
+ if delete_current and current_worktree_path is not None:
132
+ # Handle activation inline so we can do cleanup before exiting
133
+ Ensure.path_exists(ctx, target_wt_path, f"Worktree not found: {target_wt_path}")
134
+
135
+ if script:
136
+ activation_script = render_activation_script(
137
+ worktree_path=target_wt_path,
138
+ target_subpath=compute_relative_path_in_worktree(worktrees, ctx.cwd),
139
+ post_cd_commands=None,
140
+ final_message='echo "Activated worktree: $(pwd)"',
141
+ comment="work activate-script",
142
+ )
143
+ result = ctx.script_writer.write_activation_script(
144
+ activation_script,
145
+ command_name="down",
146
+ comment=f"activate {target_wt_path.name}",
147
+ )
148
+ machine_output(str(result.path), nl=False)
149
+ else:
150
+ user_output(
151
+ "Shell integration not detected. "
152
+ "Run 'erk init --shell' to set up automatic activation."
153
+ )
154
+ user_output("\nOr use: source <(erk down --script)")
155
+
156
+ # Perform cleanup (no context regeneration needed - we haven't actually changed directories)
157
+ unallocate_worktree_and_branch(ctx, repo, current_branch, current_worktree_path)
158
+
159
+ # Exit after cleanup
160
+ raise SystemExit(0)
161
+ else:
162
+ # No cleanup needed, use standard activation
163
+ activate_worktree(
164
+ ctx=ctx,
165
+ repo=repo,
166
+ target_path=target_wt_path,
167
+ script=script,
168
+ command_name="down",
169
+ preserve_relative_path=True,
170
+ post_cd_commands=None,
171
+ )
@@ -0,0 +1 @@
1
+ """Static exec group for erk scripts."""