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.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.activation import render_activation_script
|
|
8
|
+
from erk.cli.commands.branch.unassign_cmd import execute_unassign
|
|
9
|
+
from erk.cli.commands.wt.create_cmd import ensure_worktree_for_branch
|
|
10
|
+
from erk.cli.ensure import Ensure
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
from erk.core.repo_discovery import RepoContext
|
|
13
|
+
from erk.core.worktree_pool import PoolState, SlotAssignment, load_pool_state
|
|
14
|
+
from erk.core.worktree_utils import compute_relative_path_in_worktree
|
|
15
|
+
from erk_shared.debug import debug_log
|
|
16
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
17
|
+
from erk_shared.github.types import PRNotFound
|
|
18
|
+
from erk_shared.output.output import machine_output, user_confirm, user_output
|
|
19
|
+
from erk_shared.scratch.markers import PENDING_EXTRACTION_MARKER, marker_exists
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_pending_extraction_marker(worktree_path: Path, force: bool) -> None:
|
|
23
|
+
"""Check for pending extraction marker and block deletion if present.
|
|
24
|
+
|
|
25
|
+
This provides friction before worktree deletion to ensure insights are
|
|
26
|
+
extracted from the session logs. The marker is created by `erk pr land`
|
|
27
|
+
and deleted by `erk plan extraction raw`.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
worktree_path: Path to the worktree being deleted
|
|
31
|
+
force: If True, warn but don't block deletion
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
SystemExit: If marker exists and force is False
|
|
35
|
+
"""
|
|
36
|
+
if not marker_exists(worktree_path, PENDING_EXTRACTION_MARKER):
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if force:
|
|
40
|
+
user_output(
|
|
41
|
+
click.style("Warning: ", fg="yellow") + "Skipping pending extraction (--force used).\n"
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
user_output(
|
|
46
|
+
click.style("Error: ", fg="red") + "Worktree has pending extraction.\n"
|
|
47
|
+
"Run: erk plan extraction raw\n"
|
|
48
|
+
"Or use --force to skip extraction."
|
|
49
|
+
)
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_clean_working_tree(ctx: ErkContext) -> None:
|
|
54
|
+
"""Check that working tree has no uncommitted changes.
|
|
55
|
+
|
|
56
|
+
Raises SystemExit if uncommitted changes found.
|
|
57
|
+
"""
|
|
58
|
+
Ensure.invariant(
|
|
59
|
+
not ctx.git.has_uncommitted_changes(ctx.cwd),
|
|
60
|
+
"Cannot delete current branch with uncommitted changes.\n"
|
|
61
|
+
"Please commit or stash your changes first.",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def verify_pr_closed_or_merged(ctx: ErkContext, repo_root: Path, branch: str, force: bool) -> None:
|
|
66
|
+
"""Verify that the branch's PR is closed or merged on GitHub.
|
|
67
|
+
|
|
68
|
+
Warns if no PR exists, raises SystemExit if PR is still OPEN (unless force=True).
|
|
69
|
+
Allows deletion for both MERGED and CLOSED PRs (abandoned/rejected work).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
ctx: Erk context
|
|
73
|
+
repo_root: Path to the repository root
|
|
74
|
+
branch: Branch name to check
|
|
75
|
+
force: If True, prompt for confirmation instead of blocking on open PRs
|
|
76
|
+
"""
|
|
77
|
+
pr_details = ctx.github.get_pr_for_branch(repo_root, branch)
|
|
78
|
+
|
|
79
|
+
if isinstance(pr_details, PRNotFound):
|
|
80
|
+
# Warn but continue when no PR exists
|
|
81
|
+
user_output(
|
|
82
|
+
click.style("Warning: ", fg="yellow")
|
|
83
|
+
+ f"No pull request found for branch '{branch}'.\n"
|
|
84
|
+
"Proceeding with deletion without PR verification."
|
|
85
|
+
)
|
|
86
|
+
return # Allow deletion to proceed
|
|
87
|
+
|
|
88
|
+
if pr_details.state == "OPEN":
|
|
89
|
+
if force:
|
|
90
|
+
# Show warning and prompt for confirmation
|
|
91
|
+
user_output(
|
|
92
|
+
click.style("Warning: ", fg="yellow")
|
|
93
|
+
+ f"Pull request for branch '{branch}' is still open.\n"
|
|
94
|
+
+ f"{pr_details.url}"
|
|
95
|
+
)
|
|
96
|
+
if not user_confirm("Delete branch anyway?", default=False):
|
|
97
|
+
raise SystemExit(1)
|
|
98
|
+
return # User confirmed, allow deletion
|
|
99
|
+
|
|
100
|
+
# Block deletion for open PRs (active work in progress)
|
|
101
|
+
user_output(
|
|
102
|
+
click.style("Error: ", fg="red")
|
|
103
|
+
+ f"Pull request for branch '{branch}' is still open.\n"
|
|
104
|
+
+ f"{pr_details.url}\n"
|
|
105
|
+
+ "Only closed or merged branches can be deleted with --delete-current."
|
|
106
|
+
)
|
|
107
|
+
raise SystemExit(1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def delete_branch_and_worktree(
|
|
111
|
+
ctx: ErkContext, repo: RepoContext, branch: str, worktree_path: Path
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Delete the specified branch and its worktree.
|
|
114
|
+
|
|
115
|
+
Uses two-step deletion: git worktree remove, then branch delete.
|
|
116
|
+
Note: remove_worktree already calls prune internally, so no additional prune needed.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
ctx: Erk context
|
|
120
|
+
repo: Repository context (uses main_repo_root for safe directory operations)
|
|
121
|
+
branch: Branch name to delete
|
|
122
|
+
worktree_path: Path to the worktree to remove
|
|
123
|
+
"""
|
|
124
|
+
# Use main_repo_root (not repo.root) to ensure we escape to a directory that
|
|
125
|
+
# still exists after worktree removal. repo.root equals the worktree path when
|
|
126
|
+
# running from inside a worktree.
|
|
127
|
+
# main_repo_root is always set by RepoContext.__post_init__, but ty doesn't know
|
|
128
|
+
main_repo = repo.main_repo_root if repo.main_repo_root else repo.root
|
|
129
|
+
|
|
130
|
+
# Escape the worktree if we're inside it (prevents FileNotFoundError after removal)
|
|
131
|
+
# Both paths must be resolved for reliable comparison - Path.cwd() returns resolved path
|
|
132
|
+
# but worktree_path may not be resolved, causing equality check to fail for same directory
|
|
133
|
+
cwd = Path.cwd().resolve()
|
|
134
|
+
resolved_worktree = worktree_path.resolve()
|
|
135
|
+
if cwd == resolved_worktree or resolved_worktree in cwd.parents:
|
|
136
|
+
os.chdir(main_repo)
|
|
137
|
+
|
|
138
|
+
# Remove the worktree (already calls prune internally)
|
|
139
|
+
ctx.git.remove_worktree(main_repo, worktree_path, force=True)
|
|
140
|
+
user_output(f"✓ Removed worktree: {click.style(str(worktree_path), fg='green')}")
|
|
141
|
+
|
|
142
|
+
# Delete the branch using Git abstraction
|
|
143
|
+
ctx.git.delete_branch_with_graphite(main_repo, branch, force=True)
|
|
144
|
+
user_output(f"✓ Deleted branch: {click.style(branch, fg='yellow')}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def find_assignment_by_worktree_path(
|
|
148
|
+
state: PoolState, worktree_path: Path
|
|
149
|
+
) -> SlotAssignment | None:
|
|
150
|
+
"""Find a slot assignment by its worktree path.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
state: Current pool state
|
|
154
|
+
worktree_path: Path to the worktree to find
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
SlotAssignment if the worktree is a pool slot, None otherwise
|
|
158
|
+
"""
|
|
159
|
+
if not worktree_path.exists():
|
|
160
|
+
return None
|
|
161
|
+
resolved_path = worktree_path.resolve()
|
|
162
|
+
for assignment in state.assignments:
|
|
163
|
+
if not assignment.worktree_path.exists():
|
|
164
|
+
continue
|
|
165
|
+
if assignment.worktree_path.resolve() == resolved_path:
|
|
166
|
+
return assignment
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def unallocate_worktree_and_branch(
|
|
171
|
+
ctx: ErkContext,
|
|
172
|
+
repo: RepoContext,
|
|
173
|
+
branch: str,
|
|
174
|
+
worktree_path: Path,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Unallocate a worktree and delete its branch.
|
|
177
|
+
|
|
178
|
+
If worktree is a pool slot: unassigns slot (keeps directory for reuse), deletes branch.
|
|
179
|
+
If not a pool slot: removes worktree directory, deletes branch.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
ctx: ErkContext with git operations
|
|
183
|
+
repo: Repository context
|
|
184
|
+
branch: Branch name to delete
|
|
185
|
+
worktree_path: Path to the worktree to unallocate
|
|
186
|
+
"""
|
|
187
|
+
main_repo_root = repo.main_repo_root if repo.main_repo_root else repo.root
|
|
188
|
+
|
|
189
|
+
# Check if this is a slot worktree
|
|
190
|
+
state = load_pool_state(repo.pool_json_path)
|
|
191
|
+
assignment: SlotAssignment | None = None
|
|
192
|
+
if state is not None:
|
|
193
|
+
assignment = find_assignment_by_worktree_path(state, worktree_path)
|
|
194
|
+
|
|
195
|
+
if assignment is not None:
|
|
196
|
+
# Slot worktree: unassign instead of delete
|
|
197
|
+
# state is guaranteed to be non-None since assignment was found in it
|
|
198
|
+
assert state is not None
|
|
199
|
+
execute_unassign(ctx, repo, state, assignment)
|
|
200
|
+
ctx.git.delete_branch_with_graphite(main_repo_root, branch, force=True)
|
|
201
|
+
user_output(click.style("✓", fg="green") + " Unassigned slot and deleted branch")
|
|
202
|
+
else:
|
|
203
|
+
# Non-slot worktree: delete both
|
|
204
|
+
ctx.git.remove_worktree(main_repo_root, worktree_path, force=True)
|
|
205
|
+
ctx.git.delete_branch_with_graphite(main_repo_root, branch, force=True)
|
|
206
|
+
user_output(click.style("✓", fg="green") + " Removed worktree and deleted branch")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def activate_root_repo(
|
|
210
|
+
ctx: ErkContext,
|
|
211
|
+
repo: RepoContext,
|
|
212
|
+
script: bool,
|
|
213
|
+
command_name: str,
|
|
214
|
+
post_cd_commands: Sequence[str] | None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Activate the root repository and exit.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
ctx: Erk context (for script_writer)
|
|
220
|
+
repo: Repository context
|
|
221
|
+
script: Whether to output script path or user message
|
|
222
|
+
command_name: Name of the command (for script generation)
|
|
223
|
+
post_cd_commands: Optional shell commands to run after cd (e.g., git pull)
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
SystemExit: Always (successful exit after activation)
|
|
227
|
+
"""
|
|
228
|
+
# Use main_repo_root (not repo.root) to ensure we reference a directory that
|
|
229
|
+
# still exists after worktree removal. repo.root equals the worktree path when
|
|
230
|
+
# running from inside a worktree.
|
|
231
|
+
root_path = repo.main_repo_root if repo.main_repo_root else repo.root
|
|
232
|
+
|
|
233
|
+
# Compute relative path to preserve user's position within worktree
|
|
234
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
235
|
+
relative_path = compute_relative_path_in_worktree(worktrees, ctx.cwd)
|
|
236
|
+
|
|
237
|
+
if script:
|
|
238
|
+
script_content = render_activation_script(
|
|
239
|
+
worktree_path=root_path,
|
|
240
|
+
target_subpath=relative_path,
|
|
241
|
+
post_cd_commands=post_cd_commands,
|
|
242
|
+
final_message='echo "Went to root repo: $(pwd)"',
|
|
243
|
+
comment="work activate-script (root repo)",
|
|
244
|
+
)
|
|
245
|
+
result = ctx.script_writer.write_activation_script(
|
|
246
|
+
script_content,
|
|
247
|
+
command_name=command_name,
|
|
248
|
+
comment="activate root",
|
|
249
|
+
)
|
|
250
|
+
machine_output(str(result.path), nl=False)
|
|
251
|
+
else:
|
|
252
|
+
user_output(f"Went to root repo: {root_path}")
|
|
253
|
+
user_output(
|
|
254
|
+
"\nShell integration not detected. "
|
|
255
|
+
"Run 'erk init --shell' to set up automatic activation."
|
|
256
|
+
)
|
|
257
|
+
user_output(f"Or use: source <(erk {command_name} --script)")
|
|
258
|
+
raise SystemExit(0)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def activate_worktree(
|
|
262
|
+
*,
|
|
263
|
+
ctx: ErkContext,
|
|
264
|
+
repo: RepoContext,
|
|
265
|
+
target_path: Path,
|
|
266
|
+
script: bool,
|
|
267
|
+
command_name: str,
|
|
268
|
+
preserve_relative_path: bool,
|
|
269
|
+
post_cd_commands: Sequence[str] | None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Activate a worktree and exit.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ctx: Erk context (for script_writer)
|
|
275
|
+
repo: Repository context
|
|
276
|
+
target_path: Path to the target worktree directory
|
|
277
|
+
script: Whether to output script path or user message
|
|
278
|
+
command_name: Name of the command (for script generation and debug logging)
|
|
279
|
+
preserve_relative_path: If True (default), compute and preserve the user's
|
|
280
|
+
relative directory position from the current worktree
|
|
281
|
+
post_cd_commands: Optional shell commands to run after activation (e.g., entry scripts)
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
SystemExit: If worktree not found, or after successful activation
|
|
285
|
+
"""
|
|
286
|
+
wt_path = target_path
|
|
287
|
+
|
|
288
|
+
Ensure.path_exists(ctx, wt_path, f"Worktree not found: {wt_path}")
|
|
289
|
+
|
|
290
|
+
worktree_name = wt_path.name
|
|
291
|
+
|
|
292
|
+
# Auto-compute relative path if requested
|
|
293
|
+
relative_path: Path | None = None
|
|
294
|
+
if preserve_relative_path:
|
|
295
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
296
|
+
relative_path = compute_relative_path_in_worktree(worktrees, ctx.cwd)
|
|
297
|
+
|
|
298
|
+
if script:
|
|
299
|
+
activation_script = render_activation_script(
|
|
300
|
+
worktree_path=wt_path,
|
|
301
|
+
target_subpath=relative_path,
|
|
302
|
+
post_cd_commands=post_cd_commands,
|
|
303
|
+
final_message='echo "Activated worktree: $(pwd)"',
|
|
304
|
+
comment="work activate-script",
|
|
305
|
+
)
|
|
306
|
+
result = ctx.script_writer.write_activation_script(
|
|
307
|
+
activation_script,
|
|
308
|
+
command_name=command_name,
|
|
309
|
+
comment=f"activate {worktree_name}",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
debug_log(f"{command_name.capitalize()}: Generated script at {result.path}")
|
|
313
|
+
debug_log(f"{command_name.capitalize()}: Script content:\n{activation_script}")
|
|
314
|
+
debug_log(f"{command_name.capitalize()}: File exists? {result.path.exists()}")
|
|
315
|
+
|
|
316
|
+
result.output_for_shell_integration()
|
|
317
|
+
else:
|
|
318
|
+
user_output(
|
|
319
|
+
"Shell integration not detected. Run 'erk init --shell' to set up automatic activation."
|
|
320
|
+
)
|
|
321
|
+
user_output(f"\nOr use: source <(erk {command_name} --script)")
|
|
322
|
+
raise SystemExit(0)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def resolve_up_navigation(
|
|
326
|
+
ctx: ErkContext, repo: RepoContext, current_branch: str, worktrees: list[WorktreeInfo]
|
|
327
|
+
) -> tuple[str, bool]:
|
|
328
|
+
"""Resolve --up navigation to determine target branch name.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
ctx: Erk context
|
|
332
|
+
repo: Repository context
|
|
333
|
+
current_branch: Current branch name
|
|
334
|
+
worktrees: List of worktrees from git_ops.list_worktrees()
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Tuple of (target_branch, was_created)
|
|
338
|
+
- target_branch: Target branch name to switch to
|
|
339
|
+
- was_created: True if worktree was newly created, False if it already existed
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
SystemExit: If navigation fails (at top of stack)
|
|
343
|
+
"""
|
|
344
|
+
# Navigate up to child branch
|
|
345
|
+
children = Ensure.truthy(
|
|
346
|
+
ctx.graphite.get_child_branches(ctx.git, repo.root, current_branch),
|
|
347
|
+
"Already at the top of the stack (no child branches)",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Fail explicitly if multiple children exist
|
|
351
|
+
if len(children) > 1:
|
|
352
|
+
children_list = ", ".join(f"'{child}'" for child in children)
|
|
353
|
+
user_output(
|
|
354
|
+
f"Error: Branch '{current_branch}' has multiple children: {children_list}.\n"
|
|
355
|
+
f"Please create worktree for specific child: erk create <branch-name>"
|
|
356
|
+
)
|
|
357
|
+
raise SystemExit(1)
|
|
358
|
+
|
|
359
|
+
# Use the single child
|
|
360
|
+
target_branch = children[0]
|
|
361
|
+
|
|
362
|
+
# Check if target branch has a worktree, create if necessary
|
|
363
|
+
target_wt_path = ctx.git.find_worktree_for_branch(repo.root, target_branch)
|
|
364
|
+
if target_wt_path is None:
|
|
365
|
+
# Auto-create worktree for target branch
|
|
366
|
+
_worktree_path, was_created = ensure_worktree_for_branch(ctx, repo, target_branch)
|
|
367
|
+
return target_branch, was_created
|
|
368
|
+
|
|
369
|
+
return target_branch, False
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def resolve_down_navigation(
|
|
373
|
+
ctx: ErkContext,
|
|
374
|
+
repo: RepoContext,
|
|
375
|
+
current_branch: str,
|
|
376
|
+
worktrees: list[WorktreeInfo],
|
|
377
|
+
trunk_branch: str | None,
|
|
378
|
+
) -> tuple[str, bool]:
|
|
379
|
+
"""Resolve --down navigation to determine target branch name.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
ctx: Erk context
|
|
383
|
+
repo: Repository context
|
|
384
|
+
current_branch: Current branch name
|
|
385
|
+
worktrees: List of worktrees from git_ops.list_worktrees()
|
|
386
|
+
trunk_branch: Configured trunk branch name, or None for auto-detection
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Tuple of (target_branch, was_created)
|
|
390
|
+
- target_branch: Target branch name or 'root' to switch to
|
|
391
|
+
- was_created: True if worktree was newly created, False if it already existed
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
SystemExit: If navigation fails (at bottom of stack)
|
|
395
|
+
"""
|
|
396
|
+
# Navigate down to parent branch
|
|
397
|
+
parent_branch = ctx.graphite.get_parent_branch(ctx.git, repo.root, current_branch)
|
|
398
|
+
if parent_branch is None:
|
|
399
|
+
# Check if we're already on trunk
|
|
400
|
+
detected_trunk = ctx.git.detect_trunk_branch(repo.root)
|
|
401
|
+
if current_branch == detected_trunk:
|
|
402
|
+
user_output(f"Already at the bottom of the stack (on trunk branch '{detected_trunk}')")
|
|
403
|
+
raise SystemExit(1)
|
|
404
|
+
else:
|
|
405
|
+
user_output("Error: Could not determine parent branch from Graphite metadata")
|
|
406
|
+
raise SystemExit(1)
|
|
407
|
+
|
|
408
|
+
# Check if parent is the trunk - if so, switch to root
|
|
409
|
+
detected_trunk = ctx.git.detect_trunk_branch(repo.root)
|
|
410
|
+
if parent_branch == detected_trunk:
|
|
411
|
+
# Check if trunk is checked out in root (repo.root path)
|
|
412
|
+
trunk_wt_path = ctx.git.find_worktree_for_branch(repo.root, detected_trunk)
|
|
413
|
+
if trunk_wt_path is not None and trunk_wt_path == repo.root:
|
|
414
|
+
# Trunk is in root repository, not in a dedicated worktree
|
|
415
|
+
return "root", False
|
|
416
|
+
else:
|
|
417
|
+
# Trunk has a dedicated worktree
|
|
418
|
+
if trunk_wt_path is None:
|
|
419
|
+
# Auto-create worktree for trunk branch
|
|
420
|
+
_worktree_path, was_created = ensure_worktree_for_branch(ctx, repo, parent_branch)
|
|
421
|
+
return parent_branch, was_created
|
|
422
|
+
return parent_branch, False
|
|
423
|
+
else:
|
|
424
|
+
# Parent is not trunk, check if it has a worktree
|
|
425
|
+
target_wt_path = ctx.git.find_worktree_for_branch(repo.root, parent_branch)
|
|
426
|
+
if target_wt_path is None:
|
|
427
|
+
# Auto-create worktree for parent branch
|
|
428
|
+
_worktree_path, was_created = ensure_worktree_for_branch(ctx, repo, parent_branch)
|
|
429
|
+
return parent_branch, was_created
|
|
430
|
+
return parent_branch, False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Objective management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.alias import register_with_aliases
|
|
6
|
+
from erk.cli.commands.objective.list_cmd import list_objectives
|
|
7
|
+
from erk.cli.help_formatter import ErkCommandGroup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group("objective", cls=ErkCommandGroup, hidden=True)
|
|
11
|
+
def objective_group() -> None:
|
|
12
|
+
"""Manage objectives (multi-PR coordination issues)."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
register_with_aliases(objective_group, list_objectives)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""List open objectives."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from erk.cli.alias import alias
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk.core.display_utils import format_relative_time
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@alias("ls")
|
|
14
|
+
@click.command("list")
|
|
15
|
+
@click.pass_obj
|
|
16
|
+
def list_objectives(ctx: ErkContext) -> None:
|
|
17
|
+
"""List open objectives (GitHub issues with erk-objective label)."""
|
|
18
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
19
|
+
|
|
20
|
+
# Fetch objectives via issues interface
|
|
21
|
+
issues = ctx.issues.list_issues(
|
|
22
|
+
repo.root,
|
|
23
|
+
labels=["erk-objective"],
|
|
24
|
+
state="open",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if not issues:
|
|
28
|
+
click.echo("No open objectives found.", err=True)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Build Rich table with minimal columns
|
|
32
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
33
|
+
table.add_column("#", style="cyan", no_wrap=True)
|
|
34
|
+
table.add_column("title", no_wrap=False)
|
|
35
|
+
table.add_column("created", no_wrap=True)
|
|
36
|
+
table.add_column("url", no_wrap=True)
|
|
37
|
+
|
|
38
|
+
for issue in issues:
|
|
39
|
+
table.add_row(
|
|
40
|
+
f"[link={issue.url}]#{issue.number}[/link]",
|
|
41
|
+
issue.title,
|
|
42
|
+
format_relative_time(issue.created_at.isoformat()),
|
|
43
|
+
issue.url,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console = Console(stderr=True, force_terminal=True)
|
|
47
|
+
console.print(table)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Shared helpers for objective tracking in land commands.
|
|
2
|
+
|
|
3
|
+
These helpers are used by `erk land` to check for linked objectives
|
|
4
|
+
and prompt users to update them after landing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from erk.cli.output import stream_command_with_feedback
|
|
12
|
+
from erk.core.context import ErkContext
|
|
13
|
+
from erk_shared.github.metadata.plan_header import extract_plan_header_objective_issue
|
|
14
|
+
from erk_shared.naming import extract_leading_issue_number
|
|
15
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_and_display_plan_issue_closure(
|
|
19
|
+
ctx: ErkContext,
|
|
20
|
+
repo_root: Path,
|
|
21
|
+
branch: str,
|
|
22
|
+
) -> int | None:
|
|
23
|
+
"""Check and display plan issue closure status after landing.
|
|
24
|
+
|
|
25
|
+
Returns the plan issue number if found, None otherwise.
|
|
26
|
+
This is fail-open: returns None silently if the issue doesn't exist.
|
|
27
|
+
"""
|
|
28
|
+
plan_number = extract_leading_issue_number(branch)
|
|
29
|
+
if plan_number is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
# GitHubIssues.get_issue raises RuntimeError for missing issues.
|
|
33
|
+
# This is a fail-open feature (non-critical), so we catch and return None.
|
|
34
|
+
try:
|
|
35
|
+
issue = ctx.issues.get_issue(repo_root, plan_number)
|
|
36
|
+
except RuntimeError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if issue.state == "CLOSED":
|
|
40
|
+
user_output(click.style("✓", fg="green") + f" Closed plan issue #{plan_number}")
|
|
41
|
+
else:
|
|
42
|
+
user_output(
|
|
43
|
+
click.style("⚠ ", fg="yellow")
|
|
44
|
+
+ f"Plan issue #{plan_number} still open (expected auto-close)"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return plan_number
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_objective_for_branch(ctx: ErkContext, repo_root: Path, branch: str) -> int | None:
|
|
51
|
+
"""Extract objective issue number from branch's linked plan issue.
|
|
52
|
+
|
|
53
|
+
Returns objective issue number if:
|
|
54
|
+
1. Branch has P<number>- prefix (plan issue link)
|
|
55
|
+
2. Plan issue has objective_issue in its metadata
|
|
56
|
+
|
|
57
|
+
Returns None otherwise (fail-open - never blocks landing).
|
|
58
|
+
"""
|
|
59
|
+
plan_number = extract_leading_issue_number(branch)
|
|
60
|
+
if plan_number is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# GitHubIssues.get_issue raises RuntimeError for missing issues.
|
|
64
|
+
# This is a fail-open feature (non-critical), so we catch and return None.
|
|
65
|
+
try:
|
|
66
|
+
issue = ctx.issues.get_issue(repo_root, plan_number)
|
|
67
|
+
except RuntimeError:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
return extract_plan_header_objective_issue(issue.body)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def prompt_objective_update(
|
|
74
|
+
ctx: ErkContext,
|
|
75
|
+
repo_root: Path,
|
|
76
|
+
objective_number: int,
|
|
77
|
+
pr_number: int,
|
|
78
|
+
branch: str,
|
|
79
|
+
force: bool,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Prompt user to update objective after landing.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
ctx: ErkContext with claude_executor
|
|
85
|
+
repo_root: Repository root path for Claude execution
|
|
86
|
+
objective_number: The linked objective issue number
|
|
87
|
+
pr_number: The PR number that was just landed
|
|
88
|
+
branch: The branch name that was landed
|
|
89
|
+
force: If True, skip prompt (print command to run later)
|
|
90
|
+
"""
|
|
91
|
+
user_output(f" Linked to Objective #{objective_number}")
|
|
92
|
+
|
|
93
|
+
# Build the command with all arguments for context-free execution
|
|
94
|
+
# --auto-close enables automatic objective closing when all steps are complete
|
|
95
|
+
cmd = (
|
|
96
|
+
f"/erk:objective-update-with-landed-pr "
|
|
97
|
+
f"--pr {pr_number} --objective {objective_number} --branch {branch} --auto-close"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if force:
|
|
101
|
+
# --force skips all prompts, print command for later
|
|
102
|
+
user_output(f" Run '{cmd}' to update objective")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Ask y/n prompt
|
|
106
|
+
user_output("")
|
|
107
|
+
if not user_confirm("Update objective now? (runs Claude agent)", default=True):
|
|
108
|
+
user_output("")
|
|
109
|
+
user_output("Skipped. To update later, run:")
|
|
110
|
+
user_output(f" {cmd}")
|
|
111
|
+
else:
|
|
112
|
+
# Add feedback BEFORE streaming starts (important for visibility)
|
|
113
|
+
user_output("")
|
|
114
|
+
user_output("Starting objective update...")
|
|
115
|
+
|
|
116
|
+
result = stream_command_with_feedback(
|
|
117
|
+
executor=ctx.claude_executor,
|
|
118
|
+
command=cmd,
|
|
119
|
+
worktree_path=repo_root,
|
|
120
|
+
dangerous=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Add feedback AFTER streaming completes
|
|
124
|
+
if result.success:
|
|
125
|
+
user_output("")
|
|
126
|
+
user_output(click.style("✓", fg="green") + " Objective updated successfully")
|
|
127
|
+
else:
|
|
128
|
+
user_output("")
|
|
129
|
+
user_output(
|
|
130
|
+
click.style("⚠", fg="yellow") + f" Objective update failed: {result.error_message}"
|
|
131
|
+
)
|
|
132
|
+
user_output(" Run '/erk:objective-update-with-landed-pr' manually to retry")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Plan command group."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.plan.check_cmd import check_plan
|
|
6
|
+
from erk.cli.commands.plan.close_cmd import close_plan
|
|
7
|
+
from erk.cli.commands.plan.create_cmd import create_plan
|
|
8
|
+
from erk.cli.commands.plan.docs import docs_group
|
|
9
|
+
from erk.cli.commands.plan.extraction import extraction_group
|
|
10
|
+
from erk.cli.commands.plan.get import get_plan
|
|
11
|
+
from erk.cli.commands.plan.list_cmd import list_plans
|
|
12
|
+
from erk.cli.commands.plan.log_cmd import plan_log
|
|
13
|
+
from erk.cli.commands.plan.start_cmd import plan_start
|
|
14
|
+
from erk.cli.commands.submit import submit_cmd
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group("plan")
|
|
18
|
+
def plan_group() -> None:
|
|
19
|
+
"""Manage implementation plans."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
plan_group.add_command(check_plan)
|
|
24
|
+
plan_group.add_command(close_plan)
|
|
25
|
+
plan_group.add_command(create_plan, name="create")
|
|
26
|
+
plan_group.add_command(docs_group)
|
|
27
|
+
plan_group.add_command(extraction_group)
|
|
28
|
+
plan_group.add_command(get_plan)
|
|
29
|
+
plan_group.add_command(list_plans, name="list")
|
|
30
|
+
plan_group.add_command(plan_log, name="log")
|
|
31
|
+
plan_group.add_command(plan_start, name="start")
|
|
32
|
+
plan_group.add_command(submit_cmd, name="submit")
|