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,375 @@
1
+ """Signal implementation events (started/ended) to GitHub.
2
+
3
+ This exec command wraps the start/end signaling operations:
4
+ - "started": Combines post-start-comment and mark-impl-started
5
+ - "ended": Runs mark-impl-ended
6
+
7
+ Provides a single entry point for /erk:plan-implement to signal events
8
+ with graceful failure (always exits 0 for || true pattern).
9
+
10
+ Usage:
11
+ erk exec impl-signal started
12
+ erk exec impl-signal ended
13
+
14
+ Output:
15
+ JSON with success status or error information
16
+ Always exits with code 0 (graceful degradation for || true pattern)
17
+
18
+ Exit Codes:
19
+ 0: Always (even on error, to support || true pattern)
20
+
21
+ Examples:
22
+ $ erk exec impl-signal started
23
+ {"success": true, "event": "started", "issue_number": 123}
24
+
25
+ $ erk exec impl-signal ended
26
+ {"success": true, "event": "ended", "issue_number": 123}
27
+ """
28
+
29
+ import getpass
30
+ import json
31
+ import os
32
+ import subprocess
33
+ from dataclasses import asdict, dataclass
34
+ from datetime import UTC, datetime
35
+ from pathlib import Path
36
+
37
+ import click
38
+
39
+ from erk_shared.context.helpers import (
40
+ require_claude_installation,
41
+ require_repo_root,
42
+ )
43
+ from erk_shared.context.helpers import (
44
+ require_issues as require_github_issues,
45
+ )
46
+ from erk_shared.env import in_github_actions
47
+ from erk_shared.github.metadata.core import render_erk_issue_event
48
+ from erk_shared.github.metadata.plan_header import (
49
+ update_plan_header_local_impl_event,
50
+ update_plan_header_remote_impl,
51
+ )
52
+ from erk_shared.impl_folder import (
53
+ read_issue_reference,
54
+ write_local_run_state,
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class SignalSuccess:
60
+ """Success response for signal command."""
61
+
62
+ success: bool
63
+ event: str
64
+ issue_number: int
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class SignalError:
69
+ """Error response for signal command."""
70
+
71
+ success: bool
72
+ event: str
73
+ error_type: str
74
+ message: str
75
+
76
+
77
+ def _output_error(event: str, error_type: str, message: str) -> None:
78
+ """Output error JSON and exit gracefully."""
79
+ result = SignalError(
80
+ success=False,
81
+ event=event,
82
+ error_type=error_type,
83
+ message=message,
84
+ )
85
+ click.echo(json.dumps(asdict(result), indent=2))
86
+ raise SystemExit(0)
87
+
88
+
89
+ def _delete_claude_plan_file(ctx: click.Context, session_id: str, cwd: Path) -> bool:
90
+ """Delete the Claude plan file for the given session.
91
+
92
+ This is called when implementation starts to clean up the plan file.
93
+ The plan content has already been saved to GitHub and snapshotted.
94
+
95
+ Args:
96
+ ctx: Click context for dependency injection.
97
+ session_id: The session ID to look up the plan slug.
98
+ cwd: Current working directory for hint.
99
+
100
+ Returns:
101
+ True if file was deleted, False if not found or error.
102
+ """
103
+ try:
104
+ installation = require_claude_installation(ctx)
105
+ except SystemExit:
106
+ return False
107
+
108
+ slugs = installation.extract_slugs_from_session(cwd, session_id)
109
+ if not slugs:
110
+ return False
111
+
112
+ plan_file = installation.get_plans_dir_path() / f"{slugs[-1]}.md"
113
+ if plan_file.exists():
114
+ plan_file.unlink()
115
+ return True
116
+ return False
117
+
118
+
119
+ def _get_worktree_name() -> str | None:
120
+ """Get current worktree name from git worktree list."""
121
+ try:
122
+ result = subprocess.run(
123
+ ["git", "worktree", "list", "--porcelain"],
124
+ capture_output=True,
125
+ text=True,
126
+ check=True,
127
+ )
128
+
129
+ current_dir = Path.cwd().resolve()
130
+ lines = result.stdout.strip().split("\n")
131
+
132
+ for line in lines:
133
+ if line.startswith("worktree "):
134
+ worktree_path = Path(line[len("worktree ") :])
135
+ if current_dir == worktree_path or current_dir.is_relative_to(worktree_path):
136
+ return worktree_path.name
137
+
138
+ return None
139
+ except subprocess.CalledProcessError:
140
+ return None
141
+
142
+
143
+ def _get_branch_name() -> str | None:
144
+ """Get current git branch name."""
145
+ try:
146
+ result = subprocess.run(
147
+ ["git", "branch", "--show-current"],
148
+ capture_output=True,
149
+ text=True,
150
+ check=True,
151
+ )
152
+ branch = result.stdout.strip()
153
+ if branch:
154
+ return branch
155
+ return None
156
+ except subprocess.CalledProcessError:
157
+ return None
158
+
159
+
160
+ def _signal_started(ctx: click.Context, session_id: str | None) -> None:
161
+ """Handle 'started' event - post comment and update metadata."""
162
+ event = "started"
163
+
164
+ # Find impl directory (.impl/ or .worker-impl/) - check BEFORE context access
165
+ impl_dir = Path.cwd() / ".impl"
166
+ if not impl_dir.exists():
167
+ impl_dir = Path.cwd() / ".worker-impl"
168
+
169
+ # Read issue reference FIRST (doesn't require context)
170
+ issue_ref = read_issue_reference(impl_dir)
171
+ if issue_ref is None:
172
+ _output_error(event, "no-issue-reference", "No issue reference found in issue.json")
173
+ return
174
+
175
+ # Delete Claude plan file if session_id provided
176
+ # The plan has been saved to GitHub and snapshotted, so it's safe to delete
177
+ if session_id is not None:
178
+ _delete_claude_plan_file(ctx, session_id, Path.cwd())
179
+
180
+ # Now get context dependencies (after confirming we need them)
181
+ try:
182
+ repo_root = require_repo_root(ctx)
183
+ except SystemExit:
184
+ _output_error(event, "context-not-initialized", "Context not initialized")
185
+ return
186
+
187
+ # Get worktree and branch names
188
+ worktree_name = _get_worktree_name()
189
+ if worktree_name is None:
190
+ _output_error(event, "worktree-detection-failed", "Could not determine worktree name")
191
+ return
192
+
193
+ branch_name = _get_branch_name()
194
+ if branch_name is None:
195
+ _output_error(event, "branch-detection-failed", "Could not determine branch name")
196
+ return
197
+
198
+ # Capture metadata
199
+ timestamp = datetime.now(UTC).isoformat()
200
+ session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
201
+ user = getpass.getuser()
202
+
203
+ # Write local state file first (fast, no network)
204
+ try:
205
+ write_local_run_state(
206
+ impl_dir=impl_dir,
207
+ last_event="started",
208
+ timestamp=timestamp,
209
+ user=user,
210
+ session_id=session_id,
211
+ )
212
+ except (FileNotFoundError, ValueError) as e:
213
+ _output_error(event, "local-state-write-failed", f"Failed to write local state: {e}")
214
+ return
215
+
216
+ # Get GitHub Issues from context
217
+ try:
218
+ github = require_github_issues(ctx)
219
+ except SystemExit:
220
+ _output_error(event, "context-not-initialized", "Context not initialized")
221
+ return
222
+
223
+ # Post start comment
224
+ try:
225
+ description = f"""**Worktree:** `{worktree_name}`
226
+ **Branch:** `{branch_name}`"""
227
+
228
+ comment_body = render_erk_issue_event(
229
+ title="🚀 Starting implementation",
230
+ metadata=None,
231
+ description=description,
232
+ )
233
+
234
+ github.add_comment(repo_root, issue_ref.issue_number, comment_body)
235
+ except RuntimeError as e:
236
+ _output_error(event, "github-comment-failed", f"Failed to post comment: {e}")
237
+ return
238
+
239
+ # Update issue metadata
240
+ try:
241
+ issue = github.get_issue(repo_root, issue_ref.issue_number)
242
+
243
+ if in_github_actions():
244
+ updated_body = update_plan_header_remote_impl(
245
+ issue_body=issue.body,
246
+ remote_impl_at=timestamp,
247
+ )
248
+ else:
249
+ updated_body = update_plan_header_local_impl_event(
250
+ issue_body=issue.body,
251
+ local_impl_at=timestamp,
252
+ event="started",
253
+ session_id=session_id,
254
+ user=user,
255
+ )
256
+
257
+ github.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
258
+ except (RuntimeError, ValueError):
259
+ # Non-fatal - comment was posted, metadata update failed
260
+ # Continue successfully
261
+ pass
262
+
263
+ result = SignalSuccess(
264
+ success=True,
265
+ event=event,
266
+ issue_number=issue_ref.issue_number,
267
+ )
268
+ click.echo(json.dumps(asdict(result), indent=2))
269
+ raise SystemExit(0)
270
+
271
+
272
+ def _signal_ended(ctx: click.Context) -> None:
273
+ """Handle 'ended' event - update metadata."""
274
+ event = "ended"
275
+
276
+ # Find impl directory - check BEFORE context access
277
+ impl_dir = Path.cwd() / ".impl"
278
+ if not impl_dir.exists():
279
+ impl_dir = Path.cwd() / ".worker-impl"
280
+
281
+ # Read issue reference FIRST (doesn't require context)
282
+ issue_ref = read_issue_reference(impl_dir)
283
+ if issue_ref is None:
284
+ _output_error(event, "no-issue-reference", "No issue reference found in issue.json")
285
+ return
286
+
287
+ # Now get context dependencies (after confirming we need them)
288
+ try:
289
+ repo_root = require_repo_root(ctx)
290
+ except SystemExit:
291
+ _output_error(event, "context-not-initialized", "Context not initialized")
292
+ return
293
+
294
+ # Capture metadata
295
+ timestamp = datetime.now(UTC).isoformat()
296
+ session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
297
+ user = getpass.getuser()
298
+
299
+ # Write local state file first
300
+ try:
301
+ write_local_run_state(
302
+ impl_dir=impl_dir,
303
+ last_event="ended",
304
+ timestamp=timestamp,
305
+ user=user,
306
+ session_id=session_id,
307
+ )
308
+ except (FileNotFoundError, ValueError) as e:
309
+ _output_error(event, "local-state-write-failed", f"Failed to write local state: {e}")
310
+ return
311
+
312
+ # Get GitHub Issues from context
313
+ try:
314
+ github = require_github_issues(ctx)
315
+ except SystemExit:
316
+ _output_error(event, "context-not-initialized", "Context not initialized")
317
+ return
318
+
319
+ # Update issue metadata
320
+ try:
321
+ issue = github.get_issue(repo_root, issue_ref.issue_number)
322
+
323
+ if in_github_actions():
324
+ updated_body = update_plan_header_remote_impl(
325
+ issue_body=issue.body,
326
+ remote_impl_at=timestamp,
327
+ )
328
+ else:
329
+ updated_body = update_plan_header_local_impl_event(
330
+ issue_body=issue.body,
331
+ local_impl_at=timestamp,
332
+ event="ended",
333
+ session_id=session_id,
334
+ user=user,
335
+ )
336
+
337
+ github.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
338
+ except (RuntimeError, ValueError) as e:
339
+ _output_error(event, "github-api-failed", f"Failed to update issue: {e}")
340
+ return
341
+
342
+ result = SignalSuccess(
343
+ success=True,
344
+ event=event,
345
+ issue_number=issue_ref.issue_number,
346
+ )
347
+ click.echo(json.dumps(asdict(result), indent=2))
348
+ raise SystemExit(0)
349
+
350
+
351
+ @click.command(name="impl-signal")
352
+ @click.argument("event", type=click.Choice(["started", "ended"]))
353
+ @click.option(
354
+ "--session-id",
355
+ default=None,
356
+ help="Session ID for plan file deletion on 'started' event",
357
+ )
358
+ @click.pass_context
359
+ def impl_signal(ctx: click.Context, event: str, session_id: str | None) -> None:
360
+ """Signal implementation events to GitHub.
361
+
362
+ EVENT can be 'started' or 'ended'.
363
+
364
+ 'started' posts a start comment and updates issue metadata.
365
+ 'ended' updates issue metadata with ended event.
366
+
367
+ When --session-id is provided on 'started', also deletes the Claude plan file
368
+ (the content has been saved to GitHub and snapshotted).
369
+
370
+ Always exits with code 0 for graceful degradation (|| true pattern).
371
+ """
372
+ if event == "started":
373
+ _signal_started(ctx, session_id)
374
+ else:
375
+ _signal_ended(ctx)
@@ -0,0 +1,49 @@
1
+ """Verify .impl/ folder still exists after implementation.
2
+
3
+ This exec command is a guardrail for /erk:plan-implement to ensure the agent
4
+ did not delete .impl/ during implementation. The .impl/ folder MUST be preserved
5
+ for user review.
6
+
7
+ Usage:
8
+ erk exec impl-verify
9
+
10
+ Output:
11
+ JSON with validation status
12
+
13
+ Exit Codes:
14
+ 0: .impl/ folder exists
15
+ 1: .impl/ folder was deleted (violation of instructions)
16
+
17
+ Examples:
18
+ $ erk exec impl-verify
19
+ {"valid": true, "impl_dir": "/path/to/.impl"}
20
+
21
+ $ erk exec impl-verify # when .impl/ is missing
22
+ {"valid": false, "error": ".impl/ folder was deleted during implementation...", ...}
23
+ """
24
+
25
+ import json
26
+
27
+ import click
28
+
29
+ from erk_shared.context.helpers import require_cwd
30
+
31
+
32
+ @click.command(name="impl-verify")
33
+ @click.pass_context
34
+ def impl_verify(ctx: click.Context) -> None:
35
+ """Verify .impl/ folder still exists after implementation."""
36
+ cwd = require_cwd(ctx)
37
+ impl_dir = cwd / ".impl"
38
+
39
+ if not impl_dir.exists():
40
+ # Hard error - agent violated instructions
41
+ result = {
42
+ "valid": False,
43
+ "error": ".impl/ folder was deleted during implementation. This violates instructions.",
44
+ "action": "The .impl/ folder must be preserved for user review.",
45
+ }
46
+ click.echo(json.dumps(result))
47
+ raise SystemExit(1)
48
+
49
+ click.echo(json.dumps({"valid": True, "impl_dir": str(impl_dir)}))
@@ -0,0 +1,34 @@
1
+ """Convert plan title to filename.
2
+
3
+ Usage:
4
+ erk exec issue-title-to-filename "Plan Title"
5
+
6
+ Single source of truth for filename transformation for /erk:plan-save.
7
+
8
+ Output:
9
+ Filename on stdout (e.g., "my-feature-plan.md")
10
+ Error message on stderr with exit code 1 on failure
11
+
12
+ Exit Codes:
13
+ 0: Success
14
+ 1: Error (empty title)
15
+ """
16
+
17
+ import click
18
+
19
+ from erk_shared.naming import generate_filename_from_title
20
+
21
+
22
+ @click.command(name="issue-title-to-filename")
23
+ @click.argument("title")
24
+ def issue_title_to_filename(title: str) -> None:
25
+ """Convert plan title to filename.
26
+
27
+ TITLE: Plan title to convert
28
+ """
29
+ if not title or not title.strip():
30
+ click.echo(click.style("Error: ", fg="red") + "Plan title cannot be empty", err=True)
31
+ raise SystemExit(1)
32
+
33
+ filename = generate_filename_from_title(title)
34
+ click.echo(filename)