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,197 @@
1
+ """Branch unassign command - remove a branch assignment from a pool slot."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from erk.cli.commands.slot.common import get_placeholder_branch_name
9
+ from erk.cli.core import discover_repo_context
10
+ from erk.core.context import ErkContext
11
+ from erk.core.repo_discovery import RepoContext
12
+ from erk.core.worktree_pool import (
13
+ PoolState,
14
+ SlotAssignment,
15
+ load_pool_state,
16
+ save_pool_state,
17
+ )
18
+ from erk_shared.output.output import user_output
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class UnassignResult:
23
+ """Result of an unassign operation."""
24
+
25
+ branch_name: str
26
+ slot_name: str
27
+ trunk_branch: str
28
+
29
+
30
+ def execute_unassign(
31
+ ctx: ErkContext,
32
+ repo: RepoContext,
33
+ state: PoolState,
34
+ assignment: SlotAssignment,
35
+ ) -> UnassignResult:
36
+ """Execute the unassign operation for a pool slot.
37
+
38
+ This function handles:
39
+ - Checking for uncommitted changes
40
+ - Getting or creating placeholder branch
41
+ - Checking out placeholder branch
42
+ - Removing assignment from pool state
43
+
44
+ Args:
45
+ ctx: ErkContext with git operations
46
+ repo: Repository context
47
+ state: Current pool state
48
+ assignment: The assignment to remove
49
+
50
+ Returns:
51
+ UnassignResult with branch name, slot name, and trunk branch
52
+
53
+ Raises:
54
+ SystemExit: If worktree has uncommitted changes or placeholder branch cannot be determined
55
+ """
56
+ # Check for uncommitted changes before switching branches
57
+ if ctx.git.has_uncommitted_changes(assignment.worktree_path):
58
+ user_output(
59
+ f"Error: Worktree has uncommitted changes at {assignment.worktree_path}.\n"
60
+ "Commit or stash your changes before unassigning."
61
+ )
62
+ raise SystemExit(1) from None
63
+
64
+ # Get or create placeholder branch
65
+ placeholder_branch = get_placeholder_branch_name(assignment.slot_name)
66
+ if placeholder_branch is None:
67
+ user_output(
68
+ f"Error: Could not determine placeholder branch for slot {assignment.slot_name}."
69
+ )
70
+ raise SystemExit(1) from None
71
+
72
+ trunk_branch = ctx.git.detect_trunk_branch(repo.root)
73
+ local_branches = ctx.git.list_local_branches(repo.root)
74
+
75
+ if placeholder_branch not in local_branches:
76
+ ctx.git.create_branch(repo.root, placeholder_branch, trunk_branch)
77
+
78
+ # Checkout placeholder branch in the worktree
79
+ ctx.git.checkout_branch(assignment.worktree_path, placeholder_branch)
80
+
81
+ # Remove assignment from state (immutable update)
82
+ new_assignments = tuple(a for a in state.assignments if a.slot_name != assignment.slot_name)
83
+ new_state = PoolState(
84
+ version=state.version,
85
+ pool_size=state.pool_size,
86
+ slots=state.slots,
87
+ assignments=new_assignments,
88
+ )
89
+
90
+ # Save updated state (guard for dry-run mode)
91
+ if ctx.dry_run:
92
+ user_output("[DRY RUN] Would save pool state")
93
+ else:
94
+ save_pool_state(repo.pool_json_path, new_state)
95
+
96
+ return UnassignResult(
97
+ branch_name=assignment.branch_name,
98
+ slot_name=assignment.slot_name,
99
+ trunk_branch=trunk_branch,
100
+ )
101
+
102
+
103
+ def _find_assignment_by_slot(state: PoolState, slot_name: str) -> SlotAssignment | None:
104
+ """Find an assignment by slot name.
105
+
106
+ Args:
107
+ state: Current pool state
108
+ slot_name: A slot name (e.g., "erk-managed-wt-01")
109
+
110
+ Returns:
111
+ SlotAssignment if found, None otherwise
112
+ """
113
+ for assignment in state.assignments:
114
+ if assignment.slot_name == slot_name:
115
+ return assignment
116
+ return None
117
+
118
+
119
+ def _find_assignment_by_cwd(state: PoolState, cwd: Path) -> SlotAssignment | None:
120
+ """Find an assignment by checking if cwd is within a pool slot's worktree.
121
+
122
+ Args:
123
+ state: Current pool state
124
+ cwd: Current working directory
125
+
126
+ Returns:
127
+ SlotAssignment if cwd is within a pool slot, None otherwise
128
+ """
129
+ if not cwd.exists():
130
+ return None
131
+ resolved_cwd = cwd.resolve()
132
+ for assignment in state.assignments:
133
+ if not assignment.worktree_path.exists():
134
+ continue
135
+ wt_path = assignment.worktree_path.resolve()
136
+ if resolved_cwd == wt_path or wt_path in resolved_cwd.parents:
137
+ return assignment
138
+ return None
139
+
140
+
141
+ @click.command("unassign")
142
+ @click.argument("worktree", metavar="WORKTREE", required=False)
143
+ @click.pass_obj
144
+ def branch_unassign(ctx: ErkContext, worktree: str | None) -> None:
145
+ """Remove a branch assignment from a pool slot.
146
+
147
+ WORKTREE is the slot name (e.g., erk-managed-wt-01).
148
+
149
+ If no argument is provided, the current pool slot is detected from the
150
+ working directory.
151
+
152
+ The worktree directory is kept for reuse with future assignments.
153
+
154
+ Examples:
155
+ erk br unassign erk-managed-wt-01 # Unassign by worktree name
156
+ erk br unassign # Unassign current slot (from within pool worktree)
157
+ """
158
+ repo = discover_repo_context(ctx, ctx.cwd)
159
+
160
+ # Load pool state
161
+ state = load_pool_state(repo.pool_json_path)
162
+ if state is None:
163
+ user_output("Error: No pool configured. Run `erk br create` first.")
164
+ raise SystemExit(1) from None
165
+
166
+ # Find the assignment to remove
167
+ assignment: SlotAssignment | None = None
168
+
169
+ if worktree is not None:
170
+ # Find by slot name
171
+ assignment = _find_assignment_by_slot(state, worktree)
172
+ if assignment is None:
173
+ user_output(
174
+ f"Error: No worktree found for '{worktree}'.\n"
175
+ "Run `erk slot list` to see current assignments."
176
+ )
177
+ raise SystemExit(1) from None
178
+ else:
179
+ # Detect current slot from cwd
180
+ assignment = _find_assignment_by_cwd(state, ctx.cwd)
181
+ if assignment is None:
182
+ user_output(
183
+ "Error: Not inside a pool slot. Specify worktree name.\n"
184
+ "Usage: erk br unassign WORKTREE"
185
+ )
186
+ raise SystemExit(1) from None
187
+
188
+ # Execute the unassign operation
189
+ result = execute_unassign(ctx, repo, state, assignment)
190
+
191
+ user_output(
192
+ click.style("✓ ", fg="green")
193
+ + f"Unassigned {click.style(result.branch_name, fg='yellow')} "
194
+ + f"from {click.style(result.slot_name, fg='cyan')}"
195
+ )
196
+ user_output(" Switched to placeholder branch")
197
+ user_output(" Tip: Use 'erk wt co root' to return to root worktree")
@@ -0,0 +1,15 @@
1
+ """Claude Code session tools command group."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.cc.jsonl_cmd import jsonl_viewer
6
+ from erk.cli.commands.cc.session import session_group
7
+
8
+
9
+ @click.group("cc")
10
+ def cc_group() -> None:
11
+ """Claude Code session tools."""
12
+
13
+
14
+ cc_group.add_command(session_group)
15
+ cc_group.add_command(jsonl_viewer)
@@ -0,0 +1,20 @@
1
+ """JSONL viewer CLI command."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from erk.tui.jsonl_viewer.app import JsonlViewerApp
8
+
9
+
10
+ @click.command("jsonl")
11
+ @click.argument("path", type=click.Path(exists=True, path_type=Path))
12
+ def jsonl_viewer(path: Path) -> None:
13
+ """View a Claude Code session JSONL file in an interactive TUI.
14
+
15
+ PATH is the path to a .jsonl file to view.
16
+
17
+ Use Enter to expand/collapse entries, q or Esc to quit.
18
+ """
19
+ app = JsonlViewerApp(path)
20
+ app.run()
@@ -0,0 +1,30 @@
1
+ # Claude Code Session Commands
2
+
3
+ User-facing CLI commands for working with Claude Code sessions.
4
+
5
+ ## Related Documentation
6
+
7
+ **Before modifying this code, read the session documentation:**
8
+
9
+ @docs/learned/sessions/
10
+
11
+ ## Architecture
12
+
13
+ These commands use the `ClaudeCodeSessionStore` abstraction from `erk_shared.extraction.claude_code_session_store`:
14
+
15
+ - **ABC**: `ClaudeCodeSessionStore` - interface for session operations
16
+ - **Real**: `RealClaudeCodeSessionStore` - reads from `~/.claude/projects/`
17
+ - **Fake**: `FakeClaudeCodeSessionStore` - in-memory for testing
18
+
19
+ ## Commands
20
+
21
+ ### `erk cc session list`
22
+
23
+ Lists sessions for the current worktree with session ID, time, size, and summary.
24
+
25
+ **Key behaviors:**
26
+
27
+ - By default, excludes agent sessions (files starting with `agent-`)
28
+ - `--include-agents` flag includes agent sessions with parent linkage
29
+ - Sessions sorted by modification time (newest first)
30
+ - Summary extracted from first user message
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,15 @@
1
+ """Session management command group for Claude Code."""
2
+
3
+ import click
4
+
5
+ from erk.cli.commands.cc.session.list_cmd import list_sessions
6
+ from erk.cli.commands.cc.session.show_cmd import show_session
7
+
8
+
9
+ @click.group("session")
10
+ def session_group() -> None:
11
+ """Manage Claude Code sessions."""
12
+
13
+
14
+ session_group.add_command(list_sessions)
15
+ session_group.add_command(show_session)
@@ -0,0 +1,167 @@
1
+ """List Claude Code sessions for the current worktree."""
2
+
3
+ import datetime
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from erk.core.context import ErkContext
12
+ from erk_shared.extraction.claude_installation import ClaudeInstallation
13
+ from erk_shared.extraction.session_schema import extract_first_user_message_text
14
+
15
+
16
+ def format_relative_time(mtime: float) -> str:
17
+ """Format modification time as human-readable relative time.
18
+
19
+ Args:
20
+ mtime: Unix timestamp (seconds since epoch)
21
+
22
+ Returns:
23
+ Human-readable relative time string
24
+ """
25
+ now = time.time()
26
+ delta = now - mtime
27
+
28
+ if delta < 30:
29
+ return "just now"
30
+ if delta < 3600: # < 1 hour
31
+ minutes = int(delta / 60)
32
+ return f"{minutes}m ago"
33
+ if delta < 86400: # < 24 hours
34
+ hours = int(delta / 3600)
35
+ return f"{hours}h ago"
36
+ if delta < 604800: # < 7 days
37
+ days = int(delta / 86400)
38
+ return f"{days}d ago"
39
+ # >= 7 days: show absolute date
40
+ return format_display_time(mtime)
41
+
42
+
43
+ def format_display_time(mtime: float) -> str:
44
+ """Format modification time as display string.
45
+
46
+ Args:
47
+ mtime: Unix timestamp (seconds since epoch)
48
+
49
+ Returns:
50
+ Formatted date string like "Dec 3, 11:38 AM"
51
+ """
52
+ dt = datetime.datetime.fromtimestamp(mtime)
53
+ return dt.strftime("%b %-d, %-I:%M %p")
54
+
55
+
56
+ def format_size(size_bytes: int) -> str:
57
+ """Format size in bytes as human-readable string.
58
+
59
+ Args:
60
+ size_bytes: Size in bytes
61
+
62
+ Returns:
63
+ Human-readable size string like "45KB"
64
+ """
65
+ if size_bytes < 1024:
66
+ return f"{size_bytes}B"
67
+ if size_bytes < 1024 * 1024:
68
+ return f"{size_bytes // 1024}KB"
69
+ return f"{size_bytes // (1024 * 1024)}MB"
70
+
71
+
72
+ def _list_sessions_impl(
73
+ claude_installation: ClaudeInstallation,
74
+ cwd: Path,
75
+ limit: int,
76
+ include_agents: bool,
77
+ ) -> None:
78
+ """Implementation of session listing logic.
79
+
80
+ Args:
81
+ claude_installation: Claude installation to query
82
+ cwd: Current working directory (project identifier)
83
+ limit: Maximum number of sessions to show
84
+ include_agents: Whether to include agent sessions in the listing
85
+ """
86
+ # Check if project exists
87
+ if not claude_installation.has_project(cwd):
88
+ click.echo(f"No Claude Code sessions found for: {cwd}", err=True)
89
+ raise SystemExit(1)
90
+
91
+ # Get sessions
92
+ sessions = claude_installation.find_sessions(
93
+ cwd,
94
+ current_session_id=None,
95
+ min_size=0,
96
+ limit=limit,
97
+ include_agents=include_agents,
98
+ )
99
+
100
+ if not sessions:
101
+ click.echo("No sessions found.", err=True)
102
+ return
103
+
104
+ # Create Rich table
105
+ table = Table(show_header=True, header_style="bold", box=None)
106
+ table.add_column("id", style="cyan", no_wrap=True)
107
+ if include_agents:
108
+ table.add_column("parent", no_wrap=True)
109
+ table.add_column("time", no_wrap=True)
110
+ table.add_column("size", no_wrap=True, justify="right")
111
+ table.add_column("summary", no_wrap=False)
112
+
113
+ for session in sessions:
114
+ # Read session content for summary extraction
115
+ content = claude_installation.read_session(cwd, session.session_id, include_agents=False)
116
+ summary = ""
117
+ if content is not None:
118
+ summary = extract_first_user_message_text(content.main_content, max_length=50)
119
+
120
+ if include_agents:
121
+ # Show first 8 chars of parent_session_id for agents, empty for main sessions
122
+ parent_short = session.parent_session_id[:8] if session.parent_session_id else ""
123
+ table.add_row(
124
+ session.session_id,
125
+ parent_short,
126
+ format_relative_time(session.modified_at),
127
+ format_size(session.size_bytes),
128
+ summary,
129
+ )
130
+ else:
131
+ table.add_row(
132
+ session.session_id,
133
+ format_relative_time(session.modified_at),
134
+ format_size(session.size_bytes),
135
+ summary,
136
+ )
137
+
138
+ # Output table to stderr (consistent with user_output convention)
139
+ console = Console(stderr=True, force_terminal=True)
140
+ console.print(table)
141
+
142
+
143
+ @click.command("list")
144
+ @click.option(
145
+ "--limit",
146
+ default=10,
147
+ type=int,
148
+ help="Maximum number of sessions to list",
149
+ )
150
+ @click.option(
151
+ "--include-agents",
152
+ is_flag=True,
153
+ default=False,
154
+ help="Include agent sessions in the listing",
155
+ )
156
+ @click.pass_obj
157
+ def list_sessions(ctx: ErkContext, limit: int, include_agents: bool) -> None:
158
+ """List Claude Code sessions for the current worktree.
159
+
160
+ Shows a table with session ID, time, size, and summary (first user message).
161
+ """
162
+ _list_sessions_impl(
163
+ ctx.claude_installation,
164
+ ctx.cwd,
165
+ limit,
166
+ include_agents,
167
+ )
@@ -0,0 +1,175 @@
1
+ """Show details for a specific Claude Code session."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from erk.cli.commands.cc.session.list_cmd import (
9
+ format_display_time,
10
+ format_size,
11
+ )
12
+ from erk.cli.ensure import Ensure
13
+ from erk.core.context import ErkContext
14
+ from erk_shared.extraction.claude_installation import ClaudeInstallation
15
+ from erk_shared.extraction.session_schema import (
16
+ AgentInfo,
17
+ extract_agent_info_from_jsonl,
18
+ extract_first_user_message_text,
19
+ )
20
+
21
+
22
+ def format_duration(seconds: float) -> str:
23
+ """Format a duration in seconds to a human-readable string.
24
+
25
+ Args:
26
+ seconds: Duration in seconds
27
+
28
+ Returns:
29
+ Formatted string like "42s", "1m 30s", or "1h 15m"
30
+ """
31
+ if seconds < 60:
32
+ return f"{int(seconds)}s"
33
+ elif seconds < 3600:
34
+ minutes = int(seconds // 60)
35
+ secs = int(seconds % 60)
36
+ return f"{minutes}m {secs}s"
37
+ else:
38
+ hours = int(seconds // 3600)
39
+ minutes = int((seconds % 3600) // 60)
40
+ return f"{hours}h {minutes}m"
41
+
42
+
43
+ def _show_session_impl(
44
+ claude_installation: ClaudeInstallation,
45
+ cwd: Path,
46
+ session_id: str | None,
47
+ ) -> None:
48
+ """Implementation of session show logic.
49
+
50
+ Args:
51
+ claude_installation: Claude installation to query
52
+ cwd: Current working directory (project identifier)
53
+ session_id: Session ID to show details for, or None to use most recent
54
+ """
55
+ console = Console(stderr=True, force_terminal=True)
56
+
57
+ # Check if project exists
58
+ Ensure.invariant(
59
+ claude_installation.has_project(cwd),
60
+ f"No Claude Code sessions found for: {cwd}",
61
+ )
62
+
63
+ # If no session_id provided, use the most recent session
64
+ inferred = False
65
+ if session_id is None:
66
+ sessions = claude_installation.find_sessions(
67
+ cwd,
68
+ current_session_id=None,
69
+ min_size=0,
70
+ include_agents=False,
71
+ limit=1,
72
+ )
73
+ Ensure.invariant(len(sessions) > 0, "No sessions found.")
74
+ session_id = sessions[0].session_id
75
+ inferred = True
76
+
77
+ # Get the session
78
+ session = Ensure.session(claude_installation.get_session(cwd, session_id))
79
+
80
+ # Check if this is an agent session - provide helpful error
81
+ parent_id = session.parent_session_id
82
+ Ensure.invariant(
83
+ parent_id is None,
84
+ f"Cannot show agent session directly. Use parent session instead: {parent_id}",
85
+ )
86
+
87
+ # Get the session path
88
+ session_path = claude_installation.get_session_path(cwd, session_id)
89
+
90
+ # Read session content for summary and agent info extraction
91
+ content = claude_installation.read_session(cwd, session_id, include_agents=False)
92
+ summary = ""
93
+ agent_infos: dict[str, AgentInfo] = {}
94
+ if content is not None:
95
+ summary = extract_first_user_message_text(content.main_content, max_length=100)
96
+ agent_infos = extract_agent_info_from_jsonl(content.main_content)
97
+
98
+ # Print inferred message if applicable
99
+ if inferred:
100
+ msg = f"Using most recent session for this worktree: {session.session_id}"
101
+ console.print(f"[dim]{msg}[/dim]")
102
+ console.print()
103
+
104
+ # Display metadata as key-value pairs
105
+ console.print(f"[bold]ID:[/bold] {session.session_id}")
106
+ console.print(f"[bold]Size:[/bold] {format_size(session.size_bytes)}")
107
+ console.print(f"[bold]Modified:[/bold] {format_display_time(session.modified_at)}")
108
+ if summary:
109
+ console.print(f"[bold]Summary:[/bold] {summary}")
110
+ if session_path is not None:
111
+ console.print(f"[bold]Path:[/bold] {session_path}")
112
+
113
+ # Find and display child agent sessions
114
+ all_sessions = claude_installation.find_sessions(
115
+ cwd,
116
+ current_session_id=None,
117
+ min_size=0,
118
+ include_agents=True,
119
+ limit=1000,
120
+ )
121
+
122
+ # Filter to only agent sessions with this parent
123
+ child_agents = [s for s in all_sessions if s.parent_session_id == session_id]
124
+
125
+ if child_agents:
126
+ console.print()
127
+ console.print("[bold]Agent Sessions:[/bold]")
128
+
129
+ for agent in child_agents:
130
+ info = agent_infos.get(agent.session_id)
131
+ agent_path = claude_installation.get_session_path(cwd, agent.session_id)
132
+
133
+ console.print()
134
+ # Format: type("prompt") or just session_id if no info
135
+ if info and info.agent_type:
136
+ # Clean up prompt: collapse whitespace, truncate
137
+ prompt_clean = " ".join(info.prompt.split())
138
+ if len(prompt_clean) > 80:
139
+ prompt_preview = prompt_clean[:80] + "..."
140
+ else:
141
+ prompt_preview = prompt_clean
142
+ console.print(f' [cyan]{info.agent_type}[/cyan]("{prompt_preview}")')
143
+ else:
144
+ console.print(f" [cyan]{agent.session_id}[/cyan]")
145
+ # Build metadata line: time, size, and optional duration
146
+ metadata_parts = [
147
+ format_display_time(agent.modified_at),
148
+ format_size(agent.size_bytes),
149
+ ]
150
+ if info and info.duration_secs is not None:
151
+ metadata_parts.append(format_duration(info.duration_secs))
152
+ console.print(f" {' '.join(metadata_parts)}")
153
+ if agent_path:
154
+ console.print(f" {agent_path}")
155
+ else:
156
+ console.print()
157
+ console.print("[dim]No agent sessions[/dim]")
158
+
159
+
160
+ @click.command("show")
161
+ @click.argument("session_id", required=False, default=None)
162
+ @click.pass_obj
163
+ def show_session(ctx: ErkContext, session_id: str | None) -> None:
164
+ """Show details for a specific Claude Code session.
165
+
166
+ Displays session metadata (ID, size, modified time, path, summary)
167
+ and lists any child agent sessions.
168
+
169
+ If SESSION_ID is not provided, shows the most recent session.
170
+ """
171
+ _show_session_impl(
172
+ ctx.claude_installation,
173
+ ctx.cwd,
174
+ session_id,
175
+ )