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,296 @@
1
+ #!/usr/bin/env python3
2
+ """List Claude Code sessions for the current project with metadata.
3
+
4
+ This command discovers sessions in the Claude Code project directory,
5
+ extracts metadata (timestamps, summaries), and provides branch context
6
+ for intelligent session selection.
7
+
8
+ Usage:
9
+ erk exec list-sessions
10
+
11
+ Output:
12
+ JSON object with success status, branch context, and session list
13
+
14
+ Exit Codes:
15
+ 0: Success
16
+ 1: Error (project directory not found or other error)
17
+
18
+ Examples:
19
+ $ erk exec list-sessions
20
+ {
21
+ "success": true,
22
+ "branch_context": {
23
+ "current_branch": "feature-xyz",
24
+ "trunk_branch": "master",
25
+ "is_on_trunk": false
26
+ },
27
+ "current_session_id": "abc123-def456",
28
+ "sessions": [...],
29
+ "project_dir": "claude-code-project"
30
+ }
31
+ """
32
+
33
+ import json
34
+ import time
35
+ from dataclasses import asdict, dataclass
36
+ from pathlib import Path
37
+
38
+ import click
39
+
40
+ from erk_shared.context.helpers import require_claude_installation, require_cwd, require_git
41
+ from erk_shared.extraction.claude_installation import ClaudeInstallation, Session
42
+ from erk_shared.extraction.session_schema import extract_first_user_message_text
43
+ from erk_shared.git.abc import Git
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class BranchContext:
48
+ """Git branch context for session selection behavior."""
49
+
50
+ current_branch: str
51
+ trunk_branch: str
52
+ is_on_trunk: bool
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class SessionInfo:
57
+ """Metadata for a session log file."""
58
+
59
+ session_id: str
60
+ mtime_display: str
61
+ mtime_relative: str
62
+ mtime_unix: float
63
+ size_bytes: int
64
+ summary: str
65
+ is_current: bool
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ListSessionsResult:
70
+ """Success result with session list and context."""
71
+
72
+ success: bool
73
+ branch_context: dict[str, str | bool]
74
+ current_session_id: str | None
75
+ sessions: list[dict[str, str | float | int | bool]]
76
+ project_dir: str
77
+ filtered_count: int # Count of sessions filtered by --min-size
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ListSessionsError:
82
+ """Error result when listing sessions fails."""
83
+
84
+ success: bool
85
+ error: str
86
+ help: str
87
+
88
+
89
+ def get_branch_context(git: Git, cwd: Path) -> BranchContext:
90
+ """Get git branch context for determining session selection behavior.
91
+
92
+ Args:
93
+ git: Git interface for branch operations
94
+ cwd: Current working directory
95
+
96
+ Returns:
97
+ BranchContext with current branch, trunk branch, and trunk status
98
+ """
99
+ current_branch = git.get_current_branch(cwd) or ""
100
+ trunk_branch = git.detect_trunk_branch(cwd)
101
+
102
+ return BranchContext(
103
+ current_branch=current_branch,
104
+ trunk_branch=trunk_branch,
105
+ is_on_trunk=current_branch == trunk_branch,
106
+ )
107
+
108
+
109
+ def format_relative_time(mtime: float) -> str:
110
+ """Format modification time as human-readable relative time.
111
+
112
+ Args:
113
+ mtime: Unix timestamp (seconds since epoch)
114
+
115
+ Returns:
116
+ Human-readable relative time string
117
+
118
+ Examples:
119
+ >>> format_relative_time(time.time() - 10)
120
+ 'just now'
121
+ >>> format_relative_time(time.time() - 180)
122
+ '3m ago'
123
+ >>> format_relative_time(time.time() - 7200)
124
+ '2h ago'
125
+ """
126
+ now = time.time()
127
+ delta = now - mtime
128
+
129
+ if delta < 30:
130
+ return "just now"
131
+ if delta < 3600: # < 1 hour
132
+ minutes = int(delta / 60)
133
+ return f"{minutes}m ago"
134
+ if delta < 86400: # < 24 hours
135
+ hours = int(delta / 3600)
136
+ return f"{hours}h ago"
137
+ if delta < 604800: # < 7 days
138
+ days = int(delta / 86400)
139
+ return f"{days}d ago"
140
+ # >= 7 days: show absolute date
141
+ return format_display_time(mtime)
142
+
143
+
144
+ def format_display_time(mtime: float) -> str:
145
+ """Format modification time as display string.
146
+
147
+ Args:
148
+ mtime: Unix timestamp (seconds since epoch)
149
+
150
+ Returns:
151
+ Formatted date string like "Dec 3, 11:38 AM"
152
+ """
153
+ import datetime
154
+
155
+ dt = datetime.datetime.fromtimestamp(mtime)
156
+ return dt.strftime("%b %-d, %-I:%M %p")
157
+
158
+
159
+ def _list_sessions_from_store(
160
+ claude_installation: ClaudeInstallation,
161
+ cwd: Path,
162
+ current_session_id: str | None,
163
+ limit: int,
164
+ min_size: int,
165
+ ) -> tuple[list[SessionInfo], int]:
166
+ """List sessions from claude installation sorted by modification time.
167
+
168
+ Args:
169
+ claude_installation: Claude installation to query
170
+ cwd: Current working directory (project identifier)
171
+ current_session_id: Current session ID (for marking)
172
+ limit: Maximum number of sessions to return
173
+ min_size: Minimum session size in bytes (filters out tiny sessions)
174
+
175
+ Returns:
176
+ Tuple of (sessions list, count of sessions filtered by min_size)
177
+ """
178
+ # Check if project exists
179
+ if not claude_installation.has_project(cwd):
180
+ return [], 0
181
+
182
+ # Get all sessions first to count filtered
183
+ all_sessions = claude_installation.find_sessions(
184
+ cwd,
185
+ current_session_id=current_session_id,
186
+ min_size=0,
187
+ limit=1000,
188
+ include_agents=False,
189
+ )
190
+
191
+ # Filter by size
192
+ filtered_sessions: list[Session]
193
+ if min_size > 0:
194
+ filtered_sessions = [s for s in all_sessions if s.size_bytes >= min_size]
195
+ filtered_count = len(all_sessions) - len(filtered_sessions)
196
+ else:
197
+ filtered_sessions = all_sessions
198
+ filtered_count = 0
199
+
200
+ # Apply limit
201
+ limited_sessions = filtered_sessions[:limit]
202
+
203
+ # Convert to SessionInfo with summaries
204
+ session_infos: list[SessionInfo] = []
205
+ for session in limited_sessions:
206
+ # Read session content for summary extraction
207
+ content = claude_installation.read_session(cwd, session.session_id, include_agents=False)
208
+ summary = ""
209
+ if content is not None:
210
+ summary = extract_first_user_message_text(content.main_content, max_length=60)
211
+
212
+ # Determine if this is the current session
213
+ is_current = session.session_id == current_session_id
214
+
215
+ session_infos.append(
216
+ SessionInfo(
217
+ session_id=session.session_id,
218
+ mtime_display=format_display_time(session.modified_at),
219
+ mtime_relative=format_relative_time(session.modified_at),
220
+ mtime_unix=session.modified_at,
221
+ size_bytes=session.size_bytes,
222
+ summary=summary,
223
+ is_current=is_current,
224
+ )
225
+ )
226
+
227
+ return session_infos, filtered_count
228
+
229
+
230
+ @click.command(name="list-sessions")
231
+ @click.option(
232
+ "--limit",
233
+ default=10,
234
+ type=int,
235
+ help="Maximum number of sessions to list",
236
+ )
237
+ @click.option(
238
+ "--min-size",
239
+ default=0,
240
+ type=int,
241
+ help="Minimum session size in bytes (filters out tiny sessions)",
242
+ )
243
+ @click.option(
244
+ "--session-id",
245
+ default=None,
246
+ type=str,
247
+ help="Current session ID (for marking the current session)",
248
+ )
249
+ @click.pass_context
250
+ def list_sessions(ctx: click.Context, limit: int, min_size: int, session_id: str | None) -> None:
251
+ """List Claude Code sessions with metadata for the current project.
252
+
253
+ Discovers sessions in the project directory, extracts metadata
254
+ (timestamps, summaries), and provides branch context.
255
+ """
256
+ git = require_git(ctx)
257
+ claude_installation = require_claude_installation(ctx)
258
+ cwd = require_cwd(ctx)
259
+
260
+ # Check if project exists
261
+ if not claude_installation.has_project(cwd):
262
+ error = ListSessionsError(
263
+ success=False,
264
+ error=f"No Claude Code project found for: {cwd}",
265
+ help="Make sure you're in a directory with Claude Code sessions",
266
+ )
267
+ click.echo(json.dumps(asdict(error), indent=2))
268
+ raise SystemExit(1)
269
+
270
+ # Get branch context
271
+ branch_context = get_branch_context(git, cwd)
272
+
273
+ # List sessions from store
274
+ sessions, filtered_count = _list_sessions_from_store(
275
+ claude_installation,
276
+ cwd,
277
+ session_id,
278
+ limit=limit,
279
+ min_size=min_size,
280
+ )
281
+
282
+ # Build result
283
+ result = ListSessionsResult(
284
+ success=True,
285
+ branch_context={
286
+ "current_branch": branch_context.current_branch,
287
+ "trunk_branch": branch_context.trunk_branch,
288
+ "is_on_trunk": branch_context.is_on_trunk,
289
+ },
290
+ current_session_id=session_id,
291
+ sessions=[asdict(s) for s in sessions],
292
+ project_dir="claude-code-project", # Abstract - don't expose filesystem paths
293
+ filtered_count=filtered_count,
294
+ )
295
+
296
+ click.echo(json.dumps(asdict(result), indent=2))
@@ -0,0 +1,188 @@
1
+ """Mark implementation ended by updating GitHub issue metadata.
2
+
3
+ This exec command updates the plan-header metadata block in a GitHub issue
4
+ with the appropriate event fields based on the execution environment:
5
+ - Local machine: Updates last_local_impl_* fields (timestamp, event="ended", session, user)
6
+ - GitHub Actions: Updates last_remote_impl_at field
7
+
8
+ Also writes .impl/local-run-state.json for fast local access (no GitHub API needed).
9
+
10
+ Usage:
11
+ erk exec mark-impl-ended
12
+
13
+ Output:
14
+ JSON with success status or error information
15
+ Always exits with code 0 (graceful degradation for || true pattern)
16
+
17
+ Exit Codes:
18
+ 0: Always (even on error, to support || true pattern)
19
+
20
+ Examples:
21
+ $ erk exec mark-impl-ended
22
+ {"success": true, "issue_number": 123}
23
+
24
+ $ erk exec mark-impl-ended
25
+ {"success": false, "error_type": "no_issue_reference", "message": "..."}
26
+ """
27
+
28
+ import getpass
29
+ import json
30
+ import os
31
+ from dataclasses import asdict, dataclass
32
+ from datetime import UTC, datetime
33
+
34
+ import click
35
+
36
+ from erk_shared.context.helpers import (
37
+ require_cwd,
38
+ require_repo_root,
39
+ )
40
+ from erk_shared.context.helpers import (
41
+ require_issues as require_github_issues,
42
+ )
43
+ from erk_shared.env import in_github_actions
44
+ from erk_shared.github.metadata.plan_header import (
45
+ update_plan_header_local_impl_event,
46
+ update_plan_header_remote_impl,
47
+ )
48
+ from erk_shared.impl_folder import read_issue_reference, write_local_run_state
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class MarkImplSuccess:
53
+ """Success response for mark impl ended."""
54
+
55
+ success: bool
56
+ issue_number: int
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class MarkImplError:
61
+ """Error response for mark impl ended."""
62
+
63
+ success: bool
64
+ error_type: str
65
+ message: str
66
+
67
+
68
+ @click.command(name="mark-impl-ended")
69
+ @click.pass_context
70
+ def mark_impl_ended(ctx: click.Context) -> None:
71
+ """Update implementation ended event in GitHub issue and local state file.
72
+
73
+ Reads issue number from .impl/issue.json, fetches the issue from GitHub,
74
+ updates the plan-header block with current event metadata, and posts back.
75
+
76
+ Also writes .impl/local-run-state.json for fast local access.
77
+
78
+ Detects execution environment:
79
+ - Local machine: Updates last_local_impl_* fields (timestamp, event="ended", session, user)
80
+ - GitHub Actions: Updates last_remote_impl_at field
81
+
82
+ Gracefully fails with exit code 0 to support || true pattern in slash commands.
83
+ """
84
+ # Get dependencies from context
85
+ repo_root = require_repo_root(ctx)
86
+ cwd = require_cwd(ctx)
87
+
88
+ # Read issue reference from .impl/issue.json
89
+ impl_dir = cwd / ".impl"
90
+ issue_ref = read_issue_reference(impl_dir)
91
+ if issue_ref is None:
92
+ result = MarkImplError(
93
+ success=False,
94
+ error_type="no-issue-reference",
95
+ message="No issue reference found in .impl/issue.json",
96
+ )
97
+ click.echo(json.dumps(asdict(result), indent=2))
98
+ raise SystemExit(0)
99
+
100
+ # Capture metadata
101
+ timestamp = datetime.now(UTC).isoformat()
102
+ session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
103
+ user = getpass.getuser()
104
+
105
+ # Write local state file first (fast, no network)
106
+ try:
107
+ write_local_run_state(
108
+ impl_dir=impl_dir,
109
+ last_event="ended",
110
+ timestamp=timestamp,
111
+ user=user,
112
+ session_id=session_id,
113
+ )
114
+ except (FileNotFoundError, ValueError) as e:
115
+ result = MarkImplError(
116
+ success=False,
117
+ error_type="local-state-write-failed",
118
+ message=f"Failed to write local state: {e}",
119
+ )
120
+ click.echo(json.dumps(asdict(result), indent=2))
121
+ raise SystemExit(0) from None
122
+
123
+ # Get GitHub Issues from context
124
+ try:
125
+ github_issues = require_github_issues(ctx)
126
+ except SystemExit:
127
+ result = MarkImplError(
128
+ success=False,
129
+ error_type="context-not-initialized",
130
+ message="Context not initialized",
131
+ )
132
+ click.echo(json.dumps(asdict(result), indent=2))
133
+ raise SystemExit(0) from None
134
+
135
+ # Fetch current issue
136
+ try:
137
+ issue = github_issues.get_issue(repo_root, issue_ref.issue_number)
138
+ except RuntimeError as e:
139
+ result = MarkImplError(
140
+ success=False,
141
+ error_type="issue-not-found",
142
+ message=f"Issue #{issue_ref.issue_number} not found: {e}",
143
+ )
144
+ click.echo(json.dumps(asdict(result), indent=2))
145
+ raise SystemExit(0) from None
146
+
147
+ # Update impl event based on environment
148
+ try:
149
+ if in_github_actions():
150
+ updated_body = update_plan_header_remote_impl(
151
+ issue_body=issue.body,
152
+ remote_impl_at=timestamp,
153
+ )
154
+ else:
155
+ updated_body = update_plan_header_local_impl_event(
156
+ issue_body=issue.body,
157
+ local_impl_at=timestamp,
158
+ event="ended",
159
+ session_id=session_id,
160
+ user=user,
161
+ )
162
+ except ValueError as e:
163
+ # plan-header block not found (old format issue)
164
+ result = MarkImplError(
165
+ success=False,
166
+ error_type="no-plan-header-block",
167
+ message=str(e),
168
+ )
169
+ click.echo(json.dumps(asdict(result), indent=2))
170
+ raise SystemExit(0) from None
171
+
172
+ # Update issue body
173
+ try:
174
+ github_issues.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
175
+ except RuntimeError as e:
176
+ result = MarkImplError(
177
+ success=False,
178
+ error_type="github-api-failed",
179
+ message=f"Failed to update issue body: {e}",
180
+ )
181
+ click.echo(json.dumps(asdict(result), indent=2))
182
+ raise SystemExit(0) from None
183
+
184
+ result_success = MarkImplSuccess(
185
+ success=True,
186
+ issue_number=issue_ref.issue_number,
187
+ )
188
+ click.echo(json.dumps(asdict(result_success), indent=2))
@@ -0,0 +1,188 @@
1
+ """Mark implementation started by updating GitHub issue metadata.
2
+
3
+ This exec command updates the plan-header metadata block in a GitHub issue
4
+ with the appropriate event fields based on the execution environment:
5
+ - Local machine: Updates last_local_impl_* fields (timestamp, event, session, user)
6
+ - GitHub Actions: Updates last_remote_impl_at field
7
+
8
+ Also writes .impl/local-run-state.json for fast local access (no GitHub API needed).
9
+
10
+ Usage:
11
+ erk exec mark-impl-started
12
+
13
+ Output:
14
+ JSON with success status or error information
15
+ Always exits with code 0 (graceful degradation for || true pattern)
16
+
17
+ Exit Codes:
18
+ 0: Always (even on error, to support || true pattern)
19
+
20
+ Examples:
21
+ $ erk exec mark-impl-started
22
+ {"success": true, "issue_number": 123}
23
+
24
+ $ erk exec mark-impl-started
25
+ {"success": false, "error_type": "no_issue_reference", "message": "..."}
26
+ """
27
+
28
+ import getpass
29
+ import json
30
+ import os
31
+ from dataclasses import asdict, dataclass
32
+ from datetime import UTC, datetime
33
+
34
+ import click
35
+
36
+ from erk_shared.context.helpers import (
37
+ require_cwd,
38
+ require_repo_root,
39
+ )
40
+ from erk_shared.context.helpers import (
41
+ require_issues as require_github_issues,
42
+ )
43
+ from erk_shared.env import in_github_actions
44
+ from erk_shared.github.metadata.plan_header import (
45
+ update_plan_header_local_impl_event,
46
+ update_plan_header_remote_impl,
47
+ )
48
+ from erk_shared.impl_folder import read_issue_reference, write_local_run_state
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class MarkImplSuccess:
53
+ """Success response for mark impl started."""
54
+
55
+ success: bool
56
+ issue_number: int
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class MarkImplError:
61
+ """Error response for mark impl started."""
62
+
63
+ success: bool
64
+ error_type: str
65
+ message: str
66
+
67
+
68
+ @click.command(name="mark-impl-started")
69
+ @click.pass_context
70
+ def mark_impl_started(ctx: click.Context) -> None:
71
+ """Update implementation started event in GitHub issue and local state file.
72
+
73
+ Reads issue number from .impl/issue.json, fetches the issue from GitHub,
74
+ updates the plan-header block with current event metadata, and posts back.
75
+
76
+ Also writes .impl/local-run-state.json for fast local access.
77
+
78
+ Detects execution environment:
79
+ - Local machine: Updates last_local_impl_* fields (timestamp, event, session, user)
80
+ - GitHub Actions: Updates last_remote_impl_at field
81
+
82
+ Gracefully fails with exit code 0 to support || true pattern in slash commands.
83
+ """
84
+ # Get dependencies from context
85
+ repo_root = require_repo_root(ctx)
86
+ cwd = require_cwd(ctx)
87
+
88
+ # Read issue reference from .impl/issue.json
89
+ impl_dir = cwd / ".impl"
90
+ issue_ref = read_issue_reference(impl_dir)
91
+ if issue_ref is None:
92
+ result = MarkImplError(
93
+ success=False,
94
+ error_type="no-issue-reference",
95
+ message="No issue reference found in .impl/issue.json",
96
+ )
97
+ click.echo(json.dumps(asdict(result), indent=2))
98
+ raise SystemExit(0)
99
+
100
+ # Capture metadata
101
+ timestamp = datetime.now(UTC).isoformat()
102
+ session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
103
+ user = getpass.getuser()
104
+
105
+ # Write local state file first (fast, no network)
106
+ try:
107
+ write_local_run_state(
108
+ impl_dir=impl_dir,
109
+ last_event="started",
110
+ timestamp=timestamp,
111
+ user=user,
112
+ session_id=session_id,
113
+ )
114
+ except (FileNotFoundError, ValueError) as e:
115
+ result = MarkImplError(
116
+ success=False,
117
+ error_type="local-state-write-failed",
118
+ message=f"Failed to write local state: {e}",
119
+ )
120
+ click.echo(json.dumps(asdict(result), indent=2))
121
+ raise SystemExit(0) from None
122
+
123
+ # Get GitHub Issues from context
124
+ try:
125
+ github_issues = require_github_issues(ctx)
126
+ except SystemExit:
127
+ result = MarkImplError(
128
+ success=False,
129
+ error_type="context-not-initialized",
130
+ message="Context not initialized",
131
+ )
132
+ click.echo(json.dumps(asdict(result), indent=2))
133
+ raise SystemExit(0) from None
134
+
135
+ # Fetch current issue
136
+ try:
137
+ issue = github_issues.get_issue(repo_root, issue_ref.issue_number)
138
+ except RuntimeError as e:
139
+ result = MarkImplError(
140
+ success=False,
141
+ error_type="issue-not-found",
142
+ message=f"Issue #{issue_ref.issue_number} not found: {e}",
143
+ )
144
+ click.echo(json.dumps(asdict(result), indent=2))
145
+ raise SystemExit(0) from None
146
+
147
+ # Update impl event based on environment
148
+ try:
149
+ if in_github_actions():
150
+ updated_body = update_plan_header_remote_impl(
151
+ issue_body=issue.body,
152
+ remote_impl_at=timestamp,
153
+ )
154
+ else:
155
+ updated_body = update_plan_header_local_impl_event(
156
+ issue_body=issue.body,
157
+ local_impl_at=timestamp,
158
+ event="started",
159
+ session_id=session_id,
160
+ user=user,
161
+ )
162
+ except ValueError as e:
163
+ # plan-header block not found (old format issue)
164
+ result = MarkImplError(
165
+ success=False,
166
+ error_type="no-plan-header-block",
167
+ message=str(e),
168
+ )
169
+ click.echo(json.dumps(asdict(result), indent=2))
170
+ raise SystemExit(0) from None
171
+
172
+ # Update issue body
173
+ try:
174
+ github_issues.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
175
+ except RuntimeError as e:
176
+ result = MarkImplError(
177
+ success=False,
178
+ error_type="github-api-failed",
179
+ message=f"Failed to update issue body: {e}",
180
+ )
181
+ click.echo(json.dumps(asdict(result), indent=2))
182
+ raise SystemExit(0) from None
183
+
184
+ result_success = MarkImplSuccess(
185
+ success=True,
186
+ issue_number=issue_ref.issue_number,
187
+ )
188
+ click.echo(json.dumps(asdict(result_success), indent=2))