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,76 @@
1
+ """Git status collector."""
2
+
3
+ from pathlib import Path
4
+
5
+ from erk.core.context import ErkContext
6
+ from erk.status.collectors.base import StatusCollector
7
+ from erk.status.models.status_data import CommitInfo, GitStatus
8
+
9
+
10
+ class GitStatusCollector(StatusCollector):
11
+ """Collects git repository status information."""
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ """Name identifier for this collector."""
16
+ return "git"
17
+
18
+ def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
19
+ """Check if git operations are available.
20
+
21
+ Args:
22
+ ctx: Erk context
23
+ worktree_path: Path to worktree
24
+
25
+ Returns:
26
+ True if worktree exists and has git
27
+ """
28
+ if not worktree_path.exists():
29
+ return False
30
+
31
+ return True
32
+
33
+ def collect(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> GitStatus | None:
34
+ """Collect git status information.
35
+
36
+ Args:
37
+ ctx: Erk context
38
+ worktree_path: Path to worktree
39
+ repo_root: Repository root path
40
+
41
+ Returns:
42
+ GitStatus with repository information or None if collection fails
43
+ """
44
+ branch = ctx.git.get_current_branch(worktree_path)
45
+ if branch is None:
46
+ return None
47
+
48
+ # Get git status
49
+ staged, modified, untracked = ctx.git.get_file_status(worktree_path)
50
+ clean = len(staged) == 0 and len(modified) == 0 and len(untracked) == 0
51
+
52
+ # Get ahead/behind counts
53
+ ahead, behind = ctx.git.get_ahead_behind(worktree_path, branch)
54
+
55
+ # Get recent commits
56
+ commit_dicts = ctx.git.get_recent_commits(worktree_path, limit=5)
57
+ recent_commits = [
58
+ CommitInfo(
59
+ sha=c["sha"],
60
+ message=c["message"],
61
+ author=c["author"],
62
+ date=c["date"],
63
+ )
64
+ for c in commit_dicts
65
+ ]
66
+
67
+ return GitStatus(
68
+ branch=branch,
69
+ clean=clean,
70
+ ahead=ahead,
71
+ behind=behind,
72
+ staged_files=staged,
73
+ modified_files=modified,
74
+ untracked_files=untracked,
75
+ recent_commits=recent_commits,
76
+ )
@@ -0,0 +1,81 @@
1
+ """GitHub PR collector."""
2
+
3
+ from pathlib import Path
4
+
5
+ from erk.core.context import ErkContext
6
+ from erk.status.collectors.base import StatusCollector
7
+ from erk.status.models.status_data import PullRequestStatus
8
+
9
+
10
+ class GitHubPRCollector(StatusCollector):
11
+ """Collects GitHub pull request information."""
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ """Name identifier for this collector."""
16
+ return "pr"
17
+
18
+ def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
19
+ """Check if PR information should be fetched.
20
+
21
+ Args:
22
+ ctx: Erk context
23
+ worktree_path: Path to worktree
24
+
25
+ Returns:
26
+ True if PR info is enabled in config
27
+ """
28
+ if not (ctx.global_config and ctx.global_config.show_pr_info):
29
+ return False
30
+
31
+ if not worktree_path.exists():
32
+ return False
33
+
34
+ return True
35
+
36
+ def collect(
37
+ self, ctx: ErkContext, worktree_path: Path, repo_root: Path
38
+ ) -> PullRequestStatus | None:
39
+ """Collect GitHub PR information.
40
+
41
+ Args:
42
+ ctx: Erk context
43
+ worktree_path: Path to worktree
44
+ repo_root: Repository root path
45
+
46
+ Returns:
47
+ PullRequestStatus with PR information or None if collection fails
48
+ """
49
+ branch = ctx.git.get_current_branch(worktree_path)
50
+ if branch is None:
51
+ return None
52
+
53
+ # Always use Graphite (fast, no pagination issues)
54
+ prs = ctx.graphite.get_prs_from_graphite(ctx.git, repo_root)
55
+
56
+ # Fail fast if Graphite cache unavailable - no fallback to GitHub
57
+ if not prs:
58
+ return None
59
+
60
+ # Find PR for current branch
61
+ pr = prs.get(branch)
62
+ if pr is None:
63
+ return None
64
+
65
+ # Determine if ready to merge
66
+ ready_to_merge = (
67
+ pr.state == "OPEN"
68
+ and not pr.is_draft
69
+ and (pr.checks_passing is True or pr.checks_passing is None)
70
+ )
71
+
72
+ return PullRequestStatus(
73
+ number=pr.number,
74
+ title=None, # Title not available in PullRequestInfo
75
+ state=pr.state,
76
+ is_draft=pr.is_draft,
77
+ url=pr.url,
78
+ checks_passing=pr.checks_passing,
79
+ reviews=None, # Reviews not available in PullRequestInfo
80
+ ready_to_merge=ready_to_merge,
81
+ )
@@ -0,0 +1,80 @@
1
+ """Worktree stack collector."""
2
+
3
+ from pathlib import Path
4
+
5
+ from erk.core.context import ErkContext
6
+ from erk.status.collectors.base import StatusCollector
7
+ from erk.status.models.status_data import StackPosition
8
+
9
+
10
+ class GraphiteStackCollector(StatusCollector):
11
+ """Collects worktree stack position information."""
12
+
13
+ @property
14
+ def name(self) -> str:
15
+ """Name identifier for this collector."""
16
+ return "stack"
17
+
18
+ def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
19
+ """Check if Graphite is enabled and available.
20
+
21
+ Args:
22
+ ctx: Erk context
23
+ worktree_path: Path to worktree
24
+
25
+ Returns:
26
+ True if Graphite is enabled
27
+ """
28
+ if not (ctx.global_config and ctx.global_config.use_graphite):
29
+ return False
30
+
31
+ if not worktree_path.exists():
32
+ return False
33
+
34
+ return True
35
+
36
+ def collect(
37
+ self, ctx: ErkContext, worktree_path: Path, repo_root: Path
38
+ ) -> StackPosition | None:
39
+ """Collect worktree stack information.
40
+
41
+ Args:
42
+ ctx: Erk context
43
+ worktree_path: Path to worktree
44
+ repo_root: Repository root path
45
+
46
+ Returns:
47
+ StackPosition with stack information or None if collection fails
48
+ """
49
+ branch = ctx.git.get_current_branch(worktree_path)
50
+ if branch is None:
51
+ return None
52
+
53
+ # Get the stack for current branch
54
+ stack = ctx.graphite.get_branch_stack(ctx.git, repo_root, branch)
55
+ if stack is None:
56
+ return None
57
+
58
+ # Find current branch position in stack
59
+ if branch not in stack:
60
+ return None
61
+
62
+ current_idx = stack.index(branch)
63
+
64
+ # Determine parent and children
65
+ parent_branch = stack[current_idx - 1] if current_idx > 0 else None
66
+
67
+ children_branches = []
68
+ if current_idx < len(stack) - 1:
69
+ children_branches.append(stack[current_idx + 1])
70
+
71
+ # Check if this is trunk
72
+ is_trunk = current_idx == 0
73
+
74
+ return StackPosition(
75
+ stack=stack,
76
+ current_branch=branch,
77
+ parent_branch=parent_branch,
78
+ children_branches=children_branches,
79
+ is_trunk=is_trunk,
80
+ )
@@ -0,0 +1,145 @@
1
+ """Implementation folder collector."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import frontmatter
7
+
8
+ from erk.core.context import ErkContext
9
+ from erk.status.collectors.base import StatusCollector
10
+ from erk.status.models.status_data import PlanStatus
11
+ from erk_shared.impl_folder import (
12
+ get_impl_path,
13
+ read_issue_reference,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def detect_enriched_plan(repo_root: Path) -> tuple[Path | None, str | None]:
20
+ """Detect enriched plan file at repository root.
21
+
22
+ Scans for *-plan.md files and checks for erk_plan marker.
23
+
24
+ Args:
25
+ repo_root: Repository root path
26
+
27
+ Returns:
28
+ Tuple of (path, filename) or (None, None) if not found
29
+ """
30
+ if not repo_root.exists():
31
+ return None, None
32
+
33
+ # Find all *-plan.md files
34
+ plan_files = list(repo_root.glob("*-plan.md"))
35
+
36
+ if not plan_files:
37
+ return None, None
38
+
39
+ # Sort by modification time (most recent first)
40
+ plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
41
+
42
+ # Check each file for enrichment marker
43
+ for plan_file in plan_files:
44
+ # Use frontmatter library to parse YAML frontmatter
45
+ post = frontmatter.load(str(plan_file))
46
+
47
+ # Check for enrichment marker (handles missing frontmatter gracefully)
48
+ if post.get("erk_plan") is True:
49
+ return plan_file, plan_file.name
50
+
51
+ return None, None
52
+
53
+
54
+ class PlanFileCollector(StatusCollector):
55
+ """Collects information about .impl/ folder."""
56
+
57
+ @property
58
+ def name(self) -> str:
59
+ """Name identifier for this collector."""
60
+ return "plan"
61
+
62
+ def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
63
+ """Check if .impl/plan.md exists.
64
+
65
+ Args:
66
+ ctx: Erk context
67
+ worktree_path: Path to worktree
68
+
69
+ Returns:
70
+ True if .impl/plan.md exists
71
+ """
72
+ impl_path = get_impl_path(worktree_path, git_ops=ctx.git)
73
+ return impl_path is not None
74
+
75
+ def collect(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> PlanStatus | None:
76
+ """Collect implementation folder information.
77
+
78
+ Args:
79
+ ctx: Erk context
80
+ worktree_path: Path to worktree
81
+ repo_root: Repository root path
82
+
83
+ Returns:
84
+ PlanStatus with folder information or None if collection fails
85
+ """
86
+ impl_path = get_impl_path(worktree_path, git_ops=ctx.git)
87
+
88
+ # Detect enriched plan at repo root
89
+ enriched_plan_path, enriched_plan_filename = detect_enriched_plan(repo_root)
90
+
91
+ if impl_path is None:
92
+ return PlanStatus(
93
+ exists=False,
94
+ path=None,
95
+ summary=None,
96
+ line_count=0,
97
+ first_lines=[],
98
+ format="none",
99
+ enriched_plan_path=enriched_plan_path,
100
+ enriched_plan_filename=enriched_plan_filename,
101
+ )
102
+
103
+ # Read plan.md
104
+ content = impl_path.read_text(encoding="utf-8")
105
+ lines = content.splitlines()
106
+ line_count = len(lines)
107
+
108
+ # Get first 5 lines
109
+ first_lines = lines[:5] if len(lines) >= 5 else lines
110
+
111
+ # Extract summary from first few non-empty lines
112
+ summary_lines = []
113
+ for line in lines[:10]: # Look at first 10 lines
114
+ stripped = line.strip()
115
+ if stripped and not stripped.startswith("#"):
116
+ summary_lines.append(stripped)
117
+ if len(summary_lines) >= 2:
118
+ break
119
+
120
+ summary = " ".join(summary_lines) if summary_lines else None
121
+
122
+ # Truncate summary if too long
123
+ if summary and len(summary) > 100:
124
+ summary = summary[:97] + "..."
125
+
126
+ # Return folder path, not plan.md file path
127
+ impl_folder = worktree_path / ".impl"
128
+
129
+ # Read issue reference if present
130
+ issue_ref = read_issue_reference(impl_folder)
131
+ issue_number = issue_ref.issue_number if issue_ref else None
132
+ issue_url = issue_ref.issue_url if issue_ref else None
133
+
134
+ return PlanStatus(
135
+ exists=True,
136
+ path=impl_folder,
137
+ summary=summary,
138
+ line_count=line_count,
139
+ first_lines=first_lines,
140
+ format="folder",
141
+ enriched_plan_path=enriched_plan_path,
142
+ enriched_plan_filename=enriched_plan_filename,
143
+ issue_number=issue_number,
144
+ issue_url=issue_url,
145
+ )
@@ -0,0 +1,4 @@
1
+ """Data models for status command.
2
+
3
+ Import from status_data submodule.
4
+ """