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,404 @@
1
+ """Data models for status information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class WorktreeDisplayInfo:
11
+ """Worktree information for display/presentation purposes.
12
+
13
+ This represents worktree data for status rendering and display.
14
+ For infrastructure-layer worktree data, see erk.core.gitops.WorktreeInfo.
15
+ """
16
+
17
+ name: str
18
+ path: Path
19
+ branch: str | None
20
+ is_root: bool
21
+
22
+ @staticmethod
23
+ def root(path: Path, branch: str = "main", name: str = "root") -> WorktreeDisplayInfo:
24
+ """Create root worktree for test display.
25
+
26
+ Args:
27
+ path: Path to the root worktree
28
+ branch: Branch name (default: "main")
29
+ name: Display name (default: "root")
30
+
31
+ Returns:
32
+ WorktreeDisplayInfo with is_root=True
33
+
34
+ Example:
35
+ Before (4 lines):
36
+ worktree = WorktreeDisplayInfo(
37
+ name="root", path=repo_root, branch="main", is_root=True
38
+ )
39
+
40
+ After (1 line):
41
+ worktree = WorktreeDisplayInfo.root(repo_root)
42
+ """
43
+ return WorktreeDisplayInfo(path=path, branch=branch, is_root=True, name=name)
44
+
45
+ @staticmethod
46
+ def feature(path: Path, branch: str, name: str | None = None) -> WorktreeDisplayInfo:
47
+ """Create feature worktree for test display.
48
+
49
+ Args:
50
+ path: Path to the feature worktree
51
+ branch: Branch name
52
+ name: Display name (default: uses path.name)
53
+
54
+ Returns:
55
+ WorktreeDisplayInfo with is_root=False
56
+
57
+ Example:
58
+ Before (4 lines):
59
+ worktree = WorktreeDisplayInfo(
60
+ name="my-feature", path=feature_wt, branch="feature", is_root=False
61
+ )
62
+
63
+ After (1 line):
64
+ worktree = WorktreeDisplayInfo.feature(feature_wt, "feature")
65
+ """
66
+ display_name = name if name else path.name
67
+ return WorktreeDisplayInfo(path=path, branch=branch, is_root=False, name=display_name)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class CommitInfo:
72
+ """Information about a git commit."""
73
+
74
+ sha: str
75
+ message: str
76
+ author: str
77
+ date: str
78
+
79
+ @staticmethod
80
+ def test_commit(
81
+ sha: str = "abc1234",
82
+ message: str = "Test commit",
83
+ author: str = "Test User",
84
+ date: str = "1 hour ago",
85
+ ) -> CommitInfo:
86
+ """Create a commit for tests with sensible defaults.
87
+
88
+ Args:
89
+ sha: Commit SHA (default: "abc1234")
90
+ message: Commit message (default: "Test commit")
91
+ author: Commit author (default: "Test User")
92
+ date: Commit date (default: "1 hour ago")
93
+
94
+ Returns:
95
+ CommitInfo with all fields populated
96
+
97
+ Example:
98
+ Before (5 lines):
99
+ recent_commits = [
100
+ CommitInfo(sha="abc1234", message="Initial commit",
101
+ author="Test User", date="1 hour ago"),
102
+ ]
103
+
104
+ After (1 line):
105
+ recent_commits = [CommitInfo.test_commit()]
106
+ """
107
+ return CommitInfo(sha=sha, message=message, author=author, date=date)
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class GitStatus:
112
+ """Git repository status information."""
113
+
114
+ branch: str | None
115
+ clean: bool
116
+ ahead: int
117
+ behind: int
118
+ staged_files: list[str]
119
+ modified_files: list[str]
120
+ untracked_files: list[str]
121
+ recent_commits: list[CommitInfo]
122
+
123
+ @staticmethod
124
+ def clean_status(branch: str, ahead: int = 0, behind: int = 0) -> GitStatus:
125
+ """Create clean status (no changes) for tests.
126
+
127
+ Args:
128
+ branch: Branch name
129
+ ahead: Commits ahead of remote (default: 0)
130
+ behind: Commits behind remote (default: 0)
131
+
132
+ Returns:
133
+ GitStatus with clean=True and empty file lists
134
+
135
+ Example:
136
+ Before (8 lines):
137
+ status = GitStatus(
138
+ branch="test",
139
+ clean=True,
140
+ ahead=0,
141
+ behind=0,
142
+ staged_files=[],
143
+ modified_files=[],
144
+ untracked_files=[],
145
+ recent_commits=[],
146
+ )
147
+
148
+ After (1 line):
149
+ status = GitStatus.clean_status("test")
150
+ """
151
+ return GitStatus(
152
+ branch=branch,
153
+ clean=True,
154
+ ahead=ahead,
155
+ behind=behind,
156
+ staged_files=[],
157
+ modified_files=[],
158
+ untracked_files=[],
159
+ recent_commits=[],
160
+ )
161
+
162
+ @staticmethod
163
+ def dirty_status(
164
+ branch: str,
165
+ *,
166
+ modified: list[str] | None = None,
167
+ staged: list[str] | None = None,
168
+ untracked: list[str] | None = None,
169
+ ahead: int = 0,
170
+ behind: int = 0,
171
+ ) -> GitStatus:
172
+ """Create dirty status (with changes) for tests.
173
+
174
+ Args:
175
+ branch: Branch name
176
+ modified: Modified files (default: [])
177
+ staged: Staged files (default: [])
178
+ untracked: Untracked files (default: [])
179
+ ahead: Commits ahead of remote (default: 0)
180
+ behind: Commits behind remote (default: 0)
181
+
182
+ Returns:
183
+ GitStatus with clean=False and specified file lists
184
+
185
+ Example:
186
+ Before (9 lines):
187
+ status = GitStatus(
188
+ branch="feature",
189
+ clean=False,
190
+ ahead=0,
191
+ behind=0,
192
+ staged_files=[],
193
+ modified_files=["file.py"],
194
+ untracked_files=[],
195
+ recent_commits=[],
196
+ )
197
+
198
+ After (1 line):
199
+ status = GitStatus.dirty_status("feature", modified=["file.py"])
200
+ """
201
+ return GitStatus(
202
+ branch=branch,
203
+ clean=False,
204
+ ahead=ahead,
205
+ behind=behind,
206
+ staged_files=staged or [],
207
+ modified_files=modified or [],
208
+ untracked_files=untracked or [],
209
+ recent_commits=[],
210
+ )
211
+
212
+ @staticmethod
213
+ def with_commits(branch: str, commits: list[CommitInfo], clean: bool = True) -> GitStatus:
214
+ """Create status with commit history for tests.
215
+
216
+ Args:
217
+ branch: Branch name
218
+ commits: List of recent commits
219
+ clean: Whether working tree is clean (default: True)
220
+
221
+ Returns:
222
+ GitStatus with commits populated
223
+
224
+ Example:
225
+ Before (9 lines):
226
+ status = GitStatus(
227
+ branch="main",
228
+ clean=True,
229
+ ahead=0,
230
+ behind=0,
231
+ staged_files=[],
232
+ modified_files=[],
233
+ untracked_files=[],
234
+ recent_commits=[commit1, commit2],
235
+ )
236
+
237
+ After (1 line):
238
+ status = GitStatus.with_commits("main", [commit1, commit2])
239
+ """
240
+ return GitStatus(
241
+ branch=branch,
242
+ clean=clean,
243
+ ahead=0,
244
+ behind=0,
245
+ staged_files=[],
246
+ modified_files=[],
247
+ untracked_files=[],
248
+ recent_commits=commits,
249
+ )
250
+
251
+
252
+ @dataclass(frozen=True)
253
+ class StackPosition:
254
+ """Worktree stack position information."""
255
+
256
+ stack: list[str]
257
+ current_branch: str
258
+ parent_branch: str | None
259
+ children_branches: list[str]
260
+ is_trunk: bool
261
+
262
+
263
+ @dataclass(frozen=True)
264
+ class PullRequestStatus:
265
+ """Pull request status information."""
266
+
267
+ number: int
268
+ title: str | None # May not be available from all data sources
269
+ state: str
270
+ is_draft: bool
271
+ url: str
272
+ checks_passing: bool | None
273
+ reviews: list[str] | None # May not be available from all data sources
274
+ ready_to_merge: bool
275
+
276
+
277
+ @dataclass(frozen=True)
278
+ class EnvironmentStatus:
279
+ """Environment variables status."""
280
+
281
+ variables: dict[str, str]
282
+
283
+
284
+ @dataclass(frozen=True)
285
+ class DependencyStatus:
286
+ """Dependency status for various language ecosystems."""
287
+
288
+ language: str
289
+ up_to_date: bool
290
+ outdated_count: int
291
+ details: str | None
292
+
293
+
294
+ @dataclass(frozen=True)
295
+ class PlanStatus:
296
+ """Status of .impl/ folder and enriched plans."""
297
+
298
+ exists: bool
299
+ path: Path | None
300
+ summary: str | None
301
+ line_count: int
302
+ first_lines: list[str]
303
+ format: str # "folder" or "none"
304
+ enriched_plan_path: Path | None = None # Path to enriched plan file
305
+ enriched_plan_filename: str | None = None # Filename of enriched plan
306
+ issue_number: int | None = None # GitHub issue number if linked
307
+ issue_url: str | None = None # GitHub issue URL if linked
308
+
309
+
310
+ @dataclass(frozen=True)
311
+ class StatusData:
312
+ """Container for all status information."""
313
+
314
+ worktree_info: WorktreeDisplayInfo
315
+ git_status: GitStatus | None
316
+ stack_position: StackPosition | None
317
+ pr_status: PullRequestStatus | None
318
+ environment: EnvironmentStatus | None
319
+ dependencies: DependencyStatus | None
320
+ plan: PlanStatus | None
321
+ related_worktrees: list[WorktreeDisplayInfo]
322
+
323
+ @staticmethod
324
+ def minimal(worktree_info: WorktreeDisplayInfo) -> StatusData:
325
+ """Create minimal status data (all optional fields None) for tests.
326
+
327
+ Args:
328
+ worktree_info: Worktree display information
329
+
330
+ Returns:
331
+ StatusData with only worktree_info set, all other fields None/empty
332
+
333
+ Example:
334
+ Before (9 lines):
335
+ status_data = StatusData(
336
+ worktree_info=WorktreeDisplayInfo(
337
+ name="my-feature", path=wt_path, branch="feature", is_root=False
338
+ ),
339
+ git_status=None,
340
+ stack_position=None,
341
+ pr_status=None,
342
+ environment=None,
343
+ dependencies=None,
344
+ plan=None,
345
+ related_worktrees=[],
346
+ )
347
+
348
+ After (2 lines):
349
+ worktree_info = WorktreeDisplayInfo.feature(wt_path, "feature")
350
+ status_data = StatusData.minimal(worktree_info)
351
+ """
352
+ return StatusData(
353
+ worktree_info=worktree_info,
354
+ git_status=None,
355
+ stack_position=None,
356
+ pr_status=None,
357
+ environment=None,
358
+ dependencies=None,
359
+ plan=None,
360
+ related_worktrees=[],
361
+ )
362
+
363
+ @staticmethod
364
+ def with_git_status(worktree_info: WorktreeDisplayInfo, git_status: GitStatus) -> StatusData:
365
+ """Create status data with git status populated.
366
+
367
+ Args:
368
+ worktree_info: Worktree display information
369
+ git_status: Git status information
370
+
371
+ Returns:
372
+ StatusData with worktree_info and git_status set, other fields None/empty
373
+
374
+ Example:
375
+ Before (11 lines):
376
+ status_data = StatusData(
377
+ worktree_info=WorktreeDisplayInfo(
378
+ name="root", path=repo_root, branch="main", is_root=True
379
+ ),
380
+ git_status=GitStatus.clean_status("main"),
381
+ stack_position=None,
382
+ pr_status=None,
383
+ environment=None,
384
+ dependencies=None,
385
+ plan=None,
386
+ related_worktrees=[],
387
+ )
388
+
389
+ After (2 lines):
390
+ worktree_info = WorktreeDisplayInfo.root(repo_root)
391
+ status_data = StatusData.with_git_status(
392
+ worktree_info, GitStatus.clean_status("main")
393
+ )
394
+ """
395
+ return StatusData(
396
+ worktree_info=worktree_info,
397
+ git_status=git_status,
398
+ stack_position=None,
399
+ pr_status=None,
400
+ environment=None,
401
+ dependencies=None,
402
+ plan=None,
403
+ related_worktrees=[],
404
+ )
@@ -0,0 +1,169 @@
1
+ """Orchestrator for collecting and assembling status information."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+ from pathlib import Path
6
+
7
+ from erk.core.context import ErkContext
8
+ from erk.status.collectors.base import StatusCollector
9
+ from erk.status.models.status_data import StatusData, WorktreeDisplayInfo
10
+ from erk_shared.gateway.parallel.abc import ParallelTaskRunner
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class StatusOrchestrator:
16
+ """Coordinates all status collectors and assembles final data.
17
+
18
+ The orchestrator runs collectors in parallel with timeouts to ensure
19
+ responsive output even if some collectors are slow or fail.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ collectors: list[StatusCollector],
25
+ *,
26
+ timeout_seconds: float = 2.0,
27
+ runner: ParallelTaskRunner,
28
+ ) -> None:
29
+ """Create a status orchestrator.
30
+
31
+ Args:
32
+ collectors: List of status collectors to run
33
+ timeout_seconds: Maximum time to wait for each collector (default: 2.0)
34
+ runner: Parallel task runner for executing collectors
35
+ """
36
+ self.collectors = collectors
37
+ self.timeout_seconds = timeout_seconds
38
+ self.runner = runner
39
+
40
+ def collect_status(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> StatusData:
41
+ """Collect all status information in parallel.
42
+
43
+ Each collector runs in its own thread with a timeout. Failed or slow
44
+ collectors will return None for their section.
45
+
46
+ Args:
47
+ ctx: Erk context with operations
48
+ worktree_path: Path to the worktree
49
+ repo_root: Path to repository root
50
+
51
+ Returns:
52
+ StatusData with all collected information
53
+ """
54
+ # Determine worktree info
55
+ worktree_info = self._get_worktree_info(ctx, worktree_path, repo_root)
56
+
57
+ # Build tasks for available collectors
58
+ tasks: dict[str, Callable[[], object]] = {}
59
+ for collector in self.collectors:
60
+ if collector.is_available(ctx, worktree_path):
61
+ # Create closure that captures current values
62
+ def make_task(c=collector):
63
+ return lambda: c.collect(ctx, worktree_path, repo_root)
64
+
65
+ tasks[collector.name] = make_task()
66
+
67
+ # Run collectors in parallel via runner
68
+ results = self.runner.run_parallel(tasks, self.timeout_seconds)
69
+
70
+ # Get related worktrees
71
+ related_worktrees = self._get_related_worktrees(ctx, repo_root, worktree_path)
72
+
73
+ # Assemble StatusData - cast results to expected types
74
+ # Results are either the correct type or None (from collector failures)
75
+ from erk.status.models.status_data import (
76
+ DependencyStatus,
77
+ EnvironmentStatus,
78
+ GitStatus,
79
+ PlanStatus,
80
+ PullRequestStatus,
81
+ StackPosition,
82
+ )
83
+
84
+ git_result = results.get("git")
85
+ stack_result = results.get("stack")
86
+ pr_result = results.get("pr")
87
+ env_result = results.get("environment")
88
+ deps_result = results.get("dependencies")
89
+ plan_result = results.get("plan")
90
+
91
+ return StatusData(
92
+ worktree_info=worktree_info,
93
+ git_status=git_result if isinstance(git_result, GitStatus) else None,
94
+ stack_position=stack_result if isinstance(stack_result, StackPosition) else None,
95
+ pr_status=pr_result if isinstance(pr_result, PullRequestStatus) else None,
96
+ environment=env_result if isinstance(env_result, EnvironmentStatus) else None,
97
+ dependencies=deps_result if isinstance(deps_result, DependencyStatus) else None,
98
+ plan=plan_result if isinstance(plan_result, PlanStatus) else None,
99
+ related_worktrees=related_worktrees,
100
+ )
101
+
102
+ def _get_worktree_info(
103
+ self, ctx: ErkContext, worktree_path: Path, repo_root: Path
104
+ ) -> WorktreeDisplayInfo:
105
+ """Get basic worktree information.
106
+
107
+ Args:
108
+ ctx: Erk context
109
+ worktree_path: Path to worktree
110
+ repo_root: Path to repository root
111
+
112
+ Returns:
113
+ WorktreeDisplayInfo with basic information
114
+ """
115
+ # Check paths exist before resolution to avoid OSError
116
+ is_root = False
117
+ if worktree_path.exists() and repo_root.exists():
118
+ is_root = worktree_path.resolve() == repo_root.resolve()
119
+
120
+ name = "root" if is_root else worktree_path.name
121
+ branch = ctx.git.get_current_branch(worktree_path)
122
+
123
+ return WorktreeDisplayInfo(name=name, path=worktree_path, branch=branch, is_root=is_root)
124
+
125
+ def _get_related_worktrees(
126
+ self, ctx: ErkContext, repo_root: Path, current_path: Path
127
+ ) -> list[WorktreeDisplayInfo]:
128
+ """Get list of other worktrees in the repository.
129
+
130
+ Args:
131
+ ctx: Erk context
132
+ repo_root: Path to repository root
133
+ current_path: Path to current worktree (excluded from results)
134
+
135
+ Returns:
136
+ List of WorktreeDisplayInfo for other worktrees
137
+ """
138
+ worktrees = ctx.git.list_worktrees(repo_root)
139
+
140
+ # Check paths exist before resolution to avoid OSError
141
+ if not current_path.exists():
142
+ return []
143
+
144
+ current_resolved = current_path.resolve()
145
+
146
+ related = []
147
+ for wt in worktrees:
148
+ # Skip if worktree path doesn't exist
149
+ if not wt.path.exists():
150
+ continue
151
+
152
+ wt_resolved = wt.path.resolve()
153
+
154
+ # Skip current worktree
155
+ if wt_resolved == current_resolved:
156
+ continue
157
+
158
+ # Determine if this is the root worktree
159
+ is_root = False
160
+ if repo_root.exists():
161
+ is_root = wt_resolved == repo_root.resolve()
162
+
163
+ name = "root" if is_root else wt.path.name
164
+
165
+ related.append(
166
+ WorktreeDisplayInfo(name=name, path=wt.path, branch=wt.branch, is_root=is_root)
167
+ )
168
+
169
+ return related
@@ -0,0 +1,5 @@
1
+ """Status renderers.
2
+
3
+ Import from submodules:
4
+ - simple: SimpleRenderer
5
+ """