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,440 @@
1
+ """Command to display chronological event log for a plan."""
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+ from datetime import datetime
6
+ from typing import Literal, TypeAlias, TypedDict
7
+
8
+ import click
9
+
10
+ from erk.cli.core import discover_repo_context
11
+ from erk.core.context import ErkContext
12
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
13
+ from erk_shared.github.metadata.core import parse_metadata_blocks
14
+ from erk_shared.output.output import user_output
15
+
16
+ # Event type literals
17
+ EventType: TypeAlias = Literal[
18
+ "plan-created",
19
+ "submission-queued",
20
+ "workflow-started",
21
+ "implementation-status",
22
+ "plan-retry",
23
+ "worktree-created",
24
+ ]
25
+
26
+
27
+ # Event metadata types
28
+ class PlanCreatedMetadata(TypedDict, total=False):
29
+ """Metadata for plan_created event."""
30
+
31
+ worktree_name: str
32
+ issue_number: int
33
+
34
+
35
+ class SubmissionQueuedMetadata(TypedDict, total=False):
36
+ """Metadata for submission_queued event."""
37
+
38
+ status: str
39
+ submitted_by: str
40
+ expected_workflow: str
41
+
42
+
43
+ class WorkflowStartedMetadata(TypedDict, total=False):
44
+ """Metadata for workflow_started event."""
45
+
46
+ status: str
47
+ workflow_run_id: str
48
+ workflow_run_url: str
49
+
50
+
51
+ class ImplementationStatusMetadata(TypedDict, total=False):
52
+ """Metadata for implementation_status event."""
53
+
54
+ status: str
55
+ completed_steps: int
56
+ total_steps: int
57
+ step_description: str
58
+ worktree: str
59
+ branch: str
60
+
61
+
62
+ class PlanRetryMetadata(TypedDict, total=False):
63
+ """Metadata for plan_retry event."""
64
+
65
+ retry_count: int
66
+ triggered_by: str
67
+
68
+
69
+ class WorktreeCreatedMetadata(TypedDict, total=False):
70
+ """Metadata for worktree_created event."""
71
+
72
+ worktree_name: str
73
+ branch_name: str
74
+
75
+
76
+ # Union type for all metadata types
77
+ EventMetadata: TypeAlias = (
78
+ PlanCreatedMetadata
79
+ | SubmissionQueuedMetadata
80
+ | WorkflowStartedMetadata
81
+ | ImplementationStatusMetadata
82
+ | PlanRetryMetadata
83
+ | WorktreeCreatedMetadata
84
+ )
85
+
86
+
87
+ class Event(TypedDict):
88
+ """Structured event with timestamp, type, and metadata."""
89
+
90
+ timestamp: str
91
+ event_type: EventType
92
+ metadata: EventMetadata
93
+
94
+
95
+ # Type alias for event extractor functions
96
+ EventExtractor: TypeAlias = Callable[[dict], Event | None]
97
+
98
+
99
+ @click.command("log")
100
+ @click.argument("identifier", type=str)
101
+ @click.option(
102
+ "--json",
103
+ "output_json",
104
+ is_flag=True,
105
+ help="Output events as JSON instead of human-readable timeline",
106
+ )
107
+ @click.pass_obj
108
+ def plan_log(ctx: ErkContext, identifier: str, output_json: bool) -> None:
109
+ """Display chronological event log for a plan.
110
+
111
+ Shows all events from plan creation through submission, workflow execution,
112
+ implementation progress, and completion. Events are displayed in chronological
113
+ order (oldest first).
114
+
115
+ IDENTIFIER can be an issue number (e.g., "42") or a worktree name.
116
+
117
+ Examples:
118
+
119
+ \b
120
+ # View timeline for plan 42
121
+ $ erk plan log 42
122
+
123
+ # View events as JSON for scripting
124
+ $ erk plan log 42 --json
125
+
126
+ # View by worktree name
127
+ $ erk plan log erk-add-feature
128
+ """
129
+ try:
130
+ repo = discover_repo_context(ctx, ctx.cwd)
131
+ ensure_erk_metadata_dir(repo)
132
+ repo_root = repo.root
133
+
134
+ # Resolve plan identifier to issue number
135
+ plan = ctx.plan_store.get_plan(repo_root, identifier)
136
+
137
+ # Convert plan identifier to issue number (GitHub: issue number as string)
138
+ if not plan.plan_identifier.isdigit():
139
+ user_output(
140
+ click.style("Error: ", fg="red")
141
+ + f"Invalid plan identifier '{plan.plan_identifier}': not a valid issue number"
142
+ )
143
+ raise SystemExit(1)
144
+
145
+ issue_number = int(plan.plan_identifier)
146
+
147
+ # Fetch all comments for the plan issue
148
+ comment_bodies = ctx.issues.get_issue_comments(repo_root, issue_number)
149
+
150
+ # Extract events from all comments
151
+ events = _extract_events_from_comments(comment_bodies)
152
+
153
+ # Sort events chronologically (oldest first)
154
+ events.sort(key=lambda e: e["timestamp"])
155
+
156
+ # Output events
157
+ if output_json:
158
+ _output_json(events)
159
+ else:
160
+ _output_timeline(events, issue_number)
161
+
162
+ except (RuntimeError, ValueError) as e:
163
+ user_output(click.style("Error: ", fg="red") + str(e))
164
+ raise SystemExit(1) from e
165
+
166
+
167
+ def _extract_events_from_comments(comment_bodies: list[str]) -> list[Event]:
168
+ """Extract all events from comment metadata blocks.
169
+
170
+ Args:
171
+ comment_bodies: List of GitHub issue comment bodies
172
+
173
+ Returns:
174
+ List of Event objects with timestamp, event_type, and metadata fields
175
+ """
176
+ events: list[Event] = []
177
+
178
+ for comment_body in comment_bodies:
179
+ blocks = parse_metadata_blocks(comment_body)
180
+
181
+ for block in blocks:
182
+ event = _block_to_event(block.key, block.data)
183
+ if event is not None:
184
+ events.append(event)
185
+
186
+ return events
187
+
188
+
189
+ def _block_to_event(key: str, data: dict) -> Event | None:
190
+ """Convert a metadata block to an Event.
191
+
192
+ Args:
193
+ key: Metadata block key (e.g., "erk-plan", "submission-queued")
194
+ data: Metadata block data
195
+
196
+ Returns:
197
+ Event object or None if block type is not recognized
198
+ """
199
+ # Map block types to event extractors
200
+ extractors: dict[str, EventExtractor] = {
201
+ "erk-plan": _extract_plan_created_event,
202
+ "submission-queued": _extract_submission_queued_event,
203
+ "workflow-started": _extract_workflow_started_event,
204
+ "erk-implementation-status": _extract_implementation_status_event,
205
+ "plan-retry": _extract_plan_retry_event,
206
+ "erk-worktree-creation": _extract_worktree_creation_event,
207
+ }
208
+
209
+ extractor = extractors.get(key)
210
+ if extractor is None:
211
+ return None
212
+
213
+ return extractor(data)
214
+
215
+
216
+ def _extract_plan_created_event(data: dict) -> Event | None:
217
+ """Extract plan creation event from erk-plan block."""
218
+ timestamp = data.get("timestamp")
219
+ if not timestamp:
220
+ return None
221
+
222
+ metadata: PlanCreatedMetadata = {}
223
+ if "worktree_name" in data:
224
+ metadata["worktree_name"] = data["worktree_name"]
225
+ if "issue_number" in data:
226
+ metadata["issue_number"] = data["issue_number"]
227
+
228
+ return Event(
229
+ timestamp=timestamp,
230
+ event_type="plan-created",
231
+ metadata=metadata,
232
+ )
233
+
234
+
235
+ def _extract_submission_queued_event(data: dict) -> Event | None:
236
+ """Extract submission queued event from submission-queued block."""
237
+ timestamp = data.get("queued_at")
238
+ if not timestamp:
239
+ return None
240
+
241
+ metadata: SubmissionQueuedMetadata = {"status": "queued"}
242
+ if "submitted_by" in data:
243
+ metadata["submitted_by"] = data["submitted_by"]
244
+ if "expected_workflow" in data:
245
+ metadata["expected_workflow"] = data["expected_workflow"]
246
+
247
+ return Event(
248
+ timestamp=timestamp,
249
+ event_type="submission-queued",
250
+ metadata=metadata,
251
+ )
252
+
253
+
254
+ def _extract_workflow_started_event(data: dict) -> Event | None:
255
+ """Extract workflow started event from workflow-started block."""
256
+ timestamp = data.get("started_at")
257
+ if not timestamp:
258
+ return None
259
+
260
+ metadata: WorkflowStartedMetadata = {"status": "started"}
261
+ if "workflow_run_id" in data:
262
+ metadata["workflow_run_id"] = data["workflow_run_id"]
263
+ if "workflow_run_url" in data:
264
+ metadata["workflow_run_url"] = data["workflow_run_url"]
265
+
266
+ return Event(
267
+ timestamp=timestamp,
268
+ event_type="workflow-started",
269
+ metadata=metadata,
270
+ )
271
+
272
+
273
+ def _extract_implementation_status_event(data: dict) -> Event | None:
274
+ """Extract implementation status event from erk-implementation-status block."""
275
+ timestamp = data.get("timestamp")
276
+ if not timestamp:
277
+ return None
278
+
279
+ status = data.get("status")
280
+ if not status:
281
+ return None
282
+
283
+ metadata: ImplementationStatusMetadata = {"status": status}
284
+
285
+ if "completed_steps" in data:
286
+ metadata["completed_steps"] = data["completed_steps"]
287
+ if "total_steps" in data:
288
+ metadata["total_steps"] = data["total_steps"]
289
+ if "step_description" in data:
290
+ metadata["step_description"] = data["step_description"]
291
+ if "worktree" in data:
292
+ metadata["worktree"] = data["worktree"]
293
+ if "branch" in data:
294
+ metadata["branch"] = data["branch"]
295
+
296
+ return Event(
297
+ timestamp=timestamp,
298
+ event_type="implementation-status",
299
+ metadata=metadata,
300
+ )
301
+
302
+
303
+ def _extract_plan_retry_event(data: dict) -> Event | None:
304
+ """Extract plan retry event from plan-retry block."""
305
+ timestamp = data.get("retry_timestamp")
306
+ if not timestamp:
307
+ return None
308
+
309
+ metadata: PlanRetryMetadata = {}
310
+ if "retry_count" in data:
311
+ metadata["retry_count"] = data["retry_count"]
312
+ if "triggered_by" in data:
313
+ metadata["triggered_by"] = data["triggered_by"]
314
+
315
+ return Event(
316
+ timestamp=timestamp,
317
+ event_type="plan-retry",
318
+ metadata=metadata,
319
+ )
320
+
321
+
322
+ def _extract_worktree_creation_event(data: dict) -> Event | None:
323
+ """Extract worktree creation event from erk-worktree-creation block."""
324
+ timestamp = data.get("timestamp")
325
+ if not timestamp:
326
+ return None
327
+
328
+ metadata: WorktreeCreatedMetadata = {}
329
+ if "worktree_name" in data:
330
+ metadata["worktree_name"] = data["worktree_name"]
331
+ if "branch_name" in data:
332
+ metadata["branch_name"] = data["branch_name"]
333
+
334
+ return Event(
335
+ timestamp=timestamp,
336
+ event_type="worktree-created",
337
+ metadata=metadata,
338
+ )
339
+
340
+
341
+ def _output_json(events: list[Event]) -> None:
342
+ """Output events as JSON array."""
343
+ user_output(json.dumps(events, indent=2))
344
+
345
+
346
+ def _output_timeline(events: list[Event], issue_number: int) -> None:
347
+ """Output events as human-readable timeline.
348
+
349
+ Args:
350
+ events: List of Event objects sorted chronologically
351
+ issue_number: GitHub issue number for the plan
352
+ """
353
+ if not events:
354
+ user_output(f"No events found for plan #{issue_number}")
355
+ return
356
+
357
+ user_output(f"Plan #{issue_number} Event Timeline\n")
358
+
359
+ for event in events:
360
+ # Format timestamp as human-readable
361
+ timestamp_str = _format_timestamp(event["timestamp"])
362
+
363
+ # Format event description
364
+ description = _format_event_description(event)
365
+
366
+ # Output timeline entry
367
+ user_output(f"[{timestamp_str}] {description}")
368
+
369
+
370
+ def _format_timestamp(iso_timestamp: str) -> str:
371
+ """Format ISO 8601 timestamp as human-readable string.
372
+
373
+ Args:
374
+ iso_timestamp: ISO 8601 timestamp string
375
+
376
+ Returns:
377
+ Formatted timestamp like "2024-01-15 12:30:45 UTC"
378
+ """
379
+ try:
380
+ dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
381
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
382
+ except (ValueError, AttributeError):
383
+ # Fallback: return original if parsing fails
384
+ return iso_timestamp
385
+
386
+
387
+ def _format_event_description(event: Event) -> str:
388
+ """Format event as human-readable description.
389
+
390
+ Args:
391
+ event: Event object with event_type and metadata
392
+
393
+ Returns:
394
+ Formatted description string
395
+ """
396
+ event_type = event["event_type"]
397
+ metadata = event["metadata"]
398
+
399
+ if event_type == "plan-created":
400
+ worktree = metadata.get("worktree_name", "unknown")
401
+ return f"Plan created: worktree '{worktree}' assigned"
402
+
403
+ if event_type == "submission-queued":
404
+ submitted_by = metadata.get("submitted_by", "unknown")
405
+ return f"Queued for execution by {submitted_by}"
406
+
407
+ if event_type == "workflow-started":
408
+ workflow_url = metadata.get("workflow_run_url", "")
409
+ return f"GitHub Actions workflow started: {workflow_url}"
410
+
411
+ if event_type == "implementation-status":
412
+ status = metadata.get("status", "unknown")
413
+
414
+ if status == "starting":
415
+ worktree = metadata.get("worktree", "unknown")
416
+ return f"Implementation starting in worktree '{worktree}'"
417
+
418
+ if status == "in_progress":
419
+ return "Implementation in progress"
420
+
421
+ if status == "complete":
422
+ return "Implementation complete"
423
+
424
+ if status == "failed":
425
+ return "Implementation failed"
426
+
427
+ return f"Status: {status}"
428
+
429
+ if event_type == "plan-retry":
430
+ retry_count = metadata.get("retry_count", "unknown")
431
+ triggered_by = metadata.get("triggered_by", "unknown")
432
+ return f"Retry #{retry_count} triggered by {triggered_by}"
433
+
434
+ if event_type == "worktree-created":
435
+ worktree = metadata.get("worktree_name", "unknown")
436
+ branch = metadata.get("branch_name", "unknown")
437
+ return f"Worktree created: '{worktree}' (branch: {branch})"
438
+
439
+ # Fallback for unknown event types
440
+ return f"Event: {event_type}"