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,695 @@
|
|
|
1
|
+
"""Command to implement features from GitHub issues or plan files.
|
|
2
|
+
|
|
3
|
+
This unified command provides two modes:
|
|
4
|
+
- GitHub issue mode: erk implement 123 or erk implement <URL>
|
|
5
|
+
- Plan file mode: erk implement path/to/plan.md
|
|
6
|
+
|
|
7
|
+
Both modes assign a pool slot and invoke Claude for implementation.
|
|
8
|
+
Can be run from any location, including from within pool slots.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from erk.cli.alias import alias
|
|
17
|
+
from erk.cli.commands.completions import complete_plan_files
|
|
18
|
+
from erk.cli.commands.implement_shared import (
|
|
19
|
+
PlanSource,
|
|
20
|
+
WorktreeCreationResult,
|
|
21
|
+
build_claude_args,
|
|
22
|
+
build_command_sequence,
|
|
23
|
+
detect_target_type,
|
|
24
|
+
determine_base_branch,
|
|
25
|
+
execute_interactive_mode,
|
|
26
|
+
execute_non_interactive_mode,
|
|
27
|
+
implement_common_options,
|
|
28
|
+
normalize_model_name,
|
|
29
|
+
output_activation_instructions,
|
|
30
|
+
prepare_plan_source_from_file,
|
|
31
|
+
prepare_plan_source_from_issue,
|
|
32
|
+
validate_flags,
|
|
33
|
+
)
|
|
34
|
+
from erk.cli.commands.slot.common import (
|
|
35
|
+
find_branch_assignment,
|
|
36
|
+
find_inactive_slot,
|
|
37
|
+
find_next_available_slot,
|
|
38
|
+
generate_slot_name,
|
|
39
|
+
get_pool_size,
|
|
40
|
+
handle_pool_full_interactive,
|
|
41
|
+
)
|
|
42
|
+
from erk.cli.commands.wt.create_cmd import run_post_worktree_setup
|
|
43
|
+
from erk.cli.config import LoadedConfig
|
|
44
|
+
from erk.cli.core import discover_repo_context
|
|
45
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions
|
|
46
|
+
from erk.core.claude_executor import ClaudeExecutor
|
|
47
|
+
from erk.core.context import ErkContext
|
|
48
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
49
|
+
from erk.core.worktree_pool import (
|
|
50
|
+
PoolState,
|
|
51
|
+
SlotAssignment,
|
|
52
|
+
SlotInfo,
|
|
53
|
+
load_pool_state,
|
|
54
|
+
save_pool_state,
|
|
55
|
+
update_slot_objective,
|
|
56
|
+
)
|
|
57
|
+
from erk_shared.github.metadata.plan_header import extract_plan_header_objective_issue
|
|
58
|
+
from erk_shared.impl_folder import create_impl_folder, save_issue_reference
|
|
59
|
+
from erk_shared.naming import sanitize_worktree_name
|
|
60
|
+
from erk_shared.output.output import user_output
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_worktree_clean_for_checkout(
|
|
64
|
+
ctx: ErkContext,
|
|
65
|
+
wt_path: Path,
|
|
66
|
+
slot_name: str,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Raise ClickException if worktree has uncommitted changes.
|
|
69
|
+
|
|
70
|
+
Checks for uncommitted changes before checkout to provide a friendly error
|
|
71
|
+
message with actionable remediation steps, rather than letting git fail
|
|
72
|
+
with an ugly traceback.
|
|
73
|
+
"""
|
|
74
|
+
if ctx.git.has_uncommitted_changes(wt_path):
|
|
75
|
+
raise click.ClickException(
|
|
76
|
+
f"Slot '{slot_name}' has uncommitted changes that would be overwritten.\n\n"
|
|
77
|
+
f"Remediation options:\n"
|
|
78
|
+
f" 1. cd {wt_path} && git stash\n"
|
|
79
|
+
f" 2. cd {wt_path} && git commit -am 'WIP'\n"
|
|
80
|
+
f" 3. erk slot unassign {slot_name} # discard changes and reset slot"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _create_worktree_with_plan_content(
|
|
85
|
+
ctx: ErkContext,
|
|
86
|
+
*,
|
|
87
|
+
plan_source: PlanSource,
|
|
88
|
+
dry_run: bool,
|
|
89
|
+
submit: bool,
|
|
90
|
+
dangerous: bool,
|
|
91
|
+
no_interactive: bool,
|
|
92
|
+
linked_branch_name: str | None,
|
|
93
|
+
base_branch: str,
|
|
94
|
+
model: str | None,
|
|
95
|
+
force: bool,
|
|
96
|
+
objective_issue: int | None,
|
|
97
|
+
) -> WorktreeCreationResult | None:
|
|
98
|
+
"""Create worktree with plan content using slot assignment.
|
|
99
|
+
|
|
100
|
+
Always assigns a new pool slot for the implementation, even when running
|
|
101
|
+
from within a managed slot. This maximizes parallelism by keeping the
|
|
102
|
+
parent branch assigned to its current slot.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
ctx: Erk context
|
|
106
|
+
plan_source: Plan source with content and metadata
|
|
107
|
+
dry_run: Whether to perform dry run
|
|
108
|
+
submit: Whether to auto-submit PR after implementation
|
|
109
|
+
dangerous: Whether to skip permission prompts
|
|
110
|
+
no_interactive: Whether to execute non-interactively
|
|
111
|
+
linked_branch_name: Optional branch name for issue-based worktrees
|
|
112
|
+
(when provided, use this branch instead of creating new)
|
|
113
|
+
base_branch: Base branch to use as ref for worktree creation
|
|
114
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
115
|
+
force: Whether to auto-unassign oldest slot if pool is full
|
|
116
|
+
objective_issue: Optional objective issue number from plan metadata
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
WorktreeCreationResult with paths, or None if dry-run mode
|
|
120
|
+
"""
|
|
121
|
+
# Discover repository context
|
|
122
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
123
|
+
ensure_erk_metadata_dir(repo)
|
|
124
|
+
repo_root = repo.root
|
|
125
|
+
|
|
126
|
+
# Determine branch name
|
|
127
|
+
if linked_branch_name is not None:
|
|
128
|
+
# For issue mode: use the branch created for this issue
|
|
129
|
+
branch = linked_branch_name
|
|
130
|
+
else:
|
|
131
|
+
# For file mode: derive branch from plan name
|
|
132
|
+
branch = sanitize_worktree_name(plan_source.base_name)
|
|
133
|
+
|
|
134
|
+
# Get pool size from config
|
|
135
|
+
pool_size = get_pool_size(ctx)
|
|
136
|
+
|
|
137
|
+
# Load or create pool state
|
|
138
|
+
state = load_pool_state(repo.pool_json_path)
|
|
139
|
+
if state is None:
|
|
140
|
+
state = PoolState(
|
|
141
|
+
version="1.0",
|
|
142
|
+
pool_size=pool_size,
|
|
143
|
+
slots=(),
|
|
144
|
+
assignments=(),
|
|
145
|
+
)
|
|
146
|
+
elif state.pool_size != pool_size:
|
|
147
|
+
# Update pool_size from config if it changed
|
|
148
|
+
state = PoolState(
|
|
149
|
+
version=state.version,
|
|
150
|
+
pool_size=pool_size,
|
|
151
|
+
slots=state.slots,
|
|
152
|
+
assignments=state.assignments,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Check if branch is already assigned to a slot
|
|
156
|
+
existing_assignment = find_branch_assignment(state, branch)
|
|
157
|
+
if existing_assignment is not None:
|
|
158
|
+
# Branch already has a slot - use it
|
|
159
|
+
slot_name = existing_assignment.slot_name
|
|
160
|
+
wt_path = existing_assignment.worktree_path
|
|
161
|
+
ctx.feedback.info(f"Branch '{branch}' already assigned to {slot_name}")
|
|
162
|
+
|
|
163
|
+
# Handle dry-run mode
|
|
164
|
+
if dry_run:
|
|
165
|
+
_show_dry_run_output(slot_name, plan_source, submit, dangerous, no_interactive, model)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Just update .impl/ folder with new plan content
|
|
169
|
+
ctx.feedback.info("Updating .impl/ folder with plan...")
|
|
170
|
+
create_impl_folder(
|
|
171
|
+
worktree_path=wt_path,
|
|
172
|
+
plan_content=plan_source.plan_content,
|
|
173
|
+
overwrite=True,
|
|
174
|
+
)
|
|
175
|
+
ctx.feedback.success("✓ Updated .impl/ folder")
|
|
176
|
+
|
|
177
|
+
return WorktreeCreationResult(
|
|
178
|
+
worktree_path=wt_path,
|
|
179
|
+
impl_dir=wt_path / ".impl",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Check if branch already exists locally
|
|
183
|
+
local_branches = ctx.git.list_local_branches(repo_root)
|
|
184
|
+
use_existing_branch = branch in local_branches
|
|
185
|
+
|
|
186
|
+
# Find available slot
|
|
187
|
+
inactive_slot = find_inactive_slot(state, ctx.git, repo_root)
|
|
188
|
+
if inactive_slot is not None:
|
|
189
|
+
# Fast path: reuse existing worktree
|
|
190
|
+
slot_name, wt_path = inactive_slot
|
|
191
|
+
else:
|
|
192
|
+
# Find next available slot number
|
|
193
|
+
slot_num = find_next_available_slot(state, repo.worktrees_dir)
|
|
194
|
+
if slot_num is None:
|
|
195
|
+
# Pool is full - handle interactively or with --force
|
|
196
|
+
to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
|
|
197
|
+
if to_unassign is None:
|
|
198
|
+
raise SystemExit(1) from None
|
|
199
|
+
|
|
200
|
+
# Remove the assignment from state
|
|
201
|
+
new_assignments = tuple(
|
|
202
|
+
a for a in state.assignments if a.slot_name != to_unassign.slot_name
|
|
203
|
+
)
|
|
204
|
+
state = PoolState(
|
|
205
|
+
version=state.version,
|
|
206
|
+
pool_size=state.pool_size,
|
|
207
|
+
slots=state.slots,
|
|
208
|
+
assignments=new_assignments,
|
|
209
|
+
)
|
|
210
|
+
save_pool_state(repo.pool_json_path, state)
|
|
211
|
+
user_output(
|
|
212
|
+
click.style("✓ ", fg="green")
|
|
213
|
+
+ f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
|
|
214
|
+
+ f"from {click.style(to_unassign.slot_name, fg='cyan')}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Use the slot we just unassigned (it has a worktree directory that can be reused)
|
|
218
|
+
slot_name = to_unassign.slot_name
|
|
219
|
+
wt_path = to_unassign.worktree_path
|
|
220
|
+
else:
|
|
221
|
+
slot_name = generate_slot_name(slot_num)
|
|
222
|
+
wt_path = repo.worktrees_dir / slot_name
|
|
223
|
+
|
|
224
|
+
# Handle dry-run mode
|
|
225
|
+
if dry_run:
|
|
226
|
+
_show_dry_run_output(slot_name, plan_source, submit, dangerous, no_interactive, model)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Create worktree at slot path
|
|
230
|
+
ctx.feedback.info(f"Assigning to slot '{slot_name}'...")
|
|
231
|
+
|
|
232
|
+
# Load local config
|
|
233
|
+
config = ctx.local_config if ctx.local_config is not None else LoadedConfig.test()
|
|
234
|
+
|
|
235
|
+
# Respect global use_graphite config
|
|
236
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
237
|
+
|
|
238
|
+
if inactive_slot is not None:
|
|
239
|
+
# Fast path: checkout branch in existing worktree
|
|
240
|
+
# Check for uncommitted changes before checkout
|
|
241
|
+
_check_worktree_clean_for_checkout(ctx, wt_path, slot_name)
|
|
242
|
+
if use_existing_branch:
|
|
243
|
+
ctx.feedback.info(f"Checking out existing branch '{branch}'...")
|
|
244
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
245
|
+
else:
|
|
246
|
+
# Create branch and checkout
|
|
247
|
+
ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
|
|
248
|
+
ctx.git.create_branch(repo_root, branch, base_branch)
|
|
249
|
+
if use_graphite:
|
|
250
|
+
ctx.graphite.track_branch(repo_root, branch, base_branch)
|
|
251
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
252
|
+
else:
|
|
253
|
+
# On-demand slot creation
|
|
254
|
+
if not use_existing_branch:
|
|
255
|
+
# Create branch first
|
|
256
|
+
ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
|
|
257
|
+
ctx.git.create_branch(repo_root, branch, base_branch)
|
|
258
|
+
if use_graphite:
|
|
259
|
+
ctx.graphite.track_branch(repo_root, branch, base_branch)
|
|
260
|
+
|
|
261
|
+
# Check if worktree directory already exists (from pool initialization)
|
|
262
|
+
if wt_path.exists():
|
|
263
|
+
# Check for uncommitted changes before checkout
|
|
264
|
+
_check_worktree_clean_for_checkout(ctx, wt_path, slot_name)
|
|
265
|
+
# Worktree already exists - check out the branch
|
|
266
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
267
|
+
else:
|
|
268
|
+
# Create directory for worktree
|
|
269
|
+
wt_path.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
|
|
271
|
+
# Add worktree
|
|
272
|
+
ctx.git.add_worktree(
|
|
273
|
+
repo_root,
|
|
274
|
+
wt_path,
|
|
275
|
+
branch=branch,
|
|
276
|
+
ref=None,
|
|
277
|
+
create_branch=False,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
ctx.feedback.success(f"✓ Assigned {branch} to {slot_name}")
|
|
281
|
+
|
|
282
|
+
# Create slot assignment
|
|
283
|
+
now = ctx.time.now().isoformat()
|
|
284
|
+
new_assignment = SlotAssignment(
|
|
285
|
+
slot_name=slot_name,
|
|
286
|
+
branch_name=branch,
|
|
287
|
+
assigned_at=now,
|
|
288
|
+
worktree_path=wt_path,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Update state with new assignment
|
|
292
|
+
new_state = PoolState(
|
|
293
|
+
version=state.version,
|
|
294
|
+
pool_size=state.pool_size,
|
|
295
|
+
slots=state.slots,
|
|
296
|
+
assignments=(*state.assignments, new_assignment),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Save state
|
|
300
|
+
save_pool_state(repo.pool_json_path, new_state)
|
|
301
|
+
|
|
302
|
+
# Update slot with objective (if provided)
|
|
303
|
+
if objective_issue is not None:
|
|
304
|
+
# Check if slot exists in slots list
|
|
305
|
+
slot_exists = any(s.name == slot_name for s in new_state.slots)
|
|
306
|
+
if slot_exists:
|
|
307
|
+
# Update existing slot
|
|
308
|
+
new_state = update_slot_objective(new_state, slot_name, objective_issue)
|
|
309
|
+
else:
|
|
310
|
+
# Add new slot with objective
|
|
311
|
+
new_slot = SlotInfo(name=slot_name, last_objective_issue=objective_issue)
|
|
312
|
+
new_state = PoolState(
|
|
313
|
+
version=new_state.version,
|
|
314
|
+
pool_size=new_state.pool_size,
|
|
315
|
+
slots=(*new_state.slots, new_slot),
|
|
316
|
+
assignments=new_state.assignments,
|
|
317
|
+
)
|
|
318
|
+
save_pool_state(repo.pool_json_path, new_state)
|
|
319
|
+
ctx.feedback.info(f"Linked to objective #{objective_issue}")
|
|
320
|
+
|
|
321
|
+
# Run post-worktree setup
|
|
322
|
+
run_post_worktree_setup(ctx, config, wt_path, repo_root, slot_name)
|
|
323
|
+
|
|
324
|
+
# Create .impl/ folder with plan content at worktree root
|
|
325
|
+
ctx.feedback.info("Creating .impl/ folder with plan...")
|
|
326
|
+
create_impl_folder(
|
|
327
|
+
worktree_path=wt_path,
|
|
328
|
+
plan_content=plan_source.plan_content,
|
|
329
|
+
overwrite=True,
|
|
330
|
+
)
|
|
331
|
+
ctx.feedback.success("✓ Created .impl/ folder")
|
|
332
|
+
|
|
333
|
+
return WorktreeCreationResult(
|
|
334
|
+
worktree_path=wt_path,
|
|
335
|
+
impl_dir=wt_path / ".impl",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _show_dry_run_output(
|
|
340
|
+
slot_name: str,
|
|
341
|
+
plan_source: PlanSource,
|
|
342
|
+
submit: bool,
|
|
343
|
+
dangerous: bool,
|
|
344
|
+
no_interactive: bool,
|
|
345
|
+
model: str | None,
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Show dry-run output for slot assignment."""
|
|
348
|
+
dry_run_header = click.style("Dry-run mode:", fg="cyan", bold=True)
|
|
349
|
+
user_output(dry_run_header + " No changes will be made\n")
|
|
350
|
+
|
|
351
|
+
# Show execution mode
|
|
352
|
+
mode = "non-interactive" if no_interactive else "interactive"
|
|
353
|
+
user_output(f"Execution mode: {mode}\n")
|
|
354
|
+
|
|
355
|
+
user_output(f"Would assign to slot '{slot_name}'")
|
|
356
|
+
user_output(f" {plan_source.dry_run_description}")
|
|
357
|
+
|
|
358
|
+
# Show command sequence
|
|
359
|
+
commands = build_command_sequence(submit)
|
|
360
|
+
user_output("\nCommand sequence:")
|
|
361
|
+
for i, cmd in enumerate(commands, 1):
|
|
362
|
+
cmd_args = build_claude_args(cmd, dangerous, model)
|
|
363
|
+
user_output(f" {i}. {' '.join(cmd_args)}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _implement_from_issue(
|
|
367
|
+
ctx: ErkContext,
|
|
368
|
+
*,
|
|
369
|
+
issue_number: str,
|
|
370
|
+
dry_run: bool,
|
|
371
|
+
submit: bool,
|
|
372
|
+
dangerous: bool,
|
|
373
|
+
script: bool,
|
|
374
|
+
no_interactive: bool,
|
|
375
|
+
verbose: bool,
|
|
376
|
+
model: str | None,
|
|
377
|
+
force: bool,
|
|
378
|
+
executor: ClaudeExecutor,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Implement feature from GitHub issue.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
ctx: Erk context
|
|
384
|
+
issue_number: GitHub issue number
|
|
385
|
+
dry_run: Whether to perform dry run
|
|
386
|
+
submit: Whether to auto-submit PR after implementation
|
|
387
|
+
dangerous: Whether to skip permission prompts
|
|
388
|
+
script: Whether to output activation script
|
|
389
|
+
no_interactive: Whether to execute non-interactively
|
|
390
|
+
verbose: Whether to show raw output or filtered output
|
|
391
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
392
|
+
force: Whether to auto-unassign oldest slot if pool is full
|
|
393
|
+
executor: Claude CLI executor for command execution
|
|
394
|
+
"""
|
|
395
|
+
# Discover repo context for issue fetch
|
|
396
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
397
|
+
ensure_erk_metadata_dir(repo)
|
|
398
|
+
|
|
399
|
+
# Determine base branch (respects worktree stacking)
|
|
400
|
+
base_branch = determine_base_branch(ctx, repo.root)
|
|
401
|
+
|
|
402
|
+
# Prepare plan source from issue (creates branch via git)
|
|
403
|
+
issue_plan_source = prepare_plan_source_from_issue(
|
|
404
|
+
ctx, repo.root, issue_number, base_branch=base_branch
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Extract objective from plan metadata (if present)
|
|
408
|
+
plan = ctx.plan_store.get_plan(repo.root, issue_number)
|
|
409
|
+
objective_issue = extract_plan_header_objective_issue(plan.body)
|
|
410
|
+
|
|
411
|
+
# Create worktree with plan content, using the branch name
|
|
412
|
+
result = _create_worktree_with_plan_content(
|
|
413
|
+
ctx,
|
|
414
|
+
plan_source=issue_plan_source.plan_source,
|
|
415
|
+
dry_run=dry_run,
|
|
416
|
+
submit=submit,
|
|
417
|
+
dangerous=dangerous,
|
|
418
|
+
no_interactive=no_interactive,
|
|
419
|
+
linked_branch_name=issue_plan_source.branch_name,
|
|
420
|
+
base_branch=base_branch,
|
|
421
|
+
model=model,
|
|
422
|
+
force=force,
|
|
423
|
+
objective_issue=objective_issue,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Early return for dry-run mode
|
|
427
|
+
if result is None:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
wt_path = result.worktree_path
|
|
431
|
+
|
|
432
|
+
# Save issue reference for PR linking (issue-specific)
|
|
433
|
+
# Use impl_dir from result to handle monorepo project-root placement
|
|
434
|
+
ctx.feedback.info("Saving issue reference for PR linking...")
|
|
435
|
+
plan = ctx.plan_store.get_plan(repo.root, issue_number)
|
|
436
|
+
save_issue_reference(result.impl_dir, int(issue_number), plan.url, plan.title)
|
|
437
|
+
|
|
438
|
+
ctx.feedback.success(f"✓ Saved issue reference: {plan.url}")
|
|
439
|
+
|
|
440
|
+
# Execute based on mode
|
|
441
|
+
if script:
|
|
442
|
+
# Script mode - output activation script
|
|
443
|
+
branch = wt_path.name
|
|
444
|
+
target_description = f"#{issue_number}"
|
|
445
|
+
output_activation_instructions(
|
|
446
|
+
ctx,
|
|
447
|
+
wt_path=wt_path,
|
|
448
|
+
branch=branch,
|
|
449
|
+
script=script,
|
|
450
|
+
submit=submit,
|
|
451
|
+
dangerous=dangerous,
|
|
452
|
+
model=model,
|
|
453
|
+
target_description=target_description,
|
|
454
|
+
)
|
|
455
|
+
elif no_interactive:
|
|
456
|
+
# Non-interactive mode - execute via subprocess
|
|
457
|
+
commands = build_command_sequence(submit)
|
|
458
|
+
execute_non_interactive_mode(
|
|
459
|
+
worktree_path=wt_path,
|
|
460
|
+
commands=commands,
|
|
461
|
+
dangerous=dangerous,
|
|
462
|
+
verbose=verbose,
|
|
463
|
+
model=model,
|
|
464
|
+
executor=executor,
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
# Interactive mode - hand off to Claude (never returns)
|
|
468
|
+
execute_interactive_mode(ctx, repo.root, wt_path, dangerous, model, executor)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _implement_from_file(
|
|
472
|
+
ctx: ErkContext,
|
|
473
|
+
*,
|
|
474
|
+
plan_file: Path,
|
|
475
|
+
dry_run: bool,
|
|
476
|
+
submit: bool,
|
|
477
|
+
dangerous: bool,
|
|
478
|
+
script: bool,
|
|
479
|
+
no_interactive: bool,
|
|
480
|
+
verbose: bool,
|
|
481
|
+
model: str | None,
|
|
482
|
+
force: bool,
|
|
483
|
+
executor: ClaudeExecutor,
|
|
484
|
+
) -> None:
|
|
485
|
+
"""Implement feature from plan file.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
ctx: Erk context
|
|
489
|
+
plan_file: Path to plan file
|
|
490
|
+
dry_run: Whether to perform dry run
|
|
491
|
+
submit: Whether to auto-submit PR after implementation
|
|
492
|
+
dangerous: Whether to skip permission prompts
|
|
493
|
+
script: Whether to output activation script
|
|
494
|
+
no_interactive: Whether to execute non-interactively
|
|
495
|
+
verbose: Whether to show raw output or filtered output
|
|
496
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
497
|
+
force: Whether to auto-unassign oldest slot if pool is full
|
|
498
|
+
executor: Claude CLI executor for command execution
|
|
499
|
+
"""
|
|
500
|
+
# Discover repo context
|
|
501
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
502
|
+
|
|
503
|
+
# Determine base branch (respects worktree stacking)
|
|
504
|
+
base_branch = determine_base_branch(ctx, repo.root)
|
|
505
|
+
|
|
506
|
+
# Prepare plan source from file
|
|
507
|
+
plan_source = prepare_plan_source_from_file(ctx, plan_file)
|
|
508
|
+
|
|
509
|
+
# Create worktree with plan content
|
|
510
|
+
# File mode has no objective metadata
|
|
511
|
+
result = _create_worktree_with_plan_content(
|
|
512
|
+
ctx,
|
|
513
|
+
plan_source=plan_source,
|
|
514
|
+
dry_run=dry_run,
|
|
515
|
+
submit=submit,
|
|
516
|
+
dangerous=dangerous,
|
|
517
|
+
no_interactive=no_interactive,
|
|
518
|
+
linked_branch_name=None,
|
|
519
|
+
base_branch=base_branch,
|
|
520
|
+
model=model,
|
|
521
|
+
force=force,
|
|
522
|
+
objective_issue=None,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Early return for dry-run mode
|
|
526
|
+
if result is None:
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
wt_path = result.worktree_path
|
|
530
|
+
|
|
531
|
+
# Delete original plan file (move semantics, file-specific)
|
|
532
|
+
ctx.feedback.info(f"Removing original plan file: {plan_file.name}...")
|
|
533
|
+
plan_file.unlink()
|
|
534
|
+
|
|
535
|
+
ctx.feedback.success("✓ Moved plan file to worktree")
|
|
536
|
+
|
|
537
|
+
# Execute based on mode
|
|
538
|
+
if script:
|
|
539
|
+
# Script mode - output activation script
|
|
540
|
+
branch = wt_path.name
|
|
541
|
+
target_description = str(plan_file)
|
|
542
|
+
output_activation_instructions(
|
|
543
|
+
ctx,
|
|
544
|
+
wt_path=wt_path,
|
|
545
|
+
branch=branch,
|
|
546
|
+
script=script,
|
|
547
|
+
submit=submit,
|
|
548
|
+
dangerous=dangerous,
|
|
549
|
+
model=model,
|
|
550
|
+
target_description=target_description,
|
|
551
|
+
)
|
|
552
|
+
elif no_interactive:
|
|
553
|
+
# Non-interactive mode - execute via subprocess
|
|
554
|
+
commands = build_command_sequence(submit)
|
|
555
|
+
execute_non_interactive_mode(
|
|
556
|
+
worktree_path=wt_path,
|
|
557
|
+
commands=commands,
|
|
558
|
+
dangerous=dangerous,
|
|
559
|
+
verbose=verbose,
|
|
560
|
+
model=model,
|
|
561
|
+
executor=executor,
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
# Interactive mode - hand off to Claude (never returns)
|
|
565
|
+
execute_interactive_mode(ctx, repo.root, wt_path, dangerous, model, executor)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@alias("impl")
|
|
569
|
+
@click.command("implement", cls=CommandWithHiddenOptions)
|
|
570
|
+
@click.argument("target", shell_complete=complete_plan_files)
|
|
571
|
+
@implement_common_options
|
|
572
|
+
@click.option(
|
|
573
|
+
"-f",
|
|
574
|
+
"--force",
|
|
575
|
+
is_flag=True,
|
|
576
|
+
default=False,
|
|
577
|
+
help="Auto-unassign oldest slot if pool is full (no interactive prompt).",
|
|
578
|
+
)
|
|
579
|
+
@click.pass_obj
|
|
580
|
+
def implement(
|
|
581
|
+
ctx: ErkContext,
|
|
582
|
+
target: str,
|
|
583
|
+
dry_run: bool,
|
|
584
|
+
submit: bool,
|
|
585
|
+
dangerous: bool,
|
|
586
|
+
no_interactive: bool,
|
|
587
|
+
script: bool,
|
|
588
|
+
yolo: bool,
|
|
589
|
+
verbose: bool,
|
|
590
|
+
force: bool,
|
|
591
|
+
model: str | None,
|
|
592
|
+
) -> None:
|
|
593
|
+
"""Create worktree from GitHub issue or plan file and execute implementation.
|
|
594
|
+
|
|
595
|
+
By default, runs in interactive mode where you can interact with Claude
|
|
596
|
+
during implementation. Use --no-interactive for automated execution.
|
|
597
|
+
|
|
598
|
+
TARGET can be:
|
|
599
|
+
- GitHub issue number (e.g., #123 or 123)
|
|
600
|
+
- GitHub issue URL (e.g., https://github.com/user/repo/issues/123)
|
|
601
|
+
- Path to plan file (e.g., ./my-feature-plan.md)
|
|
602
|
+
|
|
603
|
+
Note: Plain numbers (e.g., 809) are always interpreted as GitHub issues.
|
|
604
|
+
For files with numeric names, use ./ prefix (e.g., ./809).
|
|
605
|
+
|
|
606
|
+
For GitHub issues, the issue must have the 'erk-plan' label.
|
|
607
|
+
|
|
608
|
+
Examples:
|
|
609
|
+
|
|
610
|
+
\b
|
|
611
|
+
# Interactive mode (default)
|
|
612
|
+
erk implement 123
|
|
613
|
+
|
|
614
|
+
\b
|
|
615
|
+
# Interactive mode, skip permissions
|
|
616
|
+
erk implement 123 --dangerous
|
|
617
|
+
|
|
618
|
+
\b
|
|
619
|
+
# Non-interactive mode (automated execution)
|
|
620
|
+
erk implement 123 --no-interactive
|
|
621
|
+
|
|
622
|
+
\b
|
|
623
|
+
# Full CI/PR workflow (requires --no-interactive)
|
|
624
|
+
erk implement 123 --no-interactive --submit
|
|
625
|
+
|
|
626
|
+
\b
|
|
627
|
+
# YOLO mode - full automation (dangerous + submit + no-interactive)
|
|
628
|
+
erk implement 123 --yolo
|
|
629
|
+
|
|
630
|
+
\b
|
|
631
|
+
# Shell integration
|
|
632
|
+
source <(erk implement 123 --script)
|
|
633
|
+
|
|
634
|
+
\b
|
|
635
|
+
# From plan file
|
|
636
|
+
erk implement ./my-feature-plan.md
|
|
637
|
+
"""
|
|
638
|
+
# Handle --yolo flag (shorthand for dangerous + submit + no-interactive)
|
|
639
|
+
if yolo:
|
|
640
|
+
dangerous = True
|
|
641
|
+
submit = True
|
|
642
|
+
no_interactive = True
|
|
643
|
+
|
|
644
|
+
# Normalize model name (validates and expands aliases)
|
|
645
|
+
model = normalize_model_name(model)
|
|
646
|
+
|
|
647
|
+
# Validate flag combinations
|
|
648
|
+
validate_flags(submit, no_interactive, script)
|
|
649
|
+
|
|
650
|
+
# Detect target type
|
|
651
|
+
target_info = detect_target_type(target)
|
|
652
|
+
|
|
653
|
+
# Output target detection diagnostic
|
|
654
|
+
if target_info.target_type in ("issue_number", "issue_url"):
|
|
655
|
+
ctx.feedback.info(f"Detected GitHub issue #{target_info.issue_number}")
|
|
656
|
+
elif target_info.target_type == "file_path":
|
|
657
|
+
ctx.feedback.info(f"Detected plan file: {target}")
|
|
658
|
+
|
|
659
|
+
if target_info.target_type in ("issue_number", "issue_url"):
|
|
660
|
+
# GitHub issue mode
|
|
661
|
+
if target_info.issue_number is None:
|
|
662
|
+
user_output(
|
|
663
|
+
click.style("Error: ", fg="red") + "Failed to extract issue number from target"
|
|
664
|
+
)
|
|
665
|
+
raise SystemExit(1) from None
|
|
666
|
+
|
|
667
|
+
_implement_from_issue(
|
|
668
|
+
ctx,
|
|
669
|
+
issue_number=target_info.issue_number,
|
|
670
|
+
dry_run=dry_run,
|
|
671
|
+
submit=submit,
|
|
672
|
+
dangerous=dangerous,
|
|
673
|
+
script=script,
|
|
674
|
+
no_interactive=no_interactive,
|
|
675
|
+
verbose=verbose,
|
|
676
|
+
model=model,
|
|
677
|
+
force=force,
|
|
678
|
+
executor=ctx.claude_executor,
|
|
679
|
+
)
|
|
680
|
+
else:
|
|
681
|
+
# Plan file mode
|
|
682
|
+
plan_file = Path(target)
|
|
683
|
+
_implement_from_file(
|
|
684
|
+
ctx,
|
|
685
|
+
plan_file=plan_file,
|
|
686
|
+
dry_run=dry_run,
|
|
687
|
+
submit=submit,
|
|
688
|
+
dangerous=dangerous,
|
|
689
|
+
script=script,
|
|
690
|
+
no_interactive=no_interactive,
|
|
691
|
+
verbose=verbose,
|
|
692
|
+
model=model,
|
|
693
|
+
force=force,
|
|
694
|
+
executor=ctx.claude_executor,
|
|
695
|
+
)
|