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,260 @@
1
+ """Check artifact sync status."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.artifacts.artifact_health import (
8
+ ArtifactStatus,
9
+ find_missing_artifacts,
10
+ find_orphaned_artifacts,
11
+ get_artifact_health,
12
+ is_erk_managed,
13
+ )
14
+ from erk.artifacts.discovery import discover_artifacts
15
+ from erk.artifacts.models import InstalledArtifact
16
+ from erk.artifacts.staleness import check_staleness
17
+ from erk.artifacts.state import load_artifact_state
18
+
19
+
20
+ def _display_orphan_warnings(orphans: dict[str, list[str]]) -> None:
21
+ """Display orphan warnings with remediation commands."""
22
+ total_orphans = sum(len(files) for files in orphans.values())
23
+ click.echo(click.style("⚠️ ", fg="yellow") + f"Found {total_orphans} orphaned artifact(s)")
24
+ click.echo(" Orphaned files (not in current erk package):")
25
+ for folder, files in sorted(orphans.items()):
26
+ click.echo(f" {folder}/:")
27
+ for filename in sorted(files):
28
+ click.echo(f" - {filename}")
29
+
30
+ click.echo("")
31
+ click.echo(" To remove:")
32
+ for folder, files in sorted(orphans.items()):
33
+ for filename in sorted(files):
34
+ # Workflows are in .github/, not .claude/
35
+ if folder.startswith(".github"):
36
+ click.echo(f" rm {folder}/{filename}")
37
+ else:
38
+ click.echo(f" rm .claude/{folder}/{filename}")
39
+
40
+
41
+ def _display_missing_warnings(missing: dict[str, list[str]]) -> None:
42
+ """Display missing artifact warnings."""
43
+ total_missing = sum(len(files) for files in missing.values())
44
+ click.echo(click.style("⚠️ ", fg="yellow") + f"Found {total_missing} missing artifact(s)")
45
+ click.echo(" Missing from project:")
46
+ for folder, files in sorted(missing.items()):
47
+ click.echo(f" {folder}:")
48
+ for filename in sorted(files):
49
+ click.echo(f" - {filename}")
50
+ click.echo("")
51
+ click.echo(" Run 'erk artifact sync' to install missing artifacts")
52
+
53
+
54
+ def _format_artifact_path(artifact: InstalledArtifact) -> str:
55
+ """Format artifact as a display path string."""
56
+ if artifact.artifact_type == "command":
57
+ # Commands can be namespaced (local:foo) or top-level (foo)
58
+ if ":" in artifact.name:
59
+ namespace, name = artifact.name.split(":", 1)
60
+ return f"commands/{namespace}/{name}.md"
61
+ return f"commands/{artifact.name}.md"
62
+ if artifact.artifact_type == "skill":
63
+ return f"skills/{artifact.name}"
64
+ if artifact.artifact_type == "agent":
65
+ return f"agents/{artifact.name}"
66
+ if artifact.artifact_type == "workflow":
67
+ return f".github/workflows/{artifact.name}.yml"
68
+ if artifact.artifact_type == "hook":
69
+ return f"hooks/{artifact.name} (settings.json)"
70
+ return artifact.name
71
+
72
+
73
+ def _display_installed_artifacts(project_dir: Path) -> None:
74
+ """Display list of artifacts actually installed in project."""
75
+ artifacts = discover_artifacts(project_dir)
76
+
77
+ if not artifacts:
78
+ click.echo(" (no artifacts installed)")
79
+ return
80
+
81
+ for artifact in artifacts:
82
+ suffix = "" if is_erk_managed(artifact) else " (unmanaged)"
83
+ click.echo(f" {_format_artifact_path(artifact)}{suffix}")
84
+
85
+
86
+ def _format_artifact_status(artifact: ArtifactStatus, show_hashes: bool) -> str:
87
+ """Format artifact status for verbose output.
88
+
89
+ Args:
90
+ artifact: The artifact status to format
91
+ show_hashes: If True, show hash comparison details
92
+ """
93
+ if artifact.status == "up-to-date":
94
+ icon = click.style("✓", fg="green")
95
+ detail = f"{artifact.current_version} (up-to-date)"
96
+ elif artifact.status == "changed-upstream":
97
+ icon = click.style("⚠", fg="yellow")
98
+ if artifact.installed_version:
99
+ detail = f"{artifact.installed_version} → {artifact.current_version} (changed upstream)"
100
+ else:
101
+ detail = f"→ {artifact.current_version} (new in this version)"
102
+ elif artifact.status == "locally-modified":
103
+ icon = click.style("⚠", fg="yellow")
104
+ detail = f"{artifact.current_version} (locally modified)"
105
+ else: # not-installed
106
+ icon = click.style("✗", fg="red")
107
+ detail = "(not installed)"
108
+
109
+ lines = [f" {icon} {artifact.name}: {detail}"]
110
+
111
+ if show_hashes:
112
+ # Show state.toml values
113
+ if artifact.installed_version is not None and artifact.installed_hash is not None:
114
+ ver = artifact.installed_version
115
+ h = artifact.installed_hash
116
+ lines.append(f" state.toml: version={ver}, hash={h}")
117
+ else:
118
+ lines.append(" state.toml: (not tracked)")
119
+
120
+ # Show current source values
121
+ if artifact.current_hash is not None:
122
+ ver = artifact.current_version
123
+ h = artifact.current_hash
124
+ lines.append(f" source: version={ver}, hash={h}")
125
+ else:
126
+ lines.append(" source: (not installed)")
127
+
128
+ return "\n".join(lines)
129
+
130
+
131
+ def _display_verbose_status(project_dir: Path, show_hashes: bool) -> bool:
132
+ """Display per-artifact status breakdown.
133
+
134
+ Shows two sections:
135
+ 1. Erk-managed artifacts with version tracking status
136
+ 2. Project artifacts (local commands, custom skills, etc.)
137
+
138
+ Args:
139
+ project_dir: Path to the project root
140
+ show_hashes: If True, show hash comparison details for each artifact
141
+
142
+ Returns True if any erk-managed artifacts need attention (not up-to-date).
143
+ """
144
+ state = load_artifact_state(project_dir)
145
+ saved_files = dict(state.files) if state else {}
146
+
147
+ health_result = get_artifact_health(project_dir, saved_files)
148
+
149
+ if health_result.skipped_reason is not None:
150
+ return False
151
+
152
+ click.echo("")
153
+ click.echo("Erk-managed artifacts:")
154
+
155
+ has_issues = False
156
+ for artifact in health_result.artifacts:
157
+ click.echo(_format_artifact_status(artifact, show_hashes))
158
+ if artifact.status != "up-to-date":
159
+ has_issues = True
160
+
161
+ # Also show project-specific artifacts (non-erk-managed)
162
+ all_artifacts = discover_artifacts(project_dir)
163
+ project_artifacts = [a for a in all_artifacts if not is_erk_managed(a)]
164
+
165
+ if project_artifacts:
166
+ click.echo("")
167
+ click.echo("Project artifacts (unmanaged):")
168
+ for artifact in project_artifacts:
169
+ click.echo(f" {_format_artifact_path(artifact)}")
170
+
171
+ return has_issues
172
+
173
+
174
+ @click.command("check")
175
+ @click.option(
176
+ "--verbose",
177
+ "-v",
178
+ count=True,
179
+ help="Show per-artifact status. Use -vv to also show hash comparisons.",
180
+ )
181
+ def check_cmd(verbose: int) -> None:
182
+ """Check if artifacts are in sync with erk version.
183
+
184
+ Compares the version recorded in .erk/state.toml against
185
+ the currently installed erk package version. Also checks
186
+ for orphaned files that should be removed.
187
+
188
+ Examples:
189
+
190
+ \b
191
+ # Check sync status
192
+ erk artifact check
193
+
194
+ \b
195
+ # Show per-artifact breakdown
196
+ erk artifact check -v
197
+
198
+ \b
199
+ # Show hash comparisons (state.toml vs source)
200
+ erk artifact check -vv
201
+ """
202
+ project_dir = Path.cwd()
203
+
204
+ staleness_result = check_staleness(project_dir)
205
+ orphan_result = find_orphaned_artifacts(project_dir)
206
+ missing_result = find_missing_artifacts(project_dir)
207
+
208
+ has_errors = False
209
+ show_per_artifact = verbose >= 1
210
+ show_hashes = verbose >= 2
211
+
212
+ # Check staleness
213
+ if staleness_result.reason == "erk-repo":
214
+ click.echo(click.style("✓ ", fg="green") + "Development mode (artifacts read from source)")
215
+ if not show_per_artifact:
216
+ _display_installed_artifacts(project_dir)
217
+ elif staleness_result.reason == "not-initialized":
218
+ click.echo(click.style("⚠️ ", fg="yellow") + "Artifacts not initialized")
219
+ click.echo(f" Current erk version: {staleness_result.current_version}")
220
+ click.echo(" Run 'erk artifact sync' to initialize")
221
+ has_errors = True
222
+ elif staleness_result.reason == "version-mismatch":
223
+ click.echo(click.style("⚠️ ", fg="yellow") + "Artifacts out of sync")
224
+ click.echo(f" Installed version: {staleness_result.installed_version}")
225
+ click.echo(f" Current erk version: {staleness_result.current_version}")
226
+ click.echo(" Run 'erk artifact sync' to update")
227
+ has_errors = True
228
+ else:
229
+ click.echo(
230
+ click.style("✓ ", fg="green")
231
+ + f"Artifacts up to date (v{staleness_result.current_version})"
232
+ )
233
+ if not show_per_artifact:
234
+ _display_installed_artifacts(project_dir)
235
+
236
+ # Show verbose per-artifact breakdown if requested
237
+ if show_per_artifact and staleness_result.reason != "not-initialized":
238
+ verbose_has_issues = _display_verbose_status(project_dir, show_hashes)
239
+ # In dev mode (erk-repo), don't report issues - artifacts come from source
240
+ if verbose_has_issues and staleness_result.reason != "erk-repo":
241
+ has_errors = True
242
+
243
+ # Check for orphans (skip if erk-repo or no-claude-dir)
244
+ if orphan_result.skipped_reason is None:
245
+ if orphan_result.orphans:
246
+ _display_orphan_warnings(orphan_result.orphans)
247
+ has_errors = True
248
+ else:
249
+ click.echo(click.style("✓ ", fg="green") + "No orphaned artifacts")
250
+
251
+ # Check for missing artifacts (skip if erk-repo or no-claude-dir)
252
+ if missing_result.skipped_reason is None:
253
+ if missing_result.missing:
254
+ _display_missing_warnings(missing_result.missing)
255
+ has_errors = True
256
+ else:
257
+ click.echo(click.style("✓ ", fg="green") + "No missing artifacts")
258
+
259
+ if has_errors:
260
+ raise SystemExit(1)
@@ -0,0 +1,31 @@
1
+ """Artifact command group for managing .claude/ artifacts."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.artifact.check import check_cmd
6
+ from erk.cli.commands.artifact.list_cmd import list_cmd
7
+ from erk.cli.commands.artifact.show import show_cmd
8
+ from erk.cli.commands.artifact.sync_cmd import sync_cmd
9
+
10
+
11
+ @click.group(name="artifact")
12
+ def artifact_group() -> None:
13
+ """Manage erk-managed artifacts.
14
+
15
+ Artifacts are Claude Code extensions like skills, commands, agents,
16
+ and workflows stored in your project's .claude/ and .github/ directories.
17
+
18
+ \b
19
+ Commands:
20
+ list List installed artifacts
21
+ show Display artifact content
22
+ sync Sync artifacts from erk package
23
+ check Check if artifacts are up to date
24
+ """
25
+
26
+
27
+ # Register subcommands
28
+ artifact_group.add_command(list_cmd)
29
+ artifact_group.add_command(show_cmd)
30
+ artifact_group.add_command(sync_cmd)
31
+ artifact_group.add_command(check_cmd)
@@ -0,0 +1,89 @@
1
+ """List artifacts installed in the project."""
2
+
3
+ from pathlib import Path
4
+ from typing import cast
5
+
6
+ import click
7
+
8
+ from erk.artifacts.artifact_health import is_erk_managed
9
+ from erk.artifacts.discovery import discover_artifacts
10
+ from erk.artifacts.models import ArtifactType
11
+
12
+
13
+ @click.command("list")
14
+ @click.option(
15
+ "--type",
16
+ "artifact_type",
17
+ type=click.Choice(["skill", "command", "agent", "workflow", "hook"]),
18
+ help="Filter by artifact type",
19
+ )
20
+ @click.option("--verbose", "-v", is_flag=True, help="Show additional details")
21
+ def list_cmd(artifact_type: str | None, verbose: bool) -> None:
22
+ """List all artifacts in project.
23
+
24
+ Examples:
25
+
26
+ \b
27
+ # List all artifacts
28
+ erk artifact list
29
+
30
+ \b
31
+ # List only skills
32
+ erk artifact list --type skill
33
+
34
+ \b
35
+ # List with details
36
+ erk artifact list --verbose
37
+ """
38
+ project_dir = Path.cwd()
39
+ claude_dir = project_dir / ".claude"
40
+ if not claude_dir.exists():
41
+ click.echo("No .claude/ directory found in current directory", err=True)
42
+ raise SystemExit(1)
43
+
44
+ artifacts = discover_artifacts(project_dir)
45
+
46
+ # Filter by type if specified
47
+ if artifact_type is not None:
48
+ assert artifact_type in ("skill", "command", "agent", "workflow", "hook")
49
+ typed_filter = cast(ArtifactType, artifact_type)
50
+ artifacts = [a for a in artifacts if a.artifact_type == typed_filter]
51
+
52
+ if not artifacts:
53
+ if artifact_type:
54
+ click.echo(f"No {artifact_type} artifacts found")
55
+ else:
56
+ click.echo("No artifacts found")
57
+ return
58
+
59
+ # Group by type for display
60
+ current_type: str | None = None
61
+ for artifact in artifacts:
62
+ if artifact.artifact_type != current_type:
63
+ if current_type is not None:
64
+ click.echo("") # Blank line between types
65
+ current_type = artifact.artifact_type
66
+ # Special headers for types with non-standard locations/display
67
+ if current_type == "workflow":
68
+ header = "Github Workflows (.github/workflows):"
69
+ elif current_type == "hook":
70
+ header = "Hooks (.claude/settings.json):"
71
+ else:
72
+ # Capitalize first letter only (e.g., "Commands:")
73
+ header = current_type.capitalize() + "s:"
74
+ click.echo(click.style(header, bold=True))
75
+
76
+ # Format badge based on management status
77
+ is_managed = is_erk_managed(artifact)
78
+ if is_managed:
79
+ badge = click.style(" [erk]", fg="cyan")
80
+ else:
81
+ badge = click.style(" [unmanaged]", fg="yellow")
82
+
83
+ if verbose:
84
+ click.echo(f" {artifact.name}{badge}")
85
+ click.echo(click.style(f" Path: {artifact.path}", dim=True))
86
+ if is_managed and artifact.content_hash:
87
+ click.echo(click.style(f" Hash: {artifact.content_hash}", dim=True))
88
+ else:
89
+ click.echo(f" {artifact.name}{badge}")
@@ -0,0 +1,62 @@
1
+ """Show artifact content."""
2
+
3
+ from pathlib import Path
4
+ from typing import cast
5
+
6
+ import click
7
+
8
+ from erk.artifacts.discovery import get_artifact_by_name
9
+ from erk.artifacts.models import ArtifactType
10
+
11
+
12
+ @click.command("show")
13
+ @click.argument("name")
14
+ @click.option(
15
+ "--type",
16
+ "artifact_type",
17
+ type=click.Choice(["skill", "command", "agent", "workflow"]),
18
+ help="Artifact type (optional, helps disambiguate)",
19
+ )
20
+ def show_cmd(name: str, artifact_type: str | None) -> None:
21
+ """Display the content of an artifact.
22
+
23
+ Examples:
24
+
25
+ \b
26
+ # Show a skill
27
+ erk artifact show dignified-python
28
+
29
+ \b
30
+ # Show a command (use colon for namespaced commands)
31
+ erk artifact show erk:plan-implement
32
+
33
+ \b
34
+ # Disambiguate by type
35
+ erk artifact show my-artifact --type skill
36
+ """
37
+ project_dir = Path.cwd()
38
+ claude_dir = project_dir / ".claude"
39
+ if not claude_dir.exists():
40
+ click.echo("No .claude/ directory found in current directory", err=True)
41
+ raise SystemExit(1)
42
+
43
+ type_filter: ArtifactType | None = None
44
+ if artifact_type is not None:
45
+ assert artifact_type in ("skill", "command", "agent", "workflow")
46
+ type_filter = cast(ArtifactType, artifact_type)
47
+
48
+ artifact = get_artifact_by_name(project_dir, name, type_filter)
49
+
50
+ if artifact is None:
51
+ click.echo(f"Artifact not found: {name}", err=True)
52
+ raise SystemExit(1)
53
+
54
+ # Display metadata
55
+ click.echo(click.style(f"# {artifact.name}", bold=True))
56
+ click.echo(click.style(f"Type: {artifact.artifact_type}", dim=True))
57
+ click.echo(click.style(f"Path: {artifact.path}", dim=True))
58
+ click.echo("")
59
+
60
+ # Display content
61
+ content = artifact.path.read_text(encoding="utf-8")
62
+ click.echo(content)
@@ -0,0 +1,39 @@
1
+ """Sync artifacts from erk package to project."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.artifacts.sync import sync_artifacts
8
+
9
+
10
+ @click.command("sync")
11
+ @click.option("-f", "--force", is_flag=True, help="Force sync even if up to date")
12
+ def sync_cmd(force: bool) -> None:
13
+ """Sync artifacts from erk package to .claude/ directory.
14
+
15
+ Copies bundled artifacts (commands, skills, agents, docs) from the
16
+ installed erk package to the current project's .claude/ directory.
17
+
18
+ When running in the erk repo itself, this is a no-op since artifacts
19
+ are read directly from source.
20
+
21
+ Examples:
22
+
23
+ \b
24
+ # Sync artifacts
25
+ erk artifact sync
26
+
27
+ \b
28
+ # Force re-sync even if up to date
29
+ erk artifact sync --force
30
+ """
31
+ project_dir = Path.cwd()
32
+
33
+ result = sync_artifacts(project_dir, force)
34
+
35
+ if result.success:
36
+ click.echo(click.style("✓ ", fg="green") + result.message)
37
+ else:
38
+ click.echo(click.style("✗ ", fg="red") + result.message, err=True)
39
+ raise SystemExit(1)
@@ -0,0 +1,26 @@
1
+ """Branch management commands."""
2
+
3
+ import click
4
+
5
+ from erk.cli.alias import alias, register_with_aliases
6
+ from erk.cli.commands.branch.assign_cmd import branch_assign
7
+ from erk.cli.commands.branch.checkout_cmd import branch_checkout
8
+ from erk.cli.commands.branch.create_cmd import branch_create
9
+ from erk.cli.commands.branch.list_cmd import branch_list
10
+ from erk.cli.commands.branch.unassign_cmd import branch_unassign
11
+ from erk.cli.help_formatter import ErkCommandGroup
12
+
13
+
14
+ @alias("br")
15
+ @click.group("branch", cls=ErkCommandGroup, grouped=False)
16
+ def branch_group() -> None:
17
+ """Manage branches."""
18
+ pass
19
+
20
+
21
+ # Register subcommands
22
+ branch_group.add_command(branch_create)
23
+ branch_group.add_command(branch_assign)
24
+ branch_group.add_command(branch_unassign)
25
+ register_with_aliases(branch_group, branch_checkout)
26
+ register_with_aliases(branch_group, branch_list)
@@ -0,0 +1,152 @@
1
+ """Branch assign command - assign an existing branch to a worktree slot."""
2
+
3
+ import sys
4
+ from datetime import UTC, datetime
5
+
6
+ import click
7
+
8
+ from erk.cli.commands.slot.common import (
9
+ cleanup_worktree_artifacts,
10
+ find_branch_assignment,
11
+ find_next_available_slot,
12
+ generate_slot_name,
13
+ get_pool_size,
14
+ handle_pool_full_interactive,
15
+ )
16
+ from erk.cli.core import discover_repo_context
17
+ from erk.cli.ensure import Ensure
18
+ from erk.core.context import ErkContext
19
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
20
+ from erk.core.worktree_pool import (
21
+ PoolState,
22
+ SlotAssignment,
23
+ load_pool_state,
24
+ save_pool_state,
25
+ )
26
+ from erk_shared.output.output import user_output
27
+
28
+
29
+ @click.command("assign")
30
+ @click.argument("branch_name", metavar="BRANCH")
31
+ @click.option("-f", "--force", is_flag=True, help="Auto-unassign oldest branch if pool is full")
32
+ @click.pass_obj
33
+ def branch_assign(ctx: ErkContext, branch_name: str, force: bool) -> None:
34
+ """Assign an EXISTING branch to an available pool slot.
35
+
36
+ BRANCH is the name of an existing git branch to assign to the pool.
37
+
38
+ The command will:
39
+ 1. Verify the branch EXISTS (fails if it doesn't)
40
+ 2. Find the next available slot in the pool
41
+ 3. Create a worktree for that slot if needed
42
+ 4. Assign the branch to the slot
43
+ 5. Persist the assignment to pool.json
44
+
45
+ Use `erk br create` to create a NEW branch and assign it.
46
+ """
47
+ repo = discover_repo_context(ctx, ctx.cwd)
48
+ ensure_erk_metadata_dir(repo)
49
+
50
+ # Get pool size from config or default
51
+ pool_size = get_pool_size(ctx)
52
+
53
+ # Load or create pool state
54
+ state = load_pool_state(repo.pool_json_path)
55
+ if state is None:
56
+ state = PoolState(
57
+ version="1.0",
58
+ pool_size=pool_size,
59
+ slots=(),
60
+ assignments=(),
61
+ )
62
+
63
+ # Check if branch is already assigned
64
+ existing = find_branch_assignment(state, branch_name)
65
+ if existing is not None:
66
+ user_output(f"Error: Branch '{branch_name}' already assigned to {existing.slot_name}")
67
+ raise SystemExit(1) from None
68
+
69
+ # Check if branch exists - assign command requires EXISTING branch
70
+ local_branches = ctx.git.list_local_branches(repo.root)
71
+ if branch_name not in local_branches:
72
+ user_output(
73
+ f"Error: Branch '{branch_name}' does not exist.\n"
74
+ "Use `erk br create` to create a new branch."
75
+ )
76
+ raise SystemExit(1) from None
77
+
78
+ # Find next available slot
79
+ slot_num = find_next_available_slot(state, repo.worktrees_dir)
80
+ if slot_num is None:
81
+ # Pool is full - handle interactively or with --force
82
+ to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
83
+ if to_unassign is None:
84
+ raise SystemExit(1) from None
85
+
86
+ # Remove the assignment from state
87
+ new_assignments = tuple(
88
+ a for a in state.assignments if a.slot_name != to_unassign.slot_name
89
+ )
90
+ state = PoolState(
91
+ version=state.version,
92
+ pool_size=state.pool_size,
93
+ slots=state.slots,
94
+ assignments=new_assignments,
95
+ )
96
+ save_pool_state(repo.pool_json_path, state)
97
+ user_output(
98
+ click.style("✓ ", fg="green")
99
+ + f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
100
+ + f"from {click.style(to_unassign.slot_name, fg='cyan')}"
101
+ )
102
+
103
+ # Use the slot we just unassigned (it has a worktree directory that can be reused)
104
+ slot_name = to_unassign.slot_name
105
+ worktree_path = to_unassign.worktree_path
106
+ else:
107
+ slot_name = generate_slot_name(slot_num)
108
+ worktree_path = repo.worktrees_dir / slot_name
109
+
110
+ # Create worktree if it doesn't exist
111
+ if not ctx.git.path_exists(worktree_path):
112
+ # Create directory for worktree
113
+ worktree_path.mkdir(parents=True, exist_ok=True)
114
+
115
+ # Add worktree
116
+ ctx.git.add_worktree(
117
+ repo.root,
118
+ worktree_path,
119
+ branch=branch_name,
120
+ ref=None,
121
+ create_branch=False,
122
+ )
123
+ else:
124
+ # Worktree exists - clean up stale artifacts and check out the branch
125
+ Ensure.invariant(
126
+ ctx.git.is_dir(worktree_path),
127
+ f"Expected {worktree_path} to be a directory",
128
+ )
129
+ cleanup_worktree_artifacts(worktree_path)
130
+ ctx.git.checkout_branch(worktree_path, branch_name)
131
+
132
+ # Create new assignment
133
+ now = datetime.now(UTC).isoformat()
134
+ new_assignment = SlotAssignment(
135
+ slot_name=slot_name,
136
+ branch_name=branch_name,
137
+ assigned_at=now,
138
+ worktree_path=worktree_path,
139
+ )
140
+
141
+ # Update state with new assignment
142
+ new_state = PoolState(
143
+ version=state.version,
144
+ pool_size=state.pool_size,
145
+ slots=state.slots,
146
+ assignments=(*state.assignments, new_assignment),
147
+ )
148
+
149
+ # Save state
150
+ save_pool_state(repo.pool_json_path, new_state)
151
+
152
+ user_output(click.style(f"✓ Assigned {branch_name} to {slot_name}", fg="green"))