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,163 @@
1
+ """Marker file operations for inter-process communication.
2
+
3
+ Usage:
4
+ erk exec marker create --session-id SESSION_ID <name>
5
+ erk exec marker exists --session-id SESSION_ID <name>
6
+ erk exec marker delete --session-id SESSION_ID <name>
7
+
8
+ Marker files are stored in `.erk/scratch/sessions/<session-id>/` and are used for
9
+ inter-process communication between hooks and commands. Session ID can be provided
10
+ via `--session-id` flag or `$CLAUDE_CODE_SESSION_ID` environment variable.
11
+
12
+ The `--session-id` flag takes precedence over the environment variable.
13
+
14
+ Exit codes:
15
+ create: 0 = created, 1 = error (missing session ID)
16
+ exists: 0 = exists, 1 = does not exist
17
+ delete: 0 = deleted (or didn't exist), 1 = error (missing session ID)
18
+ """
19
+
20
+ import json
21
+ import os
22
+
23
+ import click
24
+
25
+ from erk_shared.context.helpers import require_repo_root
26
+ from erk_shared.scratch.scratch import get_scratch_dir
27
+
28
+ MARKER_EXTENSION = ".marker"
29
+
30
+
31
+ def _resolve_session_id(session_id: str | None) -> str | None:
32
+ """Resolve session ID from explicit argument or environment variable.
33
+
34
+ Priority:
35
+ 1. Explicit session_id argument (if provided)
36
+ 2. CLAUDE_CODE_SESSION_ID environment variable
37
+ 3. None (if neither available)
38
+ """
39
+ if session_id is not None:
40
+ return session_id
41
+ return os.environ.get("CLAUDE_CODE_SESSION_ID")
42
+
43
+
44
+ def _output_json(success: bool, message: str) -> None:
45
+ """Output JSON response."""
46
+ click.echo(json.dumps({"success": success, "message": message}))
47
+
48
+
49
+ @click.group(name="marker")
50
+ def marker() -> None:
51
+ """Manage marker files for inter-process communication."""
52
+
53
+
54
+ @marker.command(name="create")
55
+ @click.argument("name")
56
+ @click.option(
57
+ "--session-id",
58
+ default=None,
59
+ help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
60
+ )
61
+ @click.option(
62
+ "--associated-objective",
63
+ type=int,
64
+ default=None,
65
+ help="Associated objective issue number (stored in marker file)",
66
+ )
67
+ @click.pass_context
68
+ def marker_create(
69
+ ctx: click.Context, name: str, session_id: str | None, associated_objective: int | None
70
+ ) -> None:
71
+ """Create a marker file.
72
+
73
+ NAME is the marker name (e.g., 'incremental-plan').
74
+ The '.marker' extension is added automatically.
75
+
76
+ If --associated-objective is provided, the issue number is stored
77
+ in the marker file content. Otherwise, an empty file is created.
78
+ """
79
+ resolved_session_id = _resolve_session_id(session_id)
80
+ if resolved_session_id is None:
81
+ msg = (
82
+ "Missing session ID: provide --session-id or set "
83
+ "CLAUDE_CODE_SESSION_ID environment variable"
84
+ )
85
+ _output_json(False, msg)
86
+ raise SystemExit(1) from None
87
+
88
+ repo_root = require_repo_root(ctx)
89
+ scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
90
+ marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
91
+ if associated_objective is not None:
92
+ marker_file.write_text(str(associated_objective), encoding="utf-8")
93
+ else:
94
+ marker_file.touch()
95
+ _output_json(True, f"Created marker: {name}")
96
+
97
+
98
+ @marker.command(name="exists")
99
+ @click.argument("name")
100
+ @click.option(
101
+ "--session-id",
102
+ default=None,
103
+ help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
104
+ )
105
+ @click.pass_context
106
+ def marker_exists(ctx: click.Context, name: str, session_id: str | None) -> None:
107
+ """Check if a marker file exists.
108
+
109
+ NAME is the marker name (e.g., 'incremental-plan').
110
+ Exit code 0 if exists, 1 if not.
111
+ """
112
+ resolved_session_id = _resolve_session_id(session_id)
113
+ if resolved_session_id is None:
114
+ msg = (
115
+ "Missing session ID: provide --session-id or set "
116
+ "CLAUDE_CODE_SESSION_ID environment variable"
117
+ )
118
+ _output_json(False, msg)
119
+ raise SystemExit(1) from None
120
+
121
+ repo_root = require_repo_root(ctx)
122
+ scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
123
+ marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
124
+
125
+ if marker_file.exists():
126
+ _output_json(True, f"Marker exists: {name}")
127
+ else:
128
+ _output_json(False, f"Marker does not exist: {name}")
129
+ raise SystemExit(1) from None
130
+
131
+
132
+ @marker.command(name="delete")
133
+ @click.argument("name")
134
+ @click.option(
135
+ "--session-id",
136
+ default=None,
137
+ help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
138
+ )
139
+ @click.pass_context
140
+ def marker_delete(ctx: click.Context, name: str, session_id: str | None) -> None:
141
+ """Delete a marker file.
142
+
143
+ NAME is the marker name (e.g., 'incremental-plan').
144
+ Succeeds even if marker doesn't exist (idempotent).
145
+ """
146
+ resolved_session_id = _resolve_session_id(session_id)
147
+ if resolved_session_id is None:
148
+ msg = (
149
+ "Missing session ID: provide --session-id or set "
150
+ "CLAUDE_CODE_SESSION_ID environment variable"
151
+ )
152
+ _output_json(False, msg)
153
+ raise SystemExit(1) from None
154
+
155
+ repo_root = require_repo_root(ctx)
156
+ scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
157
+ marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
158
+
159
+ if marker_file.exists():
160
+ marker_file.unlink()
161
+ _output_json(True, f"Deleted marker: {name}")
162
+ else:
163
+ _output_json(True, f"Marker already deleted: {name}")
@@ -0,0 +1,109 @@
1
+ """Save plan as objective GitHub issue.
2
+
3
+ Usage:
4
+ erk exec objective-save-to-issue [OPTIONS]
5
+
6
+ This command extracts a plan and creates a GitHub issue with:
7
+ - erk-plan + erk-objective labels (like extraction has erk-plan + erk-extraction)
8
+ - No title suffix
9
+ - Plan content directly in body (no metadata block)
10
+ - No commands section
11
+
12
+ Options:
13
+ --session-id ID: Session ID for scoped plan lookup
14
+ --format: json (default) or display
15
+
16
+ Exit Codes:
17
+ 0: Success - objective issue created
18
+ 1: Error - no plan found, gh failure, etc.
19
+ """
20
+
21
+ import json
22
+
23
+ import click
24
+
25
+ from erk_shared.context.helpers import (
26
+ require_claude_installation,
27
+ require_cwd,
28
+ require_repo_root,
29
+ )
30
+ from erk_shared.context.helpers import (
31
+ require_issues as require_github_issues,
32
+ )
33
+ from erk_shared.github.plan_issues import create_objective_issue
34
+
35
+
36
+ @click.command(name="objective-save-to-issue")
37
+ @click.option(
38
+ "--format",
39
+ "output_format",
40
+ type=click.Choice(["json", "display"]),
41
+ default="json",
42
+ help="Output format: json (default) or display (formatted text)",
43
+ )
44
+ @click.option(
45
+ "--session-id",
46
+ default=None,
47
+ help="Session ID for scoped plan lookup",
48
+ )
49
+ @click.pass_context
50
+ def objective_save_to_issue(ctx: click.Context, output_format: str, session_id: str | None) -> None:
51
+ """Save plan as objective GitHub issue.
52
+
53
+ Creates a GitHub issue with erk-plan + erk-objective labels and plan content in body.
54
+ """
55
+ # Get dependencies from context
56
+ github = require_github_issues(ctx)
57
+ repo_root = require_repo_root(ctx)
58
+ cwd = require_cwd(ctx)
59
+ claude_installation = require_claude_installation(ctx)
60
+
61
+ # Get plan content
62
+ plan = claude_installation.get_latest_plan(cwd, session_id=session_id)
63
+
64
+ if not plan:
65
+ if output_format == "display":
66
+ click.echo("Error: No plan found in ~/.claude/plans/", err=True)
67
+ click.echo("\nTo fix:", err=True)
68
+ click.echo("1. Create a plan (enter Plan mode if needed)", err=True)
69
+ click.echo("2. Exit Plan mode using ExitPlanMode tool", err=True)
70
+ click.echo("3. Run this command again", err=True)
71
+ else:
72
+ click.echo(json.dumps({"success": False, "error": "No plan found in ~/.claude/plans/"}))
73
+ raise SystemExit(1)
74
+
75
+ # Create objective issue
76
+ result = create_objective_issue(
77
+ github_issues=github,
78
+ repo_root=repo_root,
79
+ plan_content=plan,
80
+ title=None,
81
+ extra_labels=None,
82
+ )
83
+
84
+ if not result.success:
85
+ if output_format == "display":
86
+ click.echo(f"Error: {result.error}", err=True)
87
+ else:
88
+ click.echo(json.dumps({"success": False, "error": result.error}))
89
+ raise SystemExit(1)
90
+
91
+ # Guard for type narrowing
92
+ if result.issue_number is None:
93
+ raise RuntimeError("Unexpected: issue_number is None after success")
94
+
95
+ if output_format == "display":
96
+ click.echo(f"Objective saved to GitHub issue #{result.issue_number}")
97
+ click.echo(f"Title: {result.title}")
98
+ click.echo(f"URL: {result.issue_url}")
99
+ else:
100
+ click.echo(
101
+ json.dumps(
102
+ {
103
+ "success": True,
104
+ "issue_number": result.issue_number,
105
+ "issue_url": result.issue_url,
106
+ "title": result.title,
107
+ }
108
+ )
109
+ )
@@ -0,0 +1,269 @@
1
+ """Extract plan from ~/.claude/plans/ and create GitHub issue in one operation.
2
+
3
+ Usage:
4
+ erk exec plan-save-to-issue [OPTIONS]
5
+
6
+ This command combines plan extraction and issue creation:
7
+ 1. Extract plan from specified file, session-scoped lookup, or latest from ~/.claude/plans/
8
+ 2. Create GitHub issue with plan content
9
+
10
+ Options:
11
+ --plan-file PATH: Use specific plan file (highest priority)
12
+ --session-id ID: Use session-scoped lookup to find plan by slug
13
+ (neither): Fall back to most recent plan by modification time
14
+
15
+ Output:
16
+ --format json (default): {"success": true, "issue_number": N, ...}
17
+ --format display: Formatted text ready for display
18
+
19
+ Exit Codes:
20
+ 0: Success - plan extracted and issue created
21
+ 1: Error - no plan found, gh failure, etc.
22
+ """
23
+
24
+ import json
25
+ from pathlib import Path
26
+
27
+ import click
28
+
29
+ from erk_shared.context.helpers import (
30
+ get_repo_identifier,
31
+ require_claude_installation,
32
+ require_cwd,
33
+ require_local_config,
34
+ require_repo_root,
35
+ )
36
+ from erk_shared.context.helpers import (
37
+ require_issues as require_github_issues,
38
+ )
39
+ from erk_shared.github.plan_issues import create_plan_issue
40
+ from erk_shared.output.next_steps import format_next_steps_plain
41
+ from erk_shared.scratch.plan_snapshots import snapshot_plan_for_session
42
+ from erk_shared.scratch.scratch import get_scratch_dir
43
+
44
+
45
+ def _create_plan_saved_marker(session_id: str, repo_root: Path) -> None:
46
+ """Create marker file to indicate plan was saved to GitHub.
47
+
48
+ Args:
49
+ session_id: The session ID for the scratch directory.
50
+ repo_root: The repository root path.
51
+ """
52
+ marker_dir = get_scratch_dir(session_id, repo_root=repo_root)
53
+ marker_file = marker_dir / "exit-plan-mode-hook.plan-saved.marker"
54
+ marker_file.write_text(
55
+ "Created by: exit-plan-mode-hook (via /erk:plan-save)\n"
56
+ "Trigger: Plan was successfully saved to GitHub\n"
57
+ "Effect: Next ExitPlanMode call will be BLOCKED (remain in plan mode, session complete)\n"
58
+ "Lifecycle: Deleted after being read by next hook invocation\n",
59
+ encoding="utf-8",
60
+ )
61
+
62
+
63
+ @click.command(name="plan-save-to-issue")
64
+ @click.option(
65
+ "--format",
66
+ "output_format",
67
+ type=click.Choice(["json", "display"]),
68
+ default="json",
69
+ help="Output format: json (default) or display (formatted text)",
70
+ )
71
+ @click.option(
72
+ "--plan-file",
73
+ type=click.Path(exists=True, path_type=Path),
74
+ default=None,
75
+ help="Path to specific plan file (highest priority)",
76
+ )
77
+ @click.option(
78
+ "--session-id",
79
+ default=None,
80
+ help="Session ID for scoped plan lookup (uses slug from session logs)",
81
+ )
82
+ @click.option(
83
+ "--objective-issue",
84
+ type=int,
85
+ default=None,
86
+ help="Link plan to parent objective issue number",
87
+ )
88
+ @click.pass_context
89
+ def plan_save_to_issue(
90
+ ctx: click.Context,
91
+ output_format: str,
92
+ plan_file: Path | None,
93
+ session_id: str | None,
94
+ objective_issue: int | None,
95
+ ) -> None:
96
+ """Extract plan from ~/.claude/plans/ and create GitHub issue.
97
+
98
+ Combines plan extraction and issue creation in a single operation.
99
+ """
100
+ # Get dependencies from context
101
+ github = require_github_issues(ctx)
102
+ repo_root = require_repo_root(ctx)
103
+ cwd = require_cwd(ctx)
104
+ claude_installation = require_claude_installation(ctx)
105
+
106
+ # session_id comes from --session-id CLI option (or None if not provided)
107
+ effective_session_id = session_id
108
+
109
+ # Step 1: Extract plan (priority: plan_file > session_id > most recent)
110
+ if plan_file:
111
+ plan = plan_file.read_text(encoding="utf-8")
112
+ else:
113
+ plan = claude_installation.get_latest_plan(cwd, session_id=effective_session_id)
114
+
115
+ if not plan:
116
+ if output_format == "display":
117
+ click.echo("Error: No plan found in ~/.claude/plans/", err=True)
118
+ click.echo("\nTo fix:", err=True)
119
+ click.echo("1. Create a plan (enter Plan mode if needed)", err=True)
120
+ click.echo("2. Exit Plan mode using ExitPlanMode tool", err=True)
121
+ click.echo("3. Run this command again", err=True)
122
+ else:
123
+ click.echo(json.dumps({"success": False, "error": "No plan found in ~/.claude/plans/"}))
124
+ raise SystemExit(1)
125
+
126
+ # Determine source_repo for cross-repo plans
127
+ # When plans_repo is configured, plans are stored in a separate repo
128
+ # and source_repo records where implementation will happen
129
+ source_repo: str | None = None
130
+ config = require_local_config(ctx)
131
+ if config.plans_repo is not None:
132
+ source_repo = get_repo_identifier(ctx)
133
+
134
+ # Use consolidated create_plan_issue for the entire workflow
135
+ result = create_plan_issue(
136
+ github_issues=github,
137
+ repo_root=repo_root,
138
+ plan_content=plan,
139
+ title=None,
140
+ plan_type=None,
141
+ extra_labels=None,
142
+ title_suffix=None,
143
+ source_plan_issues=None,
144
+ extraction_session_ids=None,
145
+ source_repo=source_repo,
146
+ objective_issue=objective_issue,
147
+ )
148
+
149
+ if not result.success:
150
+ if result.issue_number is not None:
151
+ # Partial success - issue created but comment failed
152
+ if output_format == "display":
153
+ click.echo(f"Warning: {result.error}", err=True)
154
+ click.echo(f"Please manually add plan content to: {result.issue_url}", err=True)
155
+ else:
156
+ click.echo(
157
+ json.dumps(
158
+ {
159
+ "success": False,
160
+ "error": result.error,
161
+ "issue_number": result.issue_number,
162
+ "issue_url": result.issue_url,
163
+ }
164
+ )
165
+ )
166
+ else:
167
+ if output_format == "display":
168
+ click.echo(f"Error: {result.error}", err=True)
169
+ else:
170
+ click.echo(json.dumps({"success": False, "error": result.error}))
171
+ raise SystemExit(1)
172
+
173
+ # DISABLED: Session context embedding is temporarily disabled while rethinking extraction plans
174
+ # To re-enable, uncomment the following block and restore imports:
175
+ # from erk_shared.context.helpers import require_git
176
+ # from erk_shared.extraction.session_context import collect_session_context
177
+ # from erk_shared.github.metadata import render_session_content_blocks
178
+ #
179
+ # git = require_git(ctx)
180
+ # session_result = collect_session_context(
181
+ # git=git,
182
+ # cwd=cwd,
183
+ # session_store=session_store,
184
+ # current_session_id=effective_session_id,
185
+ # min_size=1024,
186
+ # limit=20,
187
+ # )
188
+ #
189
+ # if session_result is not None and result.issue_number is not None:
190
+ # # Render and post as comments
191
+ # session_label = session_result.branch_context.current_branch or "planning-session"
192
+ # content_blocks = render_session_content_blocks(
193
+ # content=session_result.combined_xml,
194
+ # session_label=session_label,
195
+ # extraction_hints=["Planning session context for downstream analysis"],
196
+ # )
197
+ #
198
+ # # Post each block as a comment (failures are non-blocking)
199
+ # for block in content_blocks:
200
+ # try:
201
+ # github.add_comment(repo_root, result.issue_number, block)
202
+ # session_context_chunks += 1
203
+ # except RuntimeError:
204
+ # # Session context is supplementary - don't fail the command
205
+ # pass
206
+ #
207
+ # session_ids = session_result.session_ids
208
+
209
+ # Output JSON still includes these for backwards compatibility
210
+ session_context_chunks = 0
211
+ session_ids: list[str] = []
212
+
213
+ # Step 9: Create marker file to indicate plan was saved
214
+ snapshot_result = None
215
+ if effective_session_id:
216
+ _create_plan_saved_marker(effective_session_id, repo_root)
217
+
218
+ # Step 9.1: Snapshot the plan file to session-scoped storage
219
+ # Determine plan file path
220
+ if plan_file:
221
+ snapshot_path = plan_file
222
+ else:
223
+ # Look up slug from session to find plan file
224
+ snapshot_path = claude_installation.find_plan_for_session(cwd, effective_session_id)
225
+
226
+ if snapshot_path is not None and snapshot_path.exists():
227
+ snapshot_result = snapshot_plan_for_session(
228
+ session_id=effective_session_id,
229
+ plan_file_path=snapshot_path,
230
+ project_cwd=cwd,
231
+ claude_installation=claude_installation,
232
+ repo_root=repo_root,
233
+ )
234
+ # NOTE: Plan file deletion moved to impl_signal.py on 'started' event
235
+ # This allows the user to modify and re-save the plan before implementing
236
+
237
+ # Step 10: Output success
238
+ # Detect enrichment status for informational output
239
+ is_enriched = "## Enrichment Details" in plan
240
+
241
+ # At this point result.success is True, so issue_number must be set
242
+ # Guard for type narrowing
243
+ if result.issue_number is None:
244
+ raise RuntimeError("Unexpected: issue_number is None after successful create_plan_issue")
245
+
246
+ if output_format == "display":
247
+ click.echo(f"Plan saved to GitHub issue #{result.issue_number}")
248
+ click.echo(f"Title: {result.title}")
249
+ click.echo(f"URL: {result.issue_url}")
250
+ click.echo(f"Enrichment: {'Yes' if is_enriched else 'No'}")
251
+ if session_context_chunks > 0:
252
+ click.echo(f"Session context: {session_context_chunks} chunks")
253
+ if snapshot_result is not None:
254
+ click.echo(f"Archived: {snapshot_result.snapshot_dir}")
255
+ click.echo()
256
+ click.echo(format_next_steps_plain(result.issue_number))
257
+ else:
258
+ output_data = {
259
+ "success": True,
260
+ "issue_number": result.issue_number,
261
+ "issue_url": result.issue_url,
262
+ "title": result.title,
263
+ "enriched": is_enriched,
264
+ "session_context_chunks": session_context_chunks,
265
+ "session_ids": session_ids,
266
+ }
267
+ if snapshot_result is not None:
268
+ output_data["archived_to"] = str(snapshot_result.snapshot_dir)
269
+ click.echo(json.dumps(output_data))
@@ -0,0 +1,147 @@
1
+ """Update an existing GitHub issue's plan comment with new content.
2
+
3
+ Usage:
4
+ erk exec plan-update-issue --issue-number N [OPTIONS]
5
+
6
+ This command updates the plan content comment on an existing GitHub issue:
7
+ 1. Find plan file (from session scratch, --plan-path, or ~/.claude/plans/)
8
+ 2. Get the first comment ID from the issue (where plan body lives)
9
+ 3. Update that comment with new plan content
10
+
11
+ Options:
12
+ --issue-number N: GitHub issue number to update (required)
13
+ --session-id ID: Session ID to find plan file in scratch storage
14
+ --plan-path PATH: Direct path to plan file (overrides session lookup)
15
+
16
+ Output:
17
+ --format json (default): {"success": true, ...}
18
+ --format display: Formatted text
19
+
20
+ Exit Codes:
21
+ 0: Success - plan comment updated
22
+ 1: Error - issue not found, no plan found, no comments, etc.
23
+ """
24
+
25
+ import json
26
+ from pathlib import Path
27
+
28
+ import click
29
+
30
+ from erk_shared.context.helpers import (
31
+ require_claude_installation,
32
+ require_cwd,
33
+ require_repo_root,
34
+ )
35
+ from erk_shared.context.helpers import (
36
+ require_issues as require_github_issues,
37
+ )
38
+ from erk_shared.github.metadata.plan_header import format_plan_content_comment
39
+
40
+
41
+ @click.command(name="plan-update-issue")
42
+ @click.option(
43
+ "--issue-number",
44
+ type=int,
45
+ required=True,
46
+ help="GitHub issue number to update",
47
+ )
48
+ @click.option(
49
+ "--format",
50
+ "output_format",
51
+ type=click.Choice(["json", "display"]),
52
+ default="json",
53
+ help="Output format: json (default) or display (formatted text)",
54
+ )
55
+ @click.option(
56
+ "--plan-path",
57
+ type=click.Path(exists=True, path_type=Path),
58
+ help="Direct path to plan file (overrides session lookup)",
59
+ )
60
+ @click.option(
61
+ "--session-id",
62
+ help="Session ID to find plan file in scratch storage",
63
+ )
64
+ @click.pass_context
65
+ def plan_update_issue(
66
+ ctx: click.Context,
67
+ issue_number: int,
68
+ output_format: str,
69
+ plan_path: Path | None,
70
+ session_id: str | None,
71
+ ) -> None:
72
+ """Update an existing GitHub issue's plan comment with new content."""
73
+ # Get dependencies from context
74
+ github = require_github_issues(ctx)
75
+ repo_root = require_repo_root(ctx)
76
+ cwd = require_cwd(ctx)
77
+ claude_installation = require_claude_installation(ctx)
78
+
79
+ # Step 1: Find plan content (priority: plan_path > session > latest)
80
+ if plan_path is not None:
81
+ plan_content = plan_path.read_text(encoding="utf-8")
82
+ else:
83
+ plan_content = claude_installation.get_latest_plan(cwd, session_id=session_id)
84
+
85
+ if not plan_content:
86
+ error_msg = "No plan found in ~/.claude/plans/"
87
+ if output_format == "display":
88
+ click.echo(f"Error: {error_msg}", err=True)
89
+ else:
90
+ click.echo(json.dumps({"success": False, "error": error_msg}))
91
+ raise SystemExit(1)
92
+
93
+ # Step 2: Get existing issue to verify it exists
94
+ try:
95
+ issue = github.get_issue(repo_root, issue_number)
96
+ except RuntimeError as e:
97
+ error_msg = f"Failed to get issue #{issue_number}: {e}"
98
+ if output_format == "display":
99
+ click.echo(f"Error: {error_msg}", err=True)
100
+ else:
101
+ click.echo(json.dumps({"success": False, "error": error_msg}))
102
+ raise SystemExit(1) from e
103
+
104
+ # Step 3: Get first comment ID (where plan body lives in Schema v2)
105
+ comments = github.get_issue_comments_with_urls(repo_root, issue_number)
106
+ if not comments:
107
+ error_msg = f"Issue #{issue_number} has no comments - cannot update plan content"
108
+ if output_format == "display":
109
+ click.echo(f"Error: {error_msg}", err=True)
110
+ else:
111
+ click.echo(json.dumps({"success": False, "error": error_msg}))
112
+ raise SystemExit(1)
113
+
114
+ first_comment = comments[0]
115
+ comment_id = first_comment.id
116
+
117
+ # Step 4: Format plan content and update comment
118
+ formatted_plan = format_plan_content_comment(plan_content.strip())
119
+
120
+ try:
121
+ github.update_comment(repo_root, comment_id, formatted_plan)
122
+ except RuntimeError as e:
123
+ error_msg = f"Failed to update comment: {e}"
124
+ if output_format == "display":
125
+ click.echo(f"Error: {error_msg}", err=True)
126
+ else:
127
+ click.echo(json.dumps({"success": False, "error": error_msg}))
128
+ raise SystemExit(1) from e
129
+
130
+ # Step 5: Output success
131
+ if output_format == "display":
132
+ click.echo(f"Plan updated on issue #{issue_number}")
133
+ click.echo(f"Title: {issue.title}")
134
+ click.echo(f"URL: {issue.url}")
135
+ click.echo(f"Comment: {first_comment.url}")
136
+ else:
137
+ click.echo(
138
+ json.dumps(
139
+ {
140
+ "success": True,
141
+ "issue_number": issue_number,
142
+ "issue_url": issue.url,
143
+ "comment_id": comment_id,
144
+ "comment_url": first_comment.url,
145
+ }
146
+ )
147
+ )