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,649 @@
|
|
|
1
|
+
"""Shared utilities for implement commands.
|
|
2
|
+
|
|
3
|
+
This module contains the common logic for erk implement - worktree-based implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import NamedTuple, TypeVar
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from erk.cli.activation import render_activation_script
|
|
16
|
+
from erk.cli.help_formatter import script_option
|
|
17
|
+
from erk.core.claude_executor import ClaudeExecutor
|
|
18
|
+
from erk.core.context import ErkContext
|
|
19
|
+
from erk.core.worktree_utils import compute_relative_path_in_worktree
|
|
20
|
+
from erk_shared.issue_workflow import (
|
|
21
|
+
IssueBranchSetup,
|
|
22
|
+
IssueValidationFailed,
|
|
23
|
+
prepare_plan_for_worktree,
|
|
24
|
+
)
|
|
25
|
+
from erk_shared.naming import (
|
|
26
|
+
sanitize_worktree_name,
|
|
27
|
+
strip_plan_from_filename,
|
|
28
|
+
)
|
|
29
|
+
from erk_shared.output.output import user_output
|
|
30
|
+
|
|
31
|
+
# Valid model names and their aliases
|
|
32
|
+
_MODEL_ALIASES: dict[str, str] = {
|
|
33
|
+
"h": "haiku",
|
|
34
|
+
"s": "sonnet",
|
|
35
|
+
"o": "opus",
|
|
36
|
+
}
|
|
37
|
+
_VALID_MODELS = {"haiku", "sonnet", "opus"}
|
|
38
|
+
|
|
39
|
+
F = TypeVar("F", bound=Callable[..., object])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def implement_common_options(fn: F) -> F:
|
|
43
|
+
"""Decorator that applies common options shared between implement commands.
|
|
44
|
+
|
|
45
|
+
This decorator applies the following options (in order from top to bottom in help):
|
|
46
|
+
- --dry-run: Print what would be executed without doing it
|
|
47
|
+
- --submit: Automatically run CI validation and submit PR
|
|
48
|
+
- --dangerous: Skip permission prompts
|
|
49
|
+
- --no-interactive: Execute commands via subprocess
|
|
50
|
+
- --script: Output shell script for integration (hidden)
|
|
51
|
+
- --yolo: Equivalent to --dangerous --submit --no-interactive
|
|
52
|
+
- --verbose: Show full Claude Code output
|
|
53
|
+
- -m/--model: Model to use for Claude
|
|
54
|
+
|
|
55
|
+
Each command using this decorator must also define its own --force option
|
|
56
|
+
since the behavior differs (worktree deletion vs pool slot unassignment).
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
@click.command("implement", cls=CommandWithHiddenOptions)
|
|
60
|
+
@click.argument("target")
|
|
61
|
+
@implement_common_options
|
|
62
|
+
@click.option("-f", "--force", ...) # Command-specific force behavior
|
|
63
|
+
@click.pass_obj
|
|
64
|
+
def implement(ctx, target, dry_run, submit, dangerous, ...):
|
|
65
|
+
...
|
|
66
|
+
"""
|
|
67
|
+
# Apply options in reverse order (Click decorators are applied bottom-up)
|
|
68
|
+
# This results in options appearing in this order in --help
|
|
69
|
+
fn = click.option(
|
|
70
|
+
"-m",
|
|
71
|
+
"--model",
|
|
72
|
+
type=str,
|
|
73
|
+
default=None,
|
|
74
|
+
help="Model to use for Claude (haiku/h, sonnet/s, opus/o)",
|
|
75
|
+
)(fn)
|
|
76
|
+
fn = click.option(
|
|
77
|
+
"--verbose",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
default=False,
|
|
80
|
+
help="Show full Claude Code output (default: filtered)",
|
|
81
|
+
)(fn)
|
|
82
|
+
fn = click.option(
|
|
83
|
+
"--yolo",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
default=False,
|
|
86
|
+
help="Equivalent to --dangerous --submit --no-interactive (full automation)",
|
|
87
|
+
)(fn)
|
|
88
|
+
fn = script_option(fn)
|
|
89
|
+
fn = click.option(
|
|
90
|
+
"--no-interactive",
|
|
91
|
+
is_flag=True,
|
|
92
|
+
default=False,
|
|
93
|
+
help="Execute commands via subprocess without user interaction",
|
|
94
|
+
)(fn)
|
|
95
|
+
fn = click.option(
|
|
96
|
+
"--dangerous",
|
|
97
|
+
is_flag=True,
|
|
98
|
+
default=False,
|
|
99
|
+
help="Skip permission prompts by passing --dangerously-skip-permissions to Claude",
|
|
100
|
+
)(fn)
|
|
101
|
+
fn = click.option(
|
|
102
|
+
"--submit",
|
|
103
|
+
is_flag=True,
|
|
104
|
+
help="Automatically run CI validation and submit PR after implementation",
|
|
105
|
+
)(fn)
|
|
106
|
+
fn = click.option(
|
|
107
|
+
"--dry-run",
|
|
108
|
+
is_flag=True,
|
|
109
|
+
help="Print what would be executed without doing it",
|
|
110
|
+
)(fn)
|
|
111
|
+
return fn
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def normalize_model_name(model: str | None) -> str | None:
|
|
115
|
+
"""Normalize model name, expanding aliases and validating.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
model: User-provided model name or alias (haiku, sonnet, opus, h, s, o, or None)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Normalized full model name (haiku, sonnet, opus) or None if not provided
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
click.ClickException: If model name is invalid
|
|
125
|
+
"""
|
|
126
|
+
if model is None:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Expand alias if present
|
|
130
|
+
normalized = _MODEL_ALIASES.get(model.lower(), model.lower())
|
|
131
|
+
|
|
132
|
+
if normalized not in _VALID_MODELS:
|
|
133
|
+
valid_options = ", ".join(sorted(_VALID_MODELS | set(_MODEL_ALIASES.keys())))
|
|
134
|
+
raise click.ClickException(f"Invalid model: '{model}'\nValid options: {valid_options}")
|
|
135
|
+
|
|
136
|
+
return normalized
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def determine_base_branch(ctx: ErkContext, repo_root: Path) -> str:
|
|
140
|
+
"""Determine the base branch for new worktree creation.
|
|
141
|
+
|
|
142
|
+
When Graphite is enabled and the user is on a non-trunk branch,
|
|
143
|
+
stack on the current branch. Otherwise, use trunk.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
ctx: Erk context
|
|
147
|
+
repo_root: Repository root path
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Base branch name to use as ref for worktree creation
|
|
151
|
+
"""
|
|
152
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo_root)
|
|
153
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
154
|
+
|
|
155
|
+
if not use_graphite:
|
|
156
|
+
return trunk_branch
|
|
157
|
+
|
|
158
|
+
current_branch = ctx.git.get_current_branch(ctx.cwd)
|
|
159
|
+
if current_branch and current_branch != trunk_branch:
|
|
160
|
+
return current_branch
|
|
161
|
+
|
|
162
|
+
return trunk_branch
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def validate_flags(submit: bool, no_interactive: bool, script: bool) -> None:
|
|
166
|
+
"""Validate flag combinations and raise ClickException if invalid.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
submit: Whether to auto-submit PR after implementation
|
|
170
|
+
no_interactive: Whether to execute non-interactively
|
|
171
|
+
script: Whether to output shell integration script
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
click.ClickException: If flag combination is invalid
|
|
175
|
+
"""
|
|
176
|
+
# --submit requires --no-interactive UNLESS using --script mode
|
|
177
|
+
# Script mode generates shell code, so --submit is allowed
|
|
178
|
+
if submit and not no_interactive and not script:
|
|
179
|
+
raise click.ClickException(
|
|
180
|
+
"--submit requires --no-interactive\n"
|
|
181
|
+
"Automated workflows must run non-interactively\n"
|
|
182
|
+
"(or use --script to generate shell integration code)"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if no_interactive and script:
|
|
186
|
+
raise click.ClickException(
|
|
187
|
+
"--no-interactive and --script are mutually exclusive\n"
|
|
188
|
+
"--script generates shell integration code for manual execution\n"
|
|
189
|
+
"--no-interactive executes commands programmatically"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def build_command_sequence(submit: bool) -> list[str]:
|
|
194
|
+
"""Build list of slash commands to execute.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
submit: Whether to include full CI/PR workflow
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of slash commands to execute in sequence
|
|
201
|
+
"""
|
|
202
|
+
commands = ["/erk:plan-implement"]
|
|
203
|
+
if submit:
|
|
204
|
+
commands.extend(["/fast-ci", "/gt:pr-submit"])
|
|
205
|
+
return commands
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_claude_args(slash_command: str, dangerous: bool, model: str | None) -> list[str]:
|
|
209
|
+
"""Build Claude command argument list for interactive script mode.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
slash_command: The slash command to execute
|
|
213
|
+
dangerous: Whether to skip permission prompts
|
|
214
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of command arguments suitable for subprocess
|
|
218
|
+
"""
|
|
219
|
+
args = ["claude", "--permission-mode", "acceptEdits"]
|
|
220
|
+
if dangerous:
|
|
221
|
+
args.append("--dangerously-skip-permissions")
|
|
222
|
+
if model is not None:
|
|
223
|
+
args.extend(["--model", model])
|
|
224
|
+
args.append(slash_command)
|
|
225
|
+
return args
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def build_claude_command(slash_command: str, dangerous: bool, model: str | None) -> str:
|
|
229
|
+
"""Build a Claude CLI invocation for interactive mode.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
slash_command: The slash command to execute (e.g., "/erk:plan-implement")
|
|
233
|
+
dangerous: Whether to skip permission prompts
|
|
234
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Complete Claude CLI command string
|
|
238
|
+
"""
|
|
239
|
+
cmd = "claude --permission-mode acceptEdits"
|
|
240
|
+
if dangerous:
|
|
241
|
+
cmd += " --dangerously-skip-permissions"
|
|
242
|
+
if model is not None:
|
|
243
|
+
cmd += f" --model {model}"
|
|
244
|
+
cmd += f' "{slash_command}"'
|
|
245
|
+
return cmd
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def execute_interactive_mode(
|
|
249
|
+
ctx: ErkContext,
|
|
250
|
+
repo_root: Path,
|
|
251
|
+
worktree_path: Path,
|
|
252
|
+
dangerous: bool,
|
|
253
|
+
model: str | None,
|
|
254
|
+
executor: ClaudeExecutor,
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Execute implementation in interactive mode using executor.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
ctx: Erk context for accessing git and current working directory
|
|
260
|
+
repo_root: Path to repository root for listing worktrees
|
|
261
|
+
worktree_path: Path to worktree directory
|
|
262
|
+
dangerous: Whether to skip permission prompts
|
|
263
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
264
|
+
executor: Claude CLI executor for process replacement
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
click.ClickException: If Claude CLI not found
|
|
268
|
+
|
|
269
|
+
Note:
|
|
270
|
+
This function never returns in production - the process is replaced by Claude
|
|
271
|
+
"""
|
|
272
|
+
click.echo("Entering interactive implementation mode...", err=True)
|
|
273
|
+
try:
|
|
274
|
+
executor.execute_interactive(
|
|
275
|
+
worktree_path,
|
|
276
|
+
dangerous,
|
|
277
|
+
"/erk:plan-implement",
|
|
278
|
+
compute_relative_path_in_worktree(ctx.git.list_worktrees(repo_root), ctx.cwd),
|
|
279
|
+
model=model,
|
|
280
|
+
)
|
|
281
|
+
except RuntimeError as e:
|
|
282
|
+
raise click.ClickException(str(e)) from e
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def execute_non_interactive_mode(
|
|
286
|
+
*,
|
|
287
|
+
worktree_path: Path,
|
|
288
|
+
commands: list[str],
|
|
289
|
+
dangerous: bool,
|
|
290
|
+
verbose: bool,
|
|
291
|
+
model: str | None,
|
|
292
|
+
executor: ClaudeExecutor,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Execute commands via Claude CLI executor with rich output formatting.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
worktree_path: Path to worktree directory
|
|
298
|
+
commands: List of slash commands to execute
|
|
299
|
+
dangerous: Whether to skip permission prompts
|
|
300
|
+
verbose: Whether to show raw output (True) or filtered output (False)
|
|
301
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
302
|
+
executor: Claude CLI executor for command execution
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
click.ClickException: If Claude CLI not found or command fails
|
|
306
|
+
"""
|
|
307
|
+
import time
|
|
308
|
+
|
|
309
|
+
from rich.console import Console
|
|
310
|
+
|
|
311
|
+
from erk.cli.output import format_implement_summary, stream_command_with_feedback
|
|
312
|
+
from erk.core.claude_executor import CommandResult
|
|
313
|
+
|
|
314
|
+
# Verify Claude is available
|
|
315
|
+
if not executor.is_claude_available():
|
|
316
|
+
raise click.ClickException(
|
|
317
|
+
"Claude CLI not found\nInstall from: https://claude.com/download"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
console = Console()
|
|
321
|
+
total_start = time.time()
|
|
322
|
+
all_results: list[CommandResult] = []
|
|
323
|
+
|
|
324
|
+
for cmd in commands:
|
|
325
|
+
if verbose:
|
|
326
|
+
# Verbose mode - simple output, no spinner
|
|
327
|
+
click.echo(f"Running {cmd}...", err=True)
|
|
328
|
+
result = executor.execute_command(
|
|
329
|
+
cmd, worktree_path, dangerous, verbose=True, model=model
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
# Filtered mode - streaming with live print-based feedback
|
|
333
|
+
result = stream_command_with_feedback(
|
|
334
|
+
executor=executor,
|
|
335
|
+
command=cmd,
|
|
336
|
+
worktree_path=worktree_path,
|
|
337
|
+
dangerous=dangerous,
|
|
338
|
+
model=model,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
all_results.append(result)
|
|
342
|
+
|
|
343
|
+
# Stop on first failure
|
|
344
|
+
if not result.success:
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
# Show final summary (unless verbose mode)
|
|
348
|
+
if not verbose:
|
|
349
|
+
total_duration = time.time() - total_start
|
|
350
|
+
summary = format_implement_summary(all_results, total_duration)
|
|
351
|
+
console.print(summary)
|
|
352
|
+
|
|
353
|
+
# Raise exception if any command failed
|
|
354
|
+
if not all(r.success for r in all_results):
|
|
355
|
+
raise click.ClickException("One or more commands failed")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def build_activation_script_with_commands(
|
|
359
|
+
worktree_path: Path, commands: list[str], dangerous: bool, model: str | None
|
|
360
|
+
) -> str:
|
|
361
|
+
"""Build activation script with Claude commands.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
worktree_path: Path to worktree
|
|
365
|
+
commands: List of slash commands to include
|
|
366
|
+
dangerous: Whether to skip permission prompts
|
|
367
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Complete activation script with commands
|
|
371
|
+
"""
|
|
372
|
+
# Get base activation script (cd + venv + env)
|
|
373
|
+
script = render_activation_script(
|
|
374
|
+
worktree_path=worktree_path,
|
|
375
|
+
target_subpath=None,
|
|
376
|
+
post_cd_commands=None,
|
|
377
|
+
final_message="", # We'll add commands instead
|
|
378
|
+
comment="implement activation",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Add Claude commands
|
|
382
|
+
shell_commands = []
|
|
383
|
+
for cmd in commands:
|
|
384
|
+
cmd_args = build_claude_args(cmd, dangerous, model)
|
|
385
|
+
# Build shell command string
|
|
386
|
+
shell_cmd = " ".join(shlex.quote(arg) for arg in cmd_args)
|
|
387
|
+
shell_commands.append(shell_cmd)
|
|
388
|
+
|
|
389
|
+
# Chain commands with && so they only run if previous command succeeded
|
|
390
|
+
script += " && \\\n".join(shell_commands) + "\n"
|
|
391
|
+
|
|
392
|
+
return script
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class TargetInfo(NamedTuple):
|
|
396
|
+
"""Information about detected target type.
|
|
397
|
+
|
|
398
|
+
Attributes:
|
|
399
|
+
target_type: Type of target - "issue_number", "issue_url", or "file_path"
|
|
400
|
+
issue_number: Extracted issue number for GitHub targets, None for file paths
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
target_type: str
|
|
404
|
+
issue_number: str | None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def detect_target_type(target: str) -> TargetInfo:
|
|
408
|
+
"""Detect whether target is an issue number, issue URL, or file path.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
target: User-provided target argument
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
TargetInfo with target type and extracted issue number (if applicable)
|
|
415
|
+
"""
|
|
416
|
+
# Check if starts with # followed by digits (issue number)
|
|
417
|
+
if target.startswith("#") and target[1:].isdigit():
|
|
418
|
+
return TargetInfo(target_type="issue_number", issue_number=target[1:])
|
|
419
|
+
|
|
420
|
+
# Check if GitHub issue URL
|
|
421
|
+
github_issue_pattern = r"github\.com/[^/]+/[^/]+/issues/(\d+)"
|
|
422
|
+
match = re.search(github_issue_pattern, target)
|
|
423
|
+
if match:
|
|
424
|
+
issue_number = match.group(1)
|
|
425
|
+
return TargetInfo(target_type="issue_url", issue_number=issue_number)
|
|
426
|
+
|
|
427
|
+
# Check if plain digits (issue number without # prefix)
|
|
428
|
+
if target.isdigit():
|
|
429
|
+
return TargetInfo(target_type="issue_number", issue_number=target)
|
|
430
|
+
|
|
431
|
+
# Otherwise, treat as file path
|
|
432
|
+
return TargetInfo(target_type="file_path", issue_number=None)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True)
|
|
436
|
+
class PlanSource:
|
|
437
|
+
"""Source information for creating a worktree with plan.
|
|
438
|
+
|
|
439
|
+
Attributes:
|
|
440
|
+
plan_content: The plan content as a string
|
|
441
|
+
base_name: Base name for generating worktree name
|
|
442
|
+
dry_run_description: Description to show in dry-run mode
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
plan_content: str
|
|
446
|
+
base_name: str
|
|
447
|
+
dry_run_description: str
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@dataclass(frozen=True)
|
|
451
|
+
class IssuePlanSource:
|
|
452
|
+
"""Extended plan source with issue-specific metadata.
|
|
453
|
+
|
|
454
|
+
Attributes:
|
|
455
|
+
plan_source: The base PlanSource with content and metadata
|
|
456
|
+
branch_name: The development branch name for this issue
|
|
457
|
+
already_existed: Whether the branch already existed
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
plan_source: PlanSource
|
|
461
|
+
branch_name: str
|
|
462
|
+
already_existed: bool
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def prepare_plan_source_from_issue(
|
|
466
|
+
ctx: ErkContext, repo_root: Path, issue_number: str, base_branch: str
|
|
467
|
+
) -> IssuePlanSource:
|
|
468
|
+
"""Prepare plan source from GitHub issue.
|
|
469
|
+
|
|
470
|
+
Creates a branch for the issue and fetches plan content.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
ctx: Erk context
|
|
474
|
+
repo_root: Repository root path
|
|
475
|
+
issue_number: GitHub issue number
|
|
476
|
+
base_branch: Base branch for creating the development branch
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
IssuePlanSource with plan content, metadata, and branch name
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
SystemExit: If issue not found or doesn't have erk-plan label
|
|
483
|
+
"""
|
|
484
|
+
# Output fetching diagnostic
|
|
485
|
+
ctx.feedback.info("Fetching issue from GitHub...")
|
|
486
|
+
|
|
487
|
+
# Fetch plan from GitHub
|
|
488
|
+
try:
|
|
489
|
+
plan = ctx.plan_store.get_plan(repo_root, issue_number)
|
|
490
|
+
except RuntimeError as e:
|
|
491
|
+
ctx.feedback.error(f"Error: {e}")
|
|
492
|
+
raise SystemExit(1) from e
|
|
493
|
+
|
|
494
|
+
# Output issue title
|
|
495
|
+
ctx.feedback.info(f"Issue: {plan.title}")
|
|
496
|
+
|
|
497
|
+
# Prepare and validate using shared helper (returns union type)
|
|
498
|
+
result = prepare_plan_for_worktree(plan, ctx.time.now())
|
|
499
|
+
|
|
500
|
+
if isinstance(result, IssueValidationFailed):
|
|
501
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
502
|
+
raise SystemExit(1) from None
|
|
503
|
+
|
|
504
|
+
setup: IssueBranchSetup = result
|
|
505
|
+
for warning in setup.warnings:
|
|
506
|
+
user_output(click.style("Warning: ", fg="yellow") + warning)
|
|
507
|
+
|
|
508
|
+
dry_run_desc = f"Would create worktree from issue #{issue_number}\n Title: {plan.title}"
|
|
509
|
+
|
|
510
|
+
plan_source = PlanSource(
|
|
511
|
+
plan_content=setup.plan_content,
|
|
512
|
+
base_name=setup.worktree_name,
|
|
513
|
+
dry_run_description=dry_run_desc,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Check if the branch already exists locally
|
|
517
|
+
local_branches = ctx.git.list_local_branches(repo_root)
|
|
518
|
+
branch_already_exists = setup.branch_name in local_branches
|
|
519
|
+
|
|
520
|
+
return IssuePlanSource(
|
|
521
|
+
plan_source=plan_source,
|
|
522
|
+
branch_name=setup.branch_name,
|
|
523
|
+
already_existed=branch_already_exists,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def prepare_plan_source_from_file(ctx: ErkContext, plan_file: Path) -> PlanSource:
|
|
528
|
+
"""Prepare plan source from file.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
ctx: Erk context
|
|
532
|
+
plan_file: Path to plan file
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
PlanSource with plan content and metadata
|
|
536
|
+
|
|
537
|
+
Raises:
|
|
538
|
+
SystemExit: If plan file doesn't exist
|
|
539
|
+
"""
|
|
540
|
+
# Validate plan file exists
|
|
541
|
+
if not plan_file.exists():
|
|
542
|
+
ctx.feedback.error(f"Error: Plan file not found: {plan_file}")
|
|
543
|
+
raise SystemExit(1) from None
|
|
544
|
+
|
|
545
|
+
# Output reading diagnostic
|
|
546
|
+
ctx.feedback.info("Reading plan file...")
|
|
547
|
+
|
|
548
|
+
# Read plan content
|
|
549
|
+
plan_content = plan_file.read_text(encoding="utf-8")
|
|
550
|
+
|
|
551
|
+
# Extract title from plan content for display
|
|
552
|
+
title = plan_file.stem
|
|
553
|
+
for line in plan_content.split("\n"):
|
|
554
|
+
stripped = line.strip()
|
|
555
|
+
if stripped.startswith("#"):
|
|
556
|
+
# Extract title from first heading
|
|
557
|
+
title = stripped.lstrip("#").strip()
|
|
558
|
+
break
|
|
559
|
+
|
|
560
|
+
# Output plan title
|
|
561
|
+
ctx.feedback.info(f"Plan: {title}")
|
|
562
|
+
|
|
563
|
+
# Derive base name from filename
|
|
564
|
+
plan_stem = plan_file.stem
|
|
565
|
+
cleaned_stem = strip_plan_from_filename(plan_stem)
|
|
566
|
+
base_name = sanitize_worktree_name(cleaned_stem)
|
|
567
|
+
|
|
568
|
+
dry_run_desc = (
|
|
569
|
+
f"Would create worktree from plan file: {plan_file}\n"
|
|
570
|
+
f" Plan file would be deleted: {plan_file}"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return PlanSource(
|
|
574
|
+
plan_content=plan_content,
|
|
575
|
+
base_name=base_name,
|
|
576
|
+
dry_run_description=dry_run_desc,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def output_activation_instructions(
|
|
581
|
+
ctx: ErkContext,
|
|
582
|
+
*,
|
|
583
|
+
wt_path: Path,
|
|
584
|
+
branch: str,
|
|
585
|
+
script: bool,
|
|
586
|
+
submit: bool,
|
|
587
|
+
dangerous: bool,
|
|
588
|
+
model: str | None,
|
|
589
|
+
target_description: str,
|
|
590
|
+
) -> None:
|
|
591
|
+
"""Output activation script or manual instructions.
|
|
592
|
+
|
|
593
|
+
This is only called when in script mode (for manual shell integration).
|
|
594
|
+
Interactive and non-interactive modes handle execution directly.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
ctx: Erk context
|
|
598
|
+
wt_path: Worktree path
|
|
599
|
+
branch: Branch name
|
|
600
|
+
script: Whether to output activation script
|
|
601
|
+
submit: Whether to auto-submit PR after implementation
|
|
602
|
+
dangerous: Whether to skip permission prompts
|
|
603
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
604
|
+
target_description: Description of target for user messages
|
|
605
|
+
"""
|
|
606
|
+
if script:
|
|
607
|
+
# Build command sequence
|
|
608
|
+
commands = build_command_sequence(submit)
|
|
609
|
+
|
|
610
|
+
# Generate activation script with commands
|
|
611
|
+
full_script = build_activation_script_with_commands(wt_path, commands, dangerous, model)
|
|
612
|
+
|
|
613
|
+
comment_suffix = "implement, CI, and submit" if submit else "implement"
|
|
614
|
+
result = ctx.script_writer.write_activation_script(
|
|
615
|
+
full_script,
|
|
616
|
+
command_name="implement",
|
|
617
|
+
comment=f"activate {wt_path.name} and {comment_suffix}",
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
result.output_for_shell_integration()
|
|
621
|
+
else:
|
|
622
|
+
# Provide manual instructions
|
|
623
|
+
user_output("\n" + click.style("Next steps:", fg="cyan", bold=True))
|
|
624
|
+
user_output(f" 1. Change to worktree: erk br co {branch}")
|
|
625
|
+
if submit:
|
|
626
|
+
user_output(" 2. Run implementation, CI, and submit PR:")
|
|
627
|
+
user_output(f" {build_claude_command('/erk:plan-implement', dangerous, model)}")
|
|
628
|
+
user_output(f" {build_claude_command('/fast-ci', dangerous, model)}")
|
|
629
|
+
user_output(f" {build_claude_command('/gt:pr-submit', dangerous, model)}")
|
|
630
|
+
else:
|
|
631
|
+
claude_cmd = build_claude_command("/erk:plan-implement", dangerous, model)
|
|
632
|
+
user_output(f" 2. Run implementation: {claude_cmd}")
|
|
633
|
+
user_output("\n" + click.style("Shell integration not detected.", fg="yellow"))
|
|
634
|
+
user_output("To activate environment and run commands, use:")
|
|
635
|
+
script_flag = "--submit --script" if submit else "--script"
|
|
636
|
+
user_output(f" source <(erk implement {target_description} {script_flag})")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@dataclass(frozen=True)
|
|
640
|
+
class WorktreeCreationResult:
|
|
641
|
+
"""Result of creating a worktree with plan content.
|
|
642
|
+
|
|
643
|
+
Attributes:
|
|
644
|
+
worktree_path: Path to the created worktree root
|
|
645
|
+
impl_dir: Path to the .impl/ directory (always at worktree root)
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
worktree_path: Path
|
|
649
|
+
impl_dir: Path
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Info command group for viewing information about erk."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.info.release_notes_cmd import release_notes_cmd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group("info")
|
|
9
|
+
def info_group() -> None:
|
|
10
|
+
"""View information about erk."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
info_group.add_command(release_notes_cmd)
|