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,754 @@
1
+ """Command to list plans with filtering."""
2
+
3
+ from collections.abc import Callable
4
+ from datetime import datetime
5
+ from typing import ParamSpec, TypeVar
6
+
7
+ import click
8
+ from rich.console import Console, Group
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from erk.cli.core import discover_repo_context
13
+ from erk.core.context import ErkContext
14
+ from erk.core.display import LiveDisplay
15
+ from erk.core.display_utils import (
16
+ format_relative_time,
17
+ format_workflow_outcome,
18
+ format_workflow_run_id,
19
+ get_workflow_run_state,
20
+ )
21
+ from erk.core.pr_utils import select_display_pr
22
+ from erk.core.repo_discovery import ensure_erk_metadata_dir
23
+ from erk.tui.app import ErkDashApp
24
+ from erk.tui.data.provider import RealPlanDataProvider
25
+ from erk.tui.data.types import PlanFilters
26
+ from erk.tui.sorting.types import SortKey, SortState
27
+ from erk_shared.gateway.browser.real import RealBrowserLauncher
28
+ from erk_shared.gateway.clipboard.real import RealClipboard
29
+ from erk_shared.github.emoji import format_checks_cell, get_pr_status_emoji
30
+ from erk_shared.github.issues import IssueInfo
31
+ from erk_shared.github.metadata.plan_header import (
32
+ extract_plan_header_local_impl_at,
33
+ extract_plan_header_local_impl_event,
34
+ extract_plan_header_remote_impl_at,
35
+ extract_plan_header_source_repo,
36
+ extract_plan_header_worktree_name,
37
+ )
38
+ from erk_shared.github.types import GitHubRepoId, GitHubRepoLocation, PullRequestInfo
39
+ from erk_shared.impl_folder import read_issue_reference
40
+ from erk_shared.output.output import user_output
41
+ from erk_shared.plan_store.types import Plan, PlanState
42
+
43
+ P = ParamSpec("P")
44
+ T = TypeVar("T")
45
+
46
+
47
+ def _issue_to_plan(issue: IssueInfo) -> Plan:
48
+ """Convert IssueInfo to Plan format.
49
+
50
+ Args:
51
+ issue: IssueInfo from GraphQL query
52
+
53
+ Returns:
54
+ Plan object with equivalent data
55
+ """
56
+ # Map issue state to PlanState
57
+ state = PlanState.OPEN if issue.state == "OPEN" else PlanState.CLOSED
58
+
59
+ return Plan(
60
+ plan_identifier=str(issue.number),
61
+ title=issue.title,
62
+ body=issue.body,
63
+ state=state,
64
+ url=issue.url,
65
+ labels=issue.labels,
66
+ assignees=issue.assignees,
67
+ created_at=issue.created_at,
68
+ updated_at=issue.updated_at,
69
+ metadata={"number": issue.number},
70
+ )
71
+
72
+
73
+ def format_pr_cell(pr: PullRequestInfo, *, use_graphite: bool, graphite_url: str | None) -> str:
74
+ """Format PR cell with clickable link and emoji: #123 👀 or #123 👀🔗
75
+
76
+ The 🔗 emoji is appended for PRs that will auto-close the linked issue when merged.
77
+
78
+ Args:
79
+ pr: PR information
80
+ use_graphite: If True, use Graphite URL; if False, use GitHub URL
81
+ graphite_url: Graphite URL for the PR (None if unavailable)
82
+
83
+ Returns:
84
+ Formatted string for table cell with OSC 8 hyperlink
85
+ """
86
+ emoji = get_pr_status_emoji(pr)
87
+ pr_text = f"#{pr.number}"
88
+
89
+ # Append 🔗 for PRs that will close the issue when merged
90
+ if pr.will_close_target:
91
+ emoji += "🔗"
92
+
93
+ # Determine which URL to use
94
+ url = graphite_url if use_graphite else pr.url
95
+
96
+ # Make PR number clickable if URL is available
97
+ # Rich supports OSC 8 via [link=...] markup
98
+ if url:
99
+ return f"[link={url}]{pr_text}[/link] {emoji}"
100
+ else:
101
+ return f"{pr_text} {emoji}"
102
+
103
+
104
+ def format_worktree_name_cell(worktree_name: str, exists_locally: bool) -> str:
105
+ """Format worktree name with existence styling.
106
+
107
+ Args:
108
+ worktree_name: Name of the worktree
109
+ exists_locally: Whether the worktree exists on the local machine
110
+
111
+ Returns:
112
+ Formatted string with Rich markup:
113
+ - Exists locally: "[yellow]name[/yellow]"
114
+ - Doesn't exist: "-"
115
+ """
116
+ if not exists_locally:
117
+ return "-"
118
+ return f"[yellow]{worktree_name}[/yellow]"
119
+
120
+
121
+ def format_local_run_cell(
122
+ last_local_impl_at: str | None,
123
+ last_local_impl_event: str | None,
124
+ ) -> str:
125
+ """Format last local implementation event as relative time with indicator.
126
+
127
+ Args:
128
+ last_local_impl_at: ISO timestamp of last local implementation, or None
129
+ last_local_impl_event: Event type ("started" or "ended"), or None
130
+
131
+ Returns:
132
+ Relative time string with event indicator (e.g., "⟳ 2h" or "✓ 2h") or "-" if no timestamp
133
+ """
134
+ relative_time = format_relative_time(last_local_impl_at)
135
+ if not relative_time:
136
+ return "-"
137
+
138
+ # Add event indicator
139
+ if last_local_impl_event == "started":
140
+ return f"⟳ {relative_time}"
141
+ if last_local_impl_event == "ended":
142
+ return f"✓ {relative_time}"
143
+
144
+ # Fallback for missing event (backward compatibility)
145
+ return relative_time
146
+
147
+
148
+ def format_remote_run_cell(last_remote_impl_at: str | None) -> str:
149
+ """Format last remote implementation timestamp as relative time.
150
+
151
+ Args:
152
+ last_remote_impl_at: ISO timestamp of last remote (GitHub Actions) implementation, or None
153
+
154
+ Returns:
155
+ Relative time string (e.g., "2h ago") or "-" if no timestamp
156
+ """
157
+ relative_time = format_relative_time(last_remote_impl_at)
158
+ return relative_time if relative_time else "-"
159
+
160
+
161
+ def plan_filter_options(f: Callable[P, T]) -> Callable[P, T]:
162
+ """Shared filter options for plan list commands."""
163
+ f = click.option(
164
+ "--label",
165
+ multiple=True,
166
+ help="Filter by label (can be specified multiple times for AND logic)",
167
+ )(f)
168
+ f = click.option(
169
+ "--state",
170
+ type=click.Choice(["open", "closed"], case_sensitive=False),
171
+ help="Filter by state",
172
+ )(f)
173
+ f = click.option(
174
+ "--run-state",
175
+ type=click.Choice(
176
+ ["queued", "in_progress", "success", "failure", "cancelled"], case_sensitive=False
177
+ ),
178
+ help="Filter by workflow run state",
179
+ )(f)
180
+ f = click.option(
181
+ "--limit",
182
+ type=int,
183
+ help="Maximum number of results to return",
184
+ )(f)
185
+ f = click.option(
186
+ "--all-users",
187
+ "-A",
188
+ is_flag=True,
189
+ default=False,
190
+ help="Show plans from all users (default: show only your plans)",
191
+ )(f)
192
+ f = click.option(
193
+ "--sort",
194
+ type=click.Choice(["issue", "activity"], case_sensitive=False),
195
+ default="issue",
196
+ help="Sort order: by issue number (default) or recent branch activity",
197
+ )(f)
198
+ return f
199
+
200
+
201
+ def dash_options(f: Callable[P, T]) -> Callable[P, T]:
202
+ """TUI-specific options for dash command."""
203
+ f = click.option(
204
+ "--interval",
205
+ type=float,
206
+ default=15.0,
207
+ help="Refresh interval in seconds (default: 15.0)",
208
+ )(f)
209
+ return f
210
+
211
+
212
+ def _build_plans_table(
213
+ ctx: ErkContext,
214
+ label: tuple[str, ...],
215
+ state: str | None,
216
+ run_state: str | None,
217
+ runs: bool,
218
+ limit: int | None,
219
+ all_users: bool,
220
+ sort: str,
221
+ ) -> tuple[Table | None, int]:
222
+ """Build plan dashboard table.
223
+
224
+ Uses PlanListService to batch all API calls into 2 total:
225
+ 1. Single unified GraphQL query for issues + PR linkages
226
+ 2. REST API calls for workflow runs (one per issue with run_id)
227
+
228
+ Returns:
229
+ Tuple of (table, plan_count). Table is None if no plans found.
230
+ """
231
+ repo = discover_repo_context(ctx, ctx.cwd)
232
+ ensure_erk_metadata_dir(repo) # Ensure erk metadata directories exist
233
+ repo_root = repo.root # Use git repository root for GitHub operations
234
+
235
+ # Build labels list - default to ["erk-plan"] if no labels specified
236
+ labels_list = list(label) if label else ["erk-plan"]
237
+
238
+ # Determine if we need workflow runs (for display or filtering)
239
+ needs_workflow_runs = runs or run_state is not None
240
+
241
+ # Get owner/repo from RepoContext (already populated via git remote URL parsing)
242
+ if repo.github is None:
243
+ user_output(click.style("Error: ", fg="red") + "Could not determine repository owner/name")
244
+ raise SystemExit(1)
245
+ owner = repo.github.owner
246
+ repo_name = repo.github.repo
247
+
248
+ # Determine creator filter: None for all users, authenticated username otherwise
249
+ creator: str | None = None
250
+ if not all_users:
251
+ is_authenticated, username, _ = ctx.github.check_auth_status()
252
+ if is_authenticated and username:
253
+ creator = username
254
+
255
+ # Use PlanListService for batched API calls
256
+ # Skip workflow runs when not needed for better performance
257
+ # PR linkages are always fetched via unified GraphQL query (no performance penalty)
258
+ try:
259
+ location = GitHubRepoLocation(root=repo_root, repo_id=GitHubRepoId(owner, repo_name))
260
+ plan_data = ctx.plan_list_service.get_plan_list_data(
261
+ location=location,
262
+ labels=labels_list,
263
+ state=state,
264
+ limit=limit,
265
+ skip_workflow_runs=not needs_workflow_runs,
266
+ creator=creator,
267
+ )
268
+ except RuntimeError as e:
269
+ user_output(click.style("Error: ", fg="red") + str(e))
270
+ raise SystemExit(1) from e
271
+
272
+ # Convert IssueInfo to Plan objects
273
+ plans = [_issue_to_plan(issue) for issue in plan_data.issues]
274
+
275
+ if not plans:
276
+ return None, 0
277
+
278
+ # Use pre-fetched data from PlanListService
279
+ pr_linkages = plan_data.pr_linkages
280
+ workflow_runs = plan_data.workflow_runs
281
+
282
+ # Build local worktree mapping from .impl/issue.json files
283
+ worktree_by_issue: dict[int, str] = {}
284
+ worktrees = ctx.git.list_worktrees(repo_root)
285
+ for worktree in worktrees:
286
+ impl_folder = worktree.path / ".impl"
287
+ if impl_folder.exists() and impl_folder.is_dir():
288
+ issue_ref = read_issue_reference(impl_folder)
289
+ if issue_ref is not None:
290
+ # If multiple worktrees have same issue, keep first found
291
+ if issue_ref.issue_number not in worktree_by_issue:
292
+ worktree_by_issue[issue_ref.issue_number] = worktree.path.name
293
+
294
+ # Apply run state filter if specified
295
+ if run_state:
296
+ filtered_plans: list[Plan] = []
297
+ for plan in plans:
298
+ # Get workflow run (keyed by issue number)
299
+ plan_issue_number = plan.metadata.get("number")
300
+ workflow_run = None
301
+ if isinstance(plan_issue_number, int):
302
+ workflow_run = workflow_runs.get(plan_issue_number)
303
+ if workflow_run is None:
304
+ # No workflow run - skip this plan when filtering
305
+ continue
306
+ plan_run_state = get_workflow_run_state(workflow_run)
307
+ if plan_run_state == run_state:
308
+ filtered_plans.append(plan)
309
+ plans = filtered_plans
310
+
311
+ # Check if filtering resulted in no plans
312
+ if not plans:
313
+ return None, 0
314
+
315
+ # Build activity timestamps for display column (always computed)
316
+ trunk = ctx.git.detect_trunk_branch(repo_root)
317
+
318
+ # Build issue -> branch mapping from worktrees
319
+ issue_to_branch: dict[int, str] = {}
320
+ for wt in worktrees:
321
+ impl_folder = wt.path / ".impl"
322
+ if impl_folder.exists() and impl_folder.is_dir():
323
+ issue_ref = read_issue_reference(impl_folder)
324
+ if issue_ref is not None and wt.branch is not None:
325
+ issue_to_branch[issue_ref.issue_number] = wt.branch
326
+
327
+ # Build activity timestamps for display and sorting
328
+ activity_by_issue: dict[int, str] = {}
329
+ for issue_num, branch in issue_to_branch.items():
330
+ timestamp = ctx.git.get_branch_last_commit_time(repo_root, branch, trunk)
331
+ if timestamp is not None:
332
+ activity_by_issue[issue_num] = timestamp
333
+
334
+ # Apply activity-based sorting if requested
335
+ if sort == "activity":
336
+
337
+ def get_last_commit_time(plan: Plan) -> tuple[bool, datetime]:
338
+ """Return sort key: (has_local_activity, commit_time).
339
+
340
+ Plans with local activity sort first (by recency), then others by issue#.
341
+ """
342
+ issue_number = plan.metadata.get("number")
343
+ if not isinstance(issue_number, int) or issue_number not in activity_by_issue:
344
+ return (False, datetime.min)
345
+ return (True, datetime.fromisoformat(activity_by_issue[issue_number]))
346
+
347
+ # Sort: plans with local activity first (by recency), then others by issue#
348
+ plans = sorted(plans, key=get_last_commit_time, reverse=True)
349
+
350
+ # Determine use_graphite for URL selection
351
+ use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
352
+
353
+ # Check if any plan has source_repo (for cross-repo plans column)
354
+ has_cross_repo_plans = any(
355
+ plan.body and extract_plan_header_source_repo(plan.body) for plan in plans
356
+ )
357
+
358
+ # Create Rich table with columns
359
+ table = Table(show_header=True, header_style="bold")
360
+ table.add_column("plan", style="cyan", no_wrap=True)
361
+ table.add_column("title", no_wrap=True)
362
+ if has_cross_repo_plans:
363
+ table.add_column("impl-repo", no_wrap=True)
364
+ table.add_column("pr", no_wrap=True)
365
+ table.add_column("chks", no_wrap=True)
366
+ table.add_column("lcl-wt", no_wrap=True)
367
+ table.add_column("lcl-actvty", no_wrap=True)
368
+ table.add_column("lcl-impl", no_wrap=True)
369
+ if runs:
370
+ table.add_column("remote-impl", no_wrap=True)
371
+ table.add_column("run-id", no_wrap=True)
372
+ table.add_column("run-state", no_wrap=True, width=12)
373
+
374
+ # Populate table rows
375
+ for plan in plans:
376
+ # Format issue number with clickable OSC 8 hyperlink
377
+ id_text = f"#{plan.plan_identifier}"
378
+ colored_id = f"[cyan]{id_text}[/cyan]"
379
+
380
+ # Make ID clickable using OSC 8 if URL is available
381
+ if plan.url:
382
+ # Rich library supports OSC 8 via markup syntax
383
+ issue_id = f"[link={plan.url}]{colored_id}[/link]"
384
+ else:
385
+ issue_id = colored_id
386
+
387
+ # Truncate title to 50 characters with ellipsis
388
+ title = plan.title
389
+ if len(title) > 50:
390
+ title = title[:47] + "..."
391
+
392
+ # Query worktree status - check local .impl/issue.json first, then issue body
393
+ issue_number = plan.metadata.get("number")
394
+ worktree_name = ""
395
+ exists_locally = False
396
+ last_local_impl_at: str | None = None
397
+ last_local_impl_event: str | None = None
398
+ last_remote_impl_at: str | None = None
399
+
400
+ # Check local mapping first (worktree exists locally)
401
+ if isinstance(issue_number, int) and issue_number in worktree_by_issue:
402
+ worktree_name = worktree_by_issue[issue_number]
403
+ exists_locally = True
404
+
405
+ # Extract from issue body - worktree may or may not exist locally
406
+ source_repo: str | None = None
407
+ if plan.body:
408
+ extracted = extract_plan_header_worktree_name(plan.body)
409
+ if extracted:
410
+ # If we don't have a local name yet, use the one from issue body
411
+ if not worktree_name:
412
+ worktree_name = extracted
413
+ # Extract implementation timestamps and event
414
+ last_local_impl_at = extract_plan_header_local_impl_at(plan.body)
415
+ last_local_impl_event = extract_plan_header_local_impl_event(plan.body)
416
+ last_remote_impl_at = extract_plan_header_remote_impl_at(plan.body)
417
+ # Extract source_repo for cross-repo plans
418
+ source_repo = extract_plan_header_source_repo(plan.body)
419
+
420
+ # Format the worktree cells
421
+ worktree_name_cell = format_worktree_name_cell(worktree_name, exists_locally)
422
+ local_run_cell = format_local_run_cell(last_local_impl_at, last_local_impl_event)
423
+ remote_run_cell = format_remote_run_cell(last_remote_impl_at)
424
+
425
+ # Get PR info for this issue
426
+ pr_cell = "-"
427
+ checks_cell = "-"
428
+ if isinstance(issue_number, int) and issue_number in pr_linkages:
429
+ issue_prs = pr_linkages[issue_number]
430
+ selected_pr = select_display_pr(issue_prs)
431
+ if selected_pr is not None:
432
+ graphite_url = ctx.graphite.get_graphite_url(
433
+ GitHubRepoId(selected_pr.owner, selected_pr.repo), selected_pr.number
434
+ )
435
+ pr_cell = format_pr_cell(
436
+ selected_pr, use_graphite=use_graphite, graphite_url=graphite_url
437
+ )
438
+ checks_cell = format_checks_cell(selected_pr)
439
+
440
+ # Get workflow run for this plan (keyed by issue number)
441
+ run_id_cell = "-"
442
+ workflow_run = None
443
+ if isinstance(issue_number, int):
444
+ workflow_run = workflow_runs.get(issue_number)
445
+ if workflow_run is not None:
446
+ # Build workflow URL from plan.url attribute
447
+ workflow_url = None
448
+ if plan.url:
449
+ # Parse owner/repo from URL like https://github.com/owner/repo/issues/123
450
+ parts = plan.url.split("/")
451
+ if len(parts) >= 5:
452
+ owner = parts[-4]
453
+ repo_name = parts[-3]
454
+ workflow_url = (
455
+ f"https://github.com/{owner}/{repo_name}/actions/runs/{workflow_run.run_id}"
456
+ )
457
+ # Format the run ID with linkification
458
+ run_id_cell = format_workflow_run_id(workflow_run, workflow_url)
459
+
460
+ # Format workflow run outcome
461
+ run_outcome_cell = format_workflow_outcome(workflow_run)
462
+
463
+ # Format activity cell (last commit time on local branch)
464
+ activity_cell = "-"
465
+ if isinstance(issue_number, int) and issue_number in activity_by_issue:
466
+ activity_cell = format_relative_time(activity_by_issue[issue_number]) or "-"
467
+
468
+ # Build row based on which columns are enabled
469
+ row: list[str] = [
470
+ issue_id,
471
+ title,
472
+ ]
473
+ if has_cross_repo_plans:
474
+ # Show just repo name (owner/repo -> repo) for brevity
475
+ impl_repo_cell = source_repo.split("/")[-1] if source_repo else "-"
476
+ row.append(impl_repo_cell)
477
+ row.extend(
478
+ [
479
+ pr_cell,
480
+ checks_cell,
481
+ worktree_name_cell,
482
+ activity_cell,
483
+ local_run_cell,
484
+ ]
485
+ )
486
+ if runs:
487
+ row.extend([remote_run_cell, run_id_cell, run_outcome_cell])
488
+ table.add_row(*row)
489
+
490
+ return table, len(plans)
491
+
492
+
493
+ def _list_plans_impl(
494
+ ctx: ErkContext,
495
+ label: tuple[str, ...],
496
+ state: str | None,
497
+ run_state: str | None,
498
+ runs: bool,
499
+ limit: int | None,
500
+ all_users: bool,
501
+ sort: str,
502
+ ) -> None:
503
+ """Implementation logic for listing plans with optional filters."""
504
+ table, plan_count = _build_plans_table(
505
+ ctx, label, state, run_state, runs, limit, all_users, sort
506
+ )
507
+
508
+ if table is None:
509
+ user_output("No plans found matching the criteria.")
510
+ return
511
+
512
+ # Display results header
513
+ user_output(f"\nFound {plan_count} plan(s):\n")
514
+
515
+ # Output table to stderr (consistent with user_output convention)
516
+ # Use width=200 to ensure proper display without truncation
517
+ # force_terminal=True ensures hyperlinks render even when Rich doesn't detect a TTY
518
+ console = Console(stderr=True, width=200, force_terminal=True)
519
+ console.print(table)
520
+ console.print() # Add blank line after table
521
+
522
+
523
+ def _build_watch_content(
524
+ table: Table | None,
525
+ count: int,
526
+ last_update: str,
527
+ seconds_remaining: int,
528
+ fetch_duration_secs: float | None = None,
529
+ ) -> Group | Panel:
530
+ """Build display content for watch mode.
531
+
532
+ Args:
533
+ table: The plans table, or None if no plans
534
+ count: Number of plans found
535
+ last_update: Formatted time of last data refresh
536
+ seconds_remaining: Seconds until next refresh
537
+ fetch_duration_secs: Duration of last data fetch in seconds, or None
538
+
539
+ Returns:
540
+ Rich renderable content for the display
541
+ """
542
+ # Build duration suffix
543
+ duration_suffix = f" ({fetch_duration_secs:.1f}s)" if fetch_duration_secs is not None else ""
544
+
545
+ footer = (
546
+ f"Found {count} plan(s) | Updated: {last_update}{duration_suffix} | "
547
+ f"Next refresh: {seconds_remaining}s | Ctrl+C to exit"
548
+ )
549
+
550
+ if table is None:
551
+ return Panel(f"No plans found.\n\n{footer}", title="erk dash --watch")
552
+ else:
553
+ return Group(table, Panel(footer, style="dim"))
554
+
555
+
556
+ def _run_watch_loop(
557
+ ctx: ErkContext,
558
+ live_display: LiveDisplay,
559
+ build_table_fn: Callable[[], tuple[Table | None, int]],
560
+ interval: float,
561
+ ) -> None:
562
+ """Run watch loop until KeyboardInterrupt.
563
+
564
+ Updates display every second with countdown timer. Fetches fresh data
565
+ when countdown reaches zero.
566
+
567
+ Args:
568
+ ctx: ErkContext with time abstraction
569
+ live_display: Display renderer for live updates
570
+ build_table_fn: Function that returns (table, count)
571
+ interval: Seconds between data refreshes
572
+ """
573
+ live_display.start()
574
+ try:
575
+ # Initial data fetch - with timing
576
+ start = ctx.time.now()
577
+ table, count = build_table_fn()
578
+ fetch_duration_secs = (ctx.time.now() - start).total_seconds()
579
+ last_update = ctx.time.now().strftime("%H:%M:%S")
580
+ seconds_remaining = int(interval)
581
+
582
+ while True:
583
+ # Update display with current countdown
584
+ content = _build_watch_content(
585
+ table, count, last_update, seconds_remaining, fetch_duration_secs
586
+ )
587
+ live_display.update(content)
588
+
589
+ # Sleep for 1 second
590
+ ctx.time.sleep(1.0)
591
+ seconds_remaining -= 1
592
+
593
+ # Refresh data when countdown reaches zero
594
+ if seconds_remaining <= 0:
595
+ start = ctx.time.now()
596
+ table, count = build_table_fn()
597
+ fetch_duration_secs = (ctx.time.now() - start).total_seconds()
598
+ last_update = ctx.time.now().strftime("%H:%M:%S")
599
+ seconds_remaining = int(interval)
600
+ except KeyboardInterrupt:
601
+ pass
602
+ finally:
603
+ live_display.stop()
604
+
605
+
606
+ def _run_interactive_mode(
607
+ ctx: ErkContext,
608
+ label: tuple[str, ...],
609
+ state: str | None,
610
+ run_state: str | None,
611
+ runs: bool,
612
+ prs: bool,
613
+ limit: int | None,
614
+ interval: float,
615
+ all_users: bool,
616
+ sort: str,
617
+ ) -> None:
618
+ """Run interactive TUI mode.
619
+
620
+ Args:
621
+ ctx: ErkContext with all dependencies
622
+ label: Labels to filter by
623
+ state: State filter ("open", "closed", or None)
624
+ run_state: Workflow run state filter
625
+ runs: Whether to show run columns
626
+ prs: Whether to show PR columns
627
+ limit: Maximum number of results
628
+ interval: Refresh interval in seconds
629
+ all_users: If True, show plans from all users; if False, filter to authenticated user
630
+ sort: Sort order ("issue" or "activity")
631
+ """
632
+ repo = discover_repo_context(ctx, ctx.cwd)
633
+ ensure_erk_metadata_dir(repo)
634
+ repo_root = repo.root
635
+
636
+ # Get owner/repo from RepoContext (already populated via git remote URL parsing)
637
+ if repo.github is None:
638
+ user_output(click.style("Error: ", fg="red") + "Could not determine repository owner/name")
639
+ raise SystemExit(1)
640
+ owner = repo.github.owner
641
+ repo_name = repo.github.repo
642
+
643
+ # Determine creator filter: None for all users, authenticated username otherwise
644
+ creator: str | None = None
645
+ if not all_users:
646
+ is_authenticated, username, _ = ctx.github.check_auth_status()
647
+ if is_authenticated and username:
648
+ creator = username
649
+
650
+ # Build labels - default to ["erk-plan"]
651
+ labels = label if label else ("erk-plan",)
652
+
653
+ # Create data provider and filters
654
+ location = GitHubRepoLocation(root=repo_root, repo_id=GitHubRepoId(owner, repo_name))
655
+ clipboard = RealClipboard()
656
+ browser = RealBrowserLauncher()
657
+ provider = RealPlanDataProvider(ctx, location, clipboard, browser)
658
+ filters = PlanFilters(
659
+ labels=labels,
660
+ state=state,
661
+ run_state=run_state,
662
+ limit=limit,
663
+ show_prs=prs,
664
+ show_runs=runs,
665
+ creator=creator,
666
+ )
667
+
668
+ # Convert sort string to SortState
669
+ initial_sort = SortState(
670
+ key=SortKey.BRANCH_ACTIVITY if sort == "activity" else SortKey.ISSUE_NUMBER
671
+ )
672
+
673
+ # Run the TUI app
674
+ app = ErkDashApp(provider, filters, refresh_interval=interval, initial_sort=initial_sort)
675
+ app.run()
676
+
677
+
678
+ @click.command("list")
679
+ @plan_filter_options
680
+ @click.option(
681
+ "--runs",
682
+ "-r",
683
+ is_flag=True,
684
+ default=False,
685
+ help="Show workflow run columns (run-id, run-state)",
686
+ )
687
+ @click.pass_obj
688
+ def list_plans(
689
+ ctx: ErkContext,
690
+ label: tuple[str, ...],
691
+ state: str | None,
692
+ run_state: str | None,
693
+ limit: int | None,
694
+ all_users: bool,
695
+ sort: str,
696
+ runs: bool,
697
+ ) -> None:
698
+ """List plans as a static table.
699
+
700
+ By default, shows only plans created by you. Use --all-users (-A)
701
+ to show plans from all users.
702
+
703
+ Examples:
704
+ erk plan list # Your plans only
705
+ erk plan list --all-users # All users' plans
706
+ erk plan list -A # All users' plans (short form)
707
+ erk plan list --state open
708
+ erk plan list --label erk-plan --label bug
709
+ erk plan list --limit 10
710
+ erk plan list --run-state in_progress
711
+ erk plan list --runs
712
+ erk plan list --sort activity # Sort by recent branch activity
713
+ """
714
+ _list_plans_impl(ctx, label, state, run_state, runs, limit, all_users, sort)
715
+
716
+
717
+ @click.command("dash")
718
+ @plan_filter_options
719
+ @dash_options
720
+ @click.pass_obj
721
+ def dash(
722
+ ctx: ErkContext,
723
+ label: tuple[str, ...],
724
+ state: str | None,
725
+ run_state: str | None,
726
+ limit: int | None,
727
+ all_users: bool,
728
+ sort: str, # noqa: ARG001 # Accepted from shared options but not used by TUI
729
+ interval: float,
730
+ ) -> None:
731
+ """Interactive plan dashboard (TUI).
732
+
733
+ By default, shows only plans created by you. Use --all-users (-A)
734
+ to show plans from all users.
735
+
736
+ Launches an interactive terminal UI for viewing and managing plans.
737
+ Shows all columns (runs) by default. For a static table output, use
738
+ 'erk plan list' instead.
739
+
740
+ Examples:
741
+ erk dash # Your plans only
742
+ erk dash --all-users # All users' plans
743
+ erk dash -A # All users' plans (short form)
744
+ erk dash --interval 10
745
+ erk dash --label erk-plan --state open
746
+ erk dash --limit 10
747
+ erk dash --run-state in_progress
748
+ erk dash --sort activity # Sort by recent branch activity
749
+ """
750
+ # Default to showing all columns (runs=True)
751
+ prs = True # Always show PRs
752
+ runs = True # Default to showing runs
753
+
754
+ _run_interactive_mode(ctx, label, state, run_state, runs, prs, limit, interval, all_users, sort)