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
erk/cli/output.py ADDED
@@ -0,0 +1,361 @@
1
+ """Output utilities for CLI commands with clear intent.
2
+
3
+ For user_output, machine_output, format_duration - import from erk_shared.output.
4
+ This module provides format_implement_summary and stream_command_with_feedback.
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ import click
13
+ from rich.panel import Panel
14
+ from rich.text import Text
15
+
16
+ from erk.core.claude_executor import (
17
+ ClaudeExecutor,
18
+ CommandResult,
19
+ ErrorEvent,
20
+ IssueNumberEvent,
21
+ NoOutputEvent,
22
+ NoTurnsEvent,
23
+ PrNumberEvent,
24
+ ProcessErrorEvent,
25
+ PrTitleEvent,
26
+ PrUrlEvent,
27
+ SpinnerUpdateEvent,
28
+ TextEvent,
29
+ ToolEvent,
30
+ )
31
+ from erk_shared.output.output import format_duration
32
+
33
+
34
+ def format_implement_summary(results: list[CommandResult], total_duration: float) -> Panel:
35
+ """Format final summary box with status, PR link, timing, errors.
36
+
37
+ Args:
38
+ results: List of CommandResult from executed commands
39
+ total_duration: Total execution time in seconds
40
+
41
+ Returns:
42
+ Rich Panel with formatted summary
43
+
44
+ Example:
45
+ >>> results = [CommandResult(success=True, pr_url="https://...", ...)]
46
+ >>> panel = format_implement_summary(results, 123.45)
47
+ >>> console.print(panel)
48
+ """
49
+ # Determine overall success
50
+ overall_success = all(r.success for r in results)
51
+
52
+ # Build summary lines
53
+ lines: list[Text] = []
54
+
55
+ # Status line
56
+ if overall_success:
57
+ lines.append(Text("✅ Status: Success", style="green"))
58
+ else:
59
+ lines.append(Text("❌ Status: Failed", style="red"))
60
+
61
+ # Duration
62
+ duration_str = format_duration(total_duration)
63
+ lines.append(Text(f"⏱ Duration: {duration_str}"))
64
+
65
+ # PR and issue metadata (if any)
66
+ pr_url: str | None = None
67
+ pr_number: int | None = None
68
+ pr_title: str | None = None
69
+ issue_number: int | None = None
70
+ for result in results:
71
+ if result.pr_url:
72
+ pr_url = result.pr_url
73
+ pr_number = result.pr_number
74
+ pr_title = result.pr_title
75
+ issue_number = result.issue_number
76
+ break
77
+
78
+ if pr_url:
79
+ # Add blank line for spacing
80
+ lines.append(Text(""))
81
+
82
+ # Show PR number with URL
83
+ if pr_number:
84
+ lines.append(Text(f"🔗 PR: #{pr_number}", style="blue bold"))
85
+ else:
86
+ lines.append(Text("🔗 PR: Created", style="blue bold"))
87
+
88
+ # Show PR title
89
+ if pr_title:
90
+ lines.append(Text(f" {pr_title}", style="cyan"))
91
+
92
+ # Show PR URL
93
+ lines.append(Text(f" {pr_url}", style="dim"))
94
+
95
+ # Show linked issue (if any)
96
+ if issue_number:
97
+ lines.append(Text(""))
98
+ lines.append(
99
+ Text(f"📋 Linked Issue: #{issue_number} (will auto-close on merge)", style="yellow")
100
+ )
101
+
102
+ # Error details (if failed)
103
+ if not overall_success:
104
+ for i, result in enumerate(results):
105
+ if not result.success:
106
+ if result.error_message:
107
+ lines.append(Text("")) # Blank line
108
+ lines.append(Text(f"Error in command {i + 1}:", style="red bold"))
109
+ lines.append(Text(result.error_message, style="red"))
110
+
111
+ # Combine lines
112
+ content = Text("\n").join(lines)
113
+
114
+ # Create panel
115
+ title = "Implementation Complete" if overall_success else "Implementation Failed"
116
+ return Panel(
117
+ content, title=title, border_style="green" if overall_success else "red", padding=(1, 2)
118
+ )
119
+
120
+
121
+ def stream_command_with_feedback(
122
+ executor: ClaudeExecutor,
123
+ command: str,
124
+ worktree_path: Path,
125
+ dangerous: bool,
126
+ model: str | None = None,
127
+ debug: bool = False,
128
+ ) -> CommandResult:
129
+ """Stream Claude command execution with live print-based feedback.
130
+
131
+ This function replaces spinner-based output with print-based feedback
132
+ that works correctly (Rich's console.status() suppresses console.print()).
133
+
134
+ Visual output format:
135
+ - Start: `--- /command ---` (bold)
136
+ - Text events: content as-is (normal)
137
+ - Tool events: ` > tool summary` (dim)
138
+ - Spinner updates: ` ... status` (dim, deduplicated)
139
+ - Error events: ` ! error message` (red)
140
+ - End (success): `--- Done (1m 23s) ---` (green)
141
+ - End (failure): `--- Failed (1m 23s) ---` (red)
142
+
143
+ Args:
144
+ executor: Claude CLI executor for command execution
145
+ command: The slash command to execute (e.g., "/gt:pr-submit")
146
+ worktree_path: Path to worktree directory to run command in
147
+ dangerous: Whether to skip permission prompts
148
+ model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
149
+ debug: Whether to show debug output for stream parsing
150
+
151
+ Returns:
152
+ CommandResult with success status, PR URL, duration, and messages
153
+ """
154
+ # Flush stderr to ensure previous user_output() messages are visible
155
+ # before stdout starts printing. This prevents buffering issues where
156
+ # stderr output appears after stdout in mixed output scenarios.
157
+ sys.stderr.flush()
158
+
159
+ # Print start marker (stderr so shell integration can capture stdout)
160
+ click.echo(click.style(f"--- {command} ---", bold=True), err=True)
161
+
162
+ start_time = time.time()
163
+ filtered_messages: list[str] = []
164
+ pr_url: str | None = None
165
+ pr_number: int | None = None
166
+ pr_title: str | None = None
167
+ issue_number: int | None = None
168
+ error_message: str | None = None
169
+ success = True
170
+ last_spinner_update: str | None = None
171
+ event_count = 0
172
+
173
+ # Stream events in real-time
174
+ event_stream = executor.execute_command_streaming(
175
+ command, worktree_path, dangerous, verbose=False, debug=debug, model=model
176
+ )
177
+ if debug:
178
+ click.echo(click.style("[DEBUG] Starting event stream...", fg="yellow"), err=True)
179
+ for event in event_stream:
180
+ event_count += 1
181
+ if debug:
182
+ click.echo(
183
+ click.style(f"[DEBUG] Event #{event_count}: {type(event).__name__}", fg="yellow"),
184
+ err=True,
185
+ )
186
+ match event:
187
+ case TextEvent(content=content):
188
+ click.echo(content, err=True)
189
+ filtered_messages.append(content)
190
+ case ToolEvent(summary=summary):
191
+ click.echo(click.style(f" > {summary}", dim=True), err=True)
192
+ filtered_messages.append(summary)
193
+ case SpinnerUpdateEvent(status=status):
194
+ # Deduplicate spinner updates - only print when status changes
195
+ if status != last_spinner_update:
196
+ click.echo(click.style(f" ... {status}", dim=True), err=True)
197
+ last_spinner_update = status
198
+ case PrUrlEvent(url=url):
199
+ pr_url = url
200
+ case PrNumberEvent(number=num):
201
+ pr_number = num # Already int, no conversion needed
202
+ case PrTitleEvent(title=title):
203
+ pr_title = title
204
+ case IssueNumberEvent(number=num):
205
+ issue_number = num # Already int, no conversion needed
206
+ case ErrorEvent(message=msg):
207
+ click.echo(click.style(f" ! {msg}", fg="red"), err=True)
208
+ error_message = msg
209
+ success = False
210
+ case NoOutputEvent(diagnostic=diag):
211
+ click.echo(click.style(f" ⚠️ {diag}", fg="yellow"), err=True)
212
+ error_message = diag
213
+ success = False
214
+ case NoTurnsEvent(diagnostic=diag):
215
+ click.echo(click.style(f" ⚠️ {diag}", fg="yellow"), err=True)
216
+ error_message = diag
217
+ success = False
218
+ case ProcessErrorEvent(message=msg):
219
+ click.echo(click.style(f" ❌ {msg}", fg="red"), err=True)
220
+ error_message = msg
221
+ success = False
222
+
223
+ if debug:
224
+ msg = f"[DEBUG] Event stream complete. Total events: {event_count}"
225
+ click.echo(click.style(msg, fg="yellow"), err=True)
226
+
227
+ duration = time.time() - start_time
228
+ duration_str = format_duration(duration)
229
+
230
+ # Print end marker (stderr so shell integration can capture stdout)
231
+ if success:
232
+ click.echo(click.style(f"--- Done ({duration_str}) ---", fg="green", bold=True), err=True)
233
+ else:
234
+ click.echo(click.style(f"--- Failed ({duration_str}) ---", fg="red", bold=True), err=True)
235
+
236
+ return CommandResult(
237
+ success=success,
238
+ pr_url=pr_url,
239
+ pr_number=pr_number,
240
+ pr_title=pr_title,
241
+ issue_number=issue_number,
242
+ duration_seconds=duration,
243
+ error_message=error_message,
244
+ filtered_messages=filtered_messages,
245
+ )
246
+
247
+
248
+ @dataclass(frozen=True)
249
+ class FixConflictsResult:
250
+ """Result from fix-conflicts streaming execution."""
251
+
252
+ success: bool
253
+ error_message: str | None = None
254
+ requires_interactive: bool = False
255
+
256
+
257
+ def stream_fix_conflicts(
258
+ executor: ClaudeExecutor,
259
+ worktree_path: Path,
260
+ ) -> FixConflictsResult:
261
+ """Stream fix-conflicts command via Claude executor with live feedback.
262
+
263
+ Handles the /erk:fix-conflicts command execution with:
264
+ - Live output streaming with visual feedback
265
+ - Semantic conflict detection (AskUserQuestion)
266
+ - Deduped spinner updates
267
+ - Rich console output with start/end markers
268
+
269
+ Args:
270
+ executor: Claude CLI executor
271
+ worktree_path: Path to run the conflict resolution in
272
+
273
+ Returns:
274
+ FixConflictsResult with success status and error details
275
+ """
276
+ error_message: str | None = None
277
+ success = True
278
+ has_work_events = False
279
+ last_spinner: str | None = None
280
+ start_time = time.time()
281
+
282
+ # Print start marker with bold styling
283
+ click.echo(click.style("--- /erk:fix-conflicts ---", bold=True))
284
+ click.echo("")
285
+
286
+ for event in executor.execute_command_streaming(
287
+ command="/erk:fix-conflicts",
288
+ worktree_path=worktree_path,
289
+ dangerous=True, # Conflict resolution modifies git state
290
+ ):
291
+ match event:
292
+ case TextEvent(content=content):
293
+ has_work_events = True
294
+ click.echo(content)
295
+ case ToolEvent(summary=summary):
296
+ has_work_events = True
297
+ # Check for user input prompts (semantic conflict requiring decision)
298
+ if "AskUserQuestion" in summary:
299
+ click.echo("")
300
+ click.echo(
301
+ click.style(
302
+ "⚠️ Semantic conflict detected - requires interactive resolution",
303
+ fg="yellow",
304
+ bold=True,
305
+ )
306
+ )
307
+ click.echo("")
308
+ click.echo("Claude needs your input to resolve this conflict.")
309
+ click.echo("Run conflict resolution interactively:")
310
+ click.echo("")
311
+ click.echo(click.style(" claude /erk:fix-conflicts", fg="cyan"))
312
+ click.echo("")
313
+ return FixConflictsResult(
314
+ success=False,
315
+ requires_interactive=True,
316
+ )
317
+ # Tool summaries with icon
318
+ click.echo(click.style(f" ⚙️ {summary}", fg="cyan", dim=True))
319
+ case SpinnerUpdateEvent(status=status):
320
+ if status != last_spinner:
321
+ click.echo(click.style(f" ⏳ {status}", dim=True))
322
+ last_spinner = status
323
+ case ErrorEvent(message=msg):
324
+ click.echo(click.style(f" ❌ {msg}", fg="red"))
325
+ error_message = msg
326
+ success = False
327
+ case NoOutputEvent(diagnostic=diag):
328
+ click.echo(click.style(f" ⚠️ {diag}", fg="yellow"))
329
+ error_message = diag
330
+ success = False
331
+ case NoTurnsEvent(diagnostic=diag):
332
+ click.echo(click.style(f" ⚠️ {diag}", fg="yellow"))
333
+ error_message = diag
334
+ success = False
335
+ case ProcessErrorEvent(message=msg):
336
+ click.echo(click.style(f" ❌ {msg}", fg="red"))
337
+ error_message = msg
338
+ success = False
339
+ case PrUrlEvent() | PrNumberEvent() | PrTitleEvent() | IssueNumberEvent():
340
+ pass # PR metadata not relevant for fix-conflicts
341
+
342
+ # Check for no-work-events failure mode
343
+ if success and not has_work_events:
344
+ success = False
345
+ error_message = (
346
+ "Claude completed without producing any output - "
347
+ "check hooks or run 'claude /erk:fix-conflicts' directly to debug"
348
+ )
349
+ click.echo(click.style(f" ⚠️ {error_message}", fg="yellow"))
350
+
351
+ # Calculate duration and print end marker
352
+ duration = time.time() - start_time
353
+ duration_str = format_duration(duration)
354
+
355
+ click.echo("")
356
+ if success:
357
+ click.echo(click.style(f"--- Done ({duration_str}) ---", fg="green", bold=True))
358
+ else:
359
+ click.echo(click.style(f"--- Failed ({duration_str}) ---", fg="red", bold=True))
360
+
361
+ return FixConflictsResult(success=success, error_message=error_message)
@@ -0,0 +1,12 @@
1
+ # work config for this repository
2
+ # Available template variables: {worktree_path}, {repo_root}, {name}
3
+
4
+ [env]
5
+ DAGSTER_GIT_REPO_DIR = "{worktree_path}"
6
+
7
+ [post_create]
8
+ shell = "bash"
9
+ commands = [
10
+ "uv venv",
11
+ "uv run make dev_install",
12
+ ]
@@ -0,0 +1,12 @@
1
+ # work config for this repository
2
+ # Available template variables: {worktree_path}, {repo_root}, {name}
3
+
4
+ [env]
5
+ # EXAMPLE_KEY = "{worktree_path}"
6
+
7
+ [post_create]
8
+ # shell = "bash"
9
+ # commands = [
10
+ # "uv venv",
11
+ # "uv run make dev_install",
12
+ # ]
@@ -0,0 +1,68 @@
1
+ # Prompt Hooks
2
+
3
+ Prompt hooks are markdown files that provide instructions to AI agents at specific points in your erk workflows.
4
+
5
+ ## What Are Prompt Hooks?
6
+
7
+ Unlike Claude Code hooks (which execute shell commands at lifecycle events), prompt hooks are **AI-readable documentation** that:
8
+
9
+ - Execute at specific workflow points (e.g., after plan implementation)
10
+ - Provide project-specific instructions to AI agents
11
+ - Customize AI behavior without code changes
12
+ - Are version-controlled with your project
13
+
14
+ ## Available Hooks
15
+
16
+ | Hook File | Fires When | Purpose |
17
+ | --------------------------- | ---------------------------------------------------- | ----------------------------------------- |
18
+ | `post-plan-implement-ci.md` | After `/erk:plan-implement` completes implementation | Define CI workflow and iteration strategy |
19
+
20
+ ## Creating a Prompt Hook
21
+
22
+ 1. Create a markdown file in `.erk/prompt-hooks/` with the appropriate name
23
+ 2. Write instructions for the AI agent in imperative mood
24
+ 3. Include specific commands, tools to load, and success criteria
25
+ 4. Reference skills to load (e.g., `Load the \`ci-iteration\` skill`)
26
+
27
+ ### Example: post-plan-implement-ci.md
28
+
29
+ \`\`\`markdown
30
+
31
+ # Post-Implementation CI
32
+
33
+ Run CI validation after plan implementation using \`make fast-ci\`.
34
+
35
+ Load the \`ci-iteration\` skill for the iterative fix workflow.
36
+
37
+ ## Iteration Process
38
+
39
+ 1. Run \`make fast-ci\` via devrun agent
40
+ 2. If checks fail: apply targeted fixes
41
+ 3. Re-run CI (max 5 iterations)
42
+ 4. On success: proceed to PR creation
43
+ \`\`\`
44
+
45
+ ## Best Practices
46
+
47
+ - **Be specific:** Provide exact commands and tool names
48
+ - **Define success:** Clear exit criteria prevent infinite loops
49
+ - **Reference skills:** Load relevant skills for specialized workflows
50
+ - **Version control:** Commit hooks alongside code
51
+ - **Document context:** Explain WHY the workflow exists
52
+
53
+ ## Customization
54
+
55
+ These are **erk-defined hooks that you can customize** for your project's needs:
56
+
57
+ - Modify existing hooks to match your CI/CD setup
58
+ - Add project-specific validation steps
59
+ - Reference your custom skills and commands
60
+ - Adjust iteration limits and error handling
61
+
62
+ ## Future Hooks
63
+
64
+ Potential hooks for future workflows (create these as needed):
65
+
66
+ - `pre-plan-implement.md` - Setup before implementation starts
67
+ - `post-pr-create.md` - After PR creation workflow
68
+ - `pre-worktree-create.md` - Validation before worktree setup
@@ -0,0 +1,32 @@
1
+ """Output utilities for exec scripts.
2
+
3
+ Exec scripts communicate results via JSON to stdout. This module provides
4
+ consistent error handling that outputs structured JSON rather than raising
5
+ exceptions, supporting shell scripting patterns.
6
+ """
7
+
8
+ import json
9
+ from typing import NoReturn
10
+
11
+ import click
12
+
13
+
14
+ def exit_with_error(error_type: str, message: str) -> NoReturn:
15
+ """Output JSON error and exit with code 0.
16
+
17
+ Exec commands exit with 0 even on error to support || true patterns
18
+ in shell scripts. The error is communicated via JSON output.
19
+
20
+ Args:
21
+ error_type: Machine-readable error category (e.g., "no_pr_for_branch")
22
+ message: Human-readable error message
23
+
24
+ Raises:
25
+ SystemExit: Always exits with code 0 after printing JSON
26
+ """
27
+ error_json = json.dumps(
28
+ {"success": False, "error_type": error_type, "message": message},
29
+ indent=2,
30
+ )
31
+ click.echo(error_json)
32
+ raise SystemExit(0)
@@ -0,0 +1,32 @@
1
+ # Erk shell integration for bash
2
+ # This function wraps the erk CLI to provide seamless worktree switching
3
+
4
+ erk() {
5
+ # Don't intercept if we're doing shell completion
6
+ [ -n "$_ERK_COMPLETE" ] && { command erk "$@"; return; }
7
+
8
+ local script_path exit_status
9
+ script_path=$(ERK_SHELL=bash command erk __shell "$@")
10
+ exit_status=$?
11
+
12
+ # Passthrough mode: run the original command directly
13
+ [ "$script_path" = "__ERK_PASSTHROUGH__" ] && { command erk "$@"; return; }
14
+
15
+ # Source the script file if it exists, regardless of exit code.
16
+ # This matches Python handler logic: use script even if command had errors.
17
+ # The script contains important state changes (like cd to target dir).
18
+ if [ -n "$script_path" ] && [ -f "$script_path" ]; then
19
+ source "$script_path"
20
+ local source_exit=$?
21
+
22
+ # Clean up unless ERK_KEEP_SCRIPTS is set
23
+ if [ -z "$ERK_KEEP_SCRIPTS" ]; then
24
+ rm -f "$script_path"
25
+ fi
26
+
27
+ return $source_exit
28
+ fi
29
+
30
+ # Only return exit_status if no script was provided
31
+ [ $exit_status -ne 0 ] && return $exit_status
32
+ }
@@ -0,0 +1,39 @@
1
+ # Erk shell integration for fish
2
+ # This function wraps the erk CLI to provide seamless worktree switching
3
+
4
+ function erk
5
+ # Don't intercept if we're doing shell completion
6
+ if set -q _ERK_COMPLETE
7
+ command erk $argv
8
+ return
9
+ end
10
+
11
+ set -l script_path (env ERK_SHELL=fish command erk __shell $argv)
12
+ set -l exit_status $status
13
+
14
+ # Passthrough mode
15
+ if test "$script_path" = "__ERK_PASSTHROUGH__"
16
+ command erk $argv
17
+ return
18
+ end
19
+
20
+ # Source the script file if it exists, regardless of exit code.
21
+ # This matches Python handler logic: use script even if command had errors.
22
+ # The script contains important state changes (like cd to target dir).
23
+ if test -n "$script_path" -a -f "$script_path"
24
+ source "$script_path"
25
+ set -l source_exit $status
26
+
27
+ # Clean up unless ERK_KEEP_SCRIPTS is set
28
+ if not set -q ERK_KEEP_SCRIPTS
29
+ rm -f "$script_path"
30
+ end
31
+
32
+ return $source_exit
33
+ end
34
+
35
+ # Only return exit_status if no script was provided
36
+ if test $exit_status -ne 0
37
+ return $exit_status
38
+ end
39
+ end