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,459 @@
|
|
|
1
|
+
"""Command to start a planning session in an assigned pool slot.
|
|
2
|
+
|
|
3
|
+
This command assigns a pool slot and launches Claude for planning,
|
|
4
|
+
without requiring a pre-existing plan file or GitHub issue.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from erk.cli.activation import render_activation_script
|
|
14
|
+
from erk.cli.commands.implement_shared import normalize_model_name
|
|
15
|
+
from erk.cli.commands.slot.common import (
|
|
16
|
+
find_branch_assignment,
|
|
17
|
+
find_inactive_slot,
|
|
18
|
+
find_next_available_slot,
|
|
19
|
+
generate_slot_name,
|
|
20
|
+
get_pool_size,
|
|
21
|
+
handle_pool_full_interactive,
|
|
22
|
+
)
|
|
23
|
+
from erk.cli.commands.wt.create_cmd import run_post_worktree_setup
|
|
24
|
+
from erk.cli.config import LoadedConfig
|
|
25
|
+
from erk.cli.core import discover_repo_context
|
|
26
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
|
|
27
|
+
from erk.core.context import ErkContext
|
|
28
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
29
|
+
from erk.core.worktree_pool import (
|
|
30
|
+
PoolState,
|
|
31
|
+
SlotAssignment,
|
|
32
|
+
load_pool_state,
|
|
33
|
+
save_pool_state,
|
|
34
|
+
)
|
|
35
|
+
from erk.core.worktree_utils import compute_relative_path_in_worktree
|
|
36
|
+
from erk_shared.naming import sanitize_worktree_name
|
|
37
|
+
from erk_shared.output.output import user_output
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _generate_timestamp_name() -> str:
|
|
41
|
+
"""Generate a timestamp-based branch name for planning.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Branch name like "planning-01-04-0930"
|
|
45
|
+
"""
|
|
46
|
+
now = datetime.now(tz=UTC)
|
|
47
|
+
return f"planning-{now.strftime('%m-%d-%H%M')}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _determine_base_branch(ctx: ErkContext, repo_root: Path) -> str:
|
|
51
|
+
"""Determine the base branch for new worktree creation.
|
|
52
|
+
|
|
53
|
+
When Graphite is enabled and the user is on a non-trunk branch,
|
|
54
|
+
stack on the current branch. Otherwise, use trunk.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ctx: Erk context
|
|
58
|
+
repo_root: Repository root path
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Base branch name to use as ref for worktree creation
|
|
62
|
+
"""
|
|
63
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo_root)
|
|
64
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
65
|
+
|
|
66
|
+
if not use_graphite:
|
|
67
|
+
return trunk_branch
|
|
68
|
+
|
|
69
|
+
current_branch = ctx.git.get_current_branch(ctx.cwd)
|
|
70
|
+
if current_branch and current_branch != trunk_branch:
|
|
71
|
+
return current_branch
|
|
72
|
+
|
|
73
|
+
return trunk_branch
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_claude_command(dangerous: bool, model: str | None) -> str:
|
|
77
|
+
"""Build a Claude CLI invocation without a slash command.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
dangerous: Whether to skip permission prompts
|
|
81
|
+
model: Optional model name (haiku, sonnet, opus) to pass to Claude CLI
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Complete Claude CLI command string
|
|
85
|
+
"""
|
|
86
|
+
cmd = "claude --permission-mode acceptEdits"
|
|
87
|
+
if dangerous:
|
|
88
|
+
cmd += " --dangerously-skip-permissions"
|
|
89
|
+
if model is not None:
|
|
90
|
+
cmd += f" --model {model}"
|
|
91
|
+
return cmd
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@click.command("start", cls=CommandWithHiddenOptions)
|
|
95
|
+
@click.option(
|
|
96
|
+
"-n",
|
|
97
|
+
"--name",
|
|
98
|
+
type=str,
|
|
99
|
+
help="Branch name for the planning session (auto-generated if not provided)",
|
|
100
|
+
)
|
|
101
|
+
@click.option(
|
|
102
|
+
"--dry-run",
|
|
103
|
+
is_flag=True,
|
|
104
|
+
help="Print what would be executed without doing it",
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--dangerous",
|
|
108
|
+
is_flag=True,
|
|
109
|
+
help="Skip permission prompts by passing --dangerously-skip-permissions to Claude",
|
|
110
|
+
)
|
|
111
|
+
@script_option
|
|
112
|
+
@click.option(
|
|
113
|
+
"-m",
|
|
114
|
+
"--model",
|
|
115
|
+
type=str,
|
|
116
|
+
help="Model to use for Claude (haiku/h, sonnet/s, opus/o)",
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"-f",
|
|
120
|
+
"--force",
|
|
121
|
+
is_flag=True,
|
|
122
|
+
help="Auto-unassign oldest slot if pool is full (no interactive prompt).",
|
|
123
|
+
)
|
|
124
|
+
@click.pass_obj
|
|
125
|
+
def plan_start(
|
|
126
|
+
ctx: ErkContext,
|
|
127
|
+
name: str | None,
|
|
128
|
+
dry_run: bool,
|
|
129
|
+
dangerous: bool,
|
|
130
|
+
script: bool,
|
|
131
|
+
model: str | None,
|
|
132
|
+
force: bool,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Start a planning session in an assigned pool slot.
|
|
135
|
+
|
|
136
|
+
This command assigns a pool slot, creates a planning branch, and launches
|
|
137
|
+
Claude for planning. Use this when you want to explore a problem space
|
|
138
|
+
before committing to a specific plan.
|
|
139
|
+
|
|
140
|
+
If --name is provided, it will be used as the branch name (sanitized).
|
|
141
|
+
Otherwise, a timestamp-based name like "planning-01-04-0930" is generated.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
|
|
145
|
+
\b
|
|
146
|
+
# Start planning with auto-generated branch name
|
|
147
|
+
erk plan start
|
|
148
|
+
|
|
149
|
+
\b
|
|
150
|
+
# Start planning with a custom branch name
|
|
151
|
+
erk plan start --name my-feature
|
|
152
|
+
|
|
153
|
+
\b
|
|
154
|
+
# Skip permission prompts
|
|
155
|
+
erk plan start --dangerous
|
|
156
|
+
|
|
157
|
+
\b
|
|
158
|
+
# Shell integration
|
|
159
|
+
source <(erk plan start --script)
|
|
160
|
+
|
|
161
|
+
\b
|
|
162
|
+
# With specific model
|
|
163
|
+
erk plan start --model opus
|
|
164
|
+
"""
|
|
165
|
+
# Normalize model name (validates and expands aliases)
|
|
166
|
+
model = normalize_model_name(model)
|
|
167
|
+
|
|
168
|
+
# Discover repository context
|
|
169
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
170
|
+
ensure_erk_metadata_dir(repo)
|
|
171
|
+
repo_root = repo.root
|
|
172
|
+
|
|
173
|
+
# Determine base branch (respects worktree stacking)
|
|
174
|
+
base_branch = _determine_base_branch(ctx, repo_root)
|
|
175
|
+
|
|
176
|
+
# Generate or sanitize branch name
|
|
177
|
+
if name is not None:
|
|
178
|
+
branch = sanitize_worktree_name(name)
|
|
179
|
+
else:
|
|
180
|
+
branch = _generate_timestamp_name()
|
|
181
|
+
|
|
182
|
+
ctx.feedback.info(f"Planning branch: {branch}")
|
|
183
|
+
|
|
184
|
+
# Get pool size from config
|
|
185
|
+
pool_size = get_pool_size(ctx)
|
|
186
|
+
|
|
187
|
+
# Load or create pool state
|
|
188
|
+
state = load_pool_state(repo.pool_json_path)
|
|
189
|
+
if state is None:
|
|
190
|
+
state = PoolState(
|
|
191
|
+
version="1.0",
|
|
192
|
+
pool_size=pool_size,
|
|
193
|
+
slots=(),
|
|
194
|
+
assignments=(),
|
|
195
|
+
)
|
|
196
|
+
elif state.pool_size != pool_size:
|
|
197
|
+
# Update pool_size from config if it changed
|
|
198
|
+
state = PoolState(
|
|
199
|
+
version=state.version,
|
|
200
|
+
pool_size=pool_size,
|
|
201
|
+
slots=state.slots,
|
|
202
|
+
assignments=state.assignments,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Check if branch is already assigned to a slot
|
|
206
|
+
existing_assignment = find_branch_assignment(state, branch)
|
|
207
|
+
if existing_assignment is not None:
|
|
208
|
+
# Branch already has a slot - use it
|
|
209
|
+
slot_name = existing_assignment.slot_name
|
|
210
|
+
wt_path = existing_assignment.worktree_path
|
|
211
|
+
ctx.feedback.info(f"Branch '{branch}' already assigned to {slot_name}")
|
|
212
|
+
|
|
213
|
+
# Handle dry-run mode
|
|
214
|
+
if dry_run:
|
|
215
|
+
_show_dry_run_output(slot_name, branch, dangerous, model)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# Execute planning session
|
|
219
|
+
_execute_planning(ctx, repo_root, wt_path, branch, dangerous, model, script)
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Check if branch already exists locally
|
|
223
|
+
local_branches = ctx.git.list_local_branches(repo_root)
|
|
224
|
+
use_existing_branch = branch in local_branches
|
|
225
|
+
|
|
226
|
+
# Find available slot
|
|
227
|
+
inactive_slot = find_inactive_slot(state, ctx.git, repo_root)
|
|
228
|
+
if inactive_slot is not None:
|
|
229
|
+
# Fast path: reuse existing worktree
|
|
230
|
+
slot_name, wt_path = inactive_slot
|
|
231
|
+
else:
|
|
232
|
+
# Find next available slot number
|
|
233
|
+
slot_num = find_next_available_slot(state, repo.worktrees_dir)
|
|
234
|
+
if slot_num is None:
|
|
235
|
+
# Pool is full - handle interactively or with --force
|
|
236
|
+
to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
|
|
237
|
+
if to_unassign is None:
|
|
238
|
+
raise SystemExit(1) from None
|
|
239
|
+
|
|
240
|
+
# Remove the assignment from state
|
|
241
|
+
new_assignments = tuple(
|
|
242
|
+
a for a in state.assignments if a.slot_name != to_unassign.slot_name
|
|
243
|
+
)
|
|
244
|
+
state = PoolState(
|
|
245
|
+
version=state.version,
|
|
246
|
+
pool_size=state.pool_size,
|
|
247
|
+
slots=state.slots,
|
|
248
|
+
assignments=new_assignments,
|
|
249
|
+
)
|
|
250
|
+
save_pool_state(repo.pool_json_path, state)
|
|
251
|
+
user_output(
|
|
252
|
+
click.style("✓ ", fg="green")
|
|
253
|
+
+ f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
|
|
254
|
+
+ f"from {click.style(to_unassign.slot_name, fg='cyan')}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Use the slot we just unassigned (it has a worktree directory that can be reused)
|
|
258
|
+
slot_name = to_unassign.slot_name
|
|
259
|
+
wt_path = to_unassign.worktree_path
|
|
260
|
+
else:
|
|
261
|
+
slot_name = generate_slot_name(slot_num)
|
|
262
|
+
wt_path = repo.worktrees_dir / slot_name
|
|
263
|
+
|
|
264
|
+
# Handle dry-run mode
|
|
265
|
+
if dry_run:
|
|
266
|
+
_show_dry_run_output(slot_name, branch, dangerous, model)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Create worktree at slot path
|
|
270
|
+
ctx.feedback.info(f"Assigning to slot '{slot_name}'...")
|
|
271
|
+
|
|
272
|
+
# Load local config
|
|
273
|
+
config = ctx.local_config if ctx.local_config is not None else LoadedConfig.test()
|
|
274
|
+
|
|
275
|
+
# Respect global use_graphite config
|
|
276
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
277
|
+
|
|
278
|
+
if inactive_slot is not None:
|
|
279
|
+
# Fast path: checkout branch in existing worktree
|
|
280
|
+
if use_existing_branch:
|
|
281
|
+
ctx.feedback.info(f"Checking out existing branch '{branch}'...")
|
|
282
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
283
|
+
else:
|
|
284
|
+
# Create branch and checkout
|
|
285
|
+
ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
|
|
286
|
+
ctx.git.create_branch(repo_root, branch, base_branch)
|
|
287
|
+
if use_graphite:
|
|
288
|
+
ctx.graphite.track_branch(repo_root, branch, base_branch)
|
|
289
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
290
|
+
else:
|
|
291
|
+
# On-demand slot creation
|
|
292
|
+
if not use_existing_branch:
|
|
293
|
+
# Create branch first
|
|
294
|
+
ctx.feedback.info(f"Creating branch '{branch}' from {base_branch}...")
|
|
295
|
+
ctx.git.create_branch(repo_root, branch, base_branch)
|
|
296
|
+
if use_graphite:
|
|
297
|
+
ctx.graphite.track_branch(repo_root, branch, base_branch)
|
|
298
|
+
|
|
299
|
+
# Check if worktree directory already exists (from pool initialization)
|
|
300
|
+
if wt_path.exists():
|
|
301
|
+
# Worktree already exists - check out the branch
|
|
302
|
+
ctx.git.checkout_branch(wt_path, branch)
|
|
303
|
+
else:
|
|
304
|
+
# Create directory for worktree
|
|
305
|
+
wt_path.mkdir(parents=True, exist_ok=True)
|
|
306
|
+
|
|
307
|
+
# Add worktree
|
|
308
|
+
ctx.git.add_worktree(
|
|
309
|
+
repo_root,
|
|
310
|
+
wt_path,
|
|
311
|
+
branch=branch,
|
|
312
|
+
ref=None,
|
|
313
|
+
create_branch=False,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
ctx.feedback.success(f"✓ Assigned {branch} to {slot_name}")
|
|
317
|
+
|
|
318
|
+
# Create slot assignment
|
|
319
|
+
now = ctx.time.now().isoformat()
|
|
320
|
+
new_assignment = SlotAssignment(
|
|
321
|
+
slot_name=slot_name,
|
|
322
|
+
branch_name=branch,
|
|
323
|
+
assigned_at=now,
|
|
324
|
+
worktree_path=wt_path,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Update state with new assignment
|
|
328
|
+
new_state = PoolState(
|
|
329
|
+
version=state.version,
|
|
330
|
+
pool_size=state.pool_size,
|
|
331
|
+
slots=state.slots,
|
|
332
|
+
assignments=(*state.assignments, new_assignment),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Save state
|
|
336
|
+
save_pool_state(repo.pool_json_path, new_state)
|
|
337
|
+
|
|
338
|
+
# Run post-worktree setup
|
|
339
|
+
run_post_worktree_setup(ctx, config, wt_path, repo_root, slot_name)
|
|
340
|
+
|
|
341
|
+
# Execute planning session
|
|
342
|
+
_execute_planning(ctx, repo_root, wt_path, branch, dangerous, model, script)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _show_dry_run_output(
|
|
346
|
+
slot_name: str,
|
|
347
|
+
branch: str,
|
|
348
|
+
dangerous: bool,
|
|
349
|
+
model: str | None,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Show dry-run output for slot assignment."""
|
|
352
|
+
dry_run_header = click.style("Dry-run mode:", fg="cyan", bold=True)
|
|
353
|
+
user_output(dry_run_header + " No changes will be made\n")
|
|
354
|
+
|
|
355
|
+
user_output(f"Would assign to slot '{slot_name}'")
|
|
356
|
+
user_output(f" Branch: {branch}")
|
|
357
|
+
|
|
358
|
+
user_output("\nWould launch Claude:")
|
|
359
|
+
claude_cmd = _build_claude_command(dangerous, model)
|
|
360
|
+
user_output(f" {claude_cmd}")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _execute_planning(
|
|
364
|
+
ctx: ErkContext,
|
|
365
|
+
repo_root: Path,
|
|
366
|
+
wt_path: Path,
|
|
367
|
+
branch: str,
|
|
368
|
+
dangerous: bool,
|
|
369
|
+
model: str | None,
|
|
370
|
+
script: bool,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Execute planning session - script mode or interactive.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
ctx: Erk context
|
|
376
|
+
repo_root: Repository root path
|
|
377
|
+
wt_path: Worktree path
|
|
378
|
+
branch: Branch name
|
|
379
|
+
dangerous: Whether to skip permission prompts
|
|
380
|
+
model: Optional model name
|
|
381
|
+
script: Whether to output shell script instead of launching Claude
|
|
382
|
+
"""
|
|
383
|
+
if script:
|
|
384
|
+
# Script mode - output activation script
|
|
385
|
+
_output_activation_script(ctx, wt_path, branch, dangerous, model)
|
|
386
|
+
else:
|
|
387
|
+
# Interactive mode - launch Claude
|
|
388
|
+
_launch_claude_interactive(ctx, repo_root, wt_path, dangerous, model)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _output_activation_script(
|
|
392
|
+
ctx: ErkContext,
|
|
393
|
+
wt_path: Path,
|
|
394
|
+
branch: str,
|
|
395
|
+
dangerous: bool,
|
|
396
|
+
model: str | None,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Output activation script for shell integration.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
ctx: Erk context
|
|
402
|
+
wt_path: Worktree path
|
|
403
|
+
branch: Branch name (for comment)
|
|
404
|
+
dangerous: Whether to skip permission prompts
|
|
405
|
+
model: Optional model name
|
|
406
|
+
"""
|
|
407
|
+
# Build Claude command (no slash command)
|
|
408
|
+
claude_cmd = _build_claude_command(dangerous, model)
|
|
409
|
+
|
|
410
|
+
# Get base activation script (cd + venv + env)
|
|
411
|
+
full_script = render_activation_script(
|
|
412
|
+
worktree_path=wt_path,
|
|
413
|
+
target_subpath=None,
|
|
414
|
+
post_cd_commands=None,
|
|
415
|
+
final_message=claude_cmd,
|
|
416
|
+
comment=f"plan start {branch}",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
result = ctx.script_writer.write_activation_script(
|
|
420
|
+
full_script,
|
|
421
|
+
command_name="plan-start",
|
|
422
|
+
comment=f"activate {wt_path.name} and launch Claude for planning",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
result.output_for_shell_integration()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _launch_claude_interactive(
|
|
429
|
+
ctx: ErkContext,
|
|
430
|
+
repo_root: Path,
|
|
431
|
+
wt_path: Path,
|
|
432
|
+
dangerous: bool,
|
|
433
|
+
model: str | None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Launch Claude in interactive mode for planning.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
ctx: Erk context
|
|
439
|
+
repo_root: Repository root path
|
|
440
|
+
wt_path: Worktree path
|
|
441
|
+
dangerous: Whether to skip permission prompts
|
|
442
|
+
model: Optional model name
|
|
443
|
+
|
|
444
|
+
Note:
|
|
445
|
+
This function never returns in production - the process is replaced by Claude
|
|
446
|
+
"""
|
|
447
|
+
click.echo("Entering interactive planning mode...", err=True)
|
|
448
|
+
try:
|
|
449
|
+
# Launch Claude without a slash command (empty string)
|
|
450
|
+
# The executor handles empty command by not appending it to args
|
|
451
|
+
ctx.claude_executor.execute_interactive(
|
|
452
|
+
wt_path,
|
|
453
|
+
dangerous,
|
|
454
|
+
"", # No slash command - user will drive the planning session
|
|
455
|
+
compute_relative_path_in_worktree(ctx.git.list_worktrees(repo_root), ctx.cwd),
|
|
456
|
+
model=model,
|
|
457
|
+
)
|
|
458
|
+
except RuntimeError as e:
|
|
459
|
+
raise click.ClickException(str(e)) from e
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Planner box management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.planner.configure_cmd import configure_planner
|
|
6
|
+
from erk.cli.commands.planner.connect_cmd import connect_planner
|
|
7
|
+
from erk.cli.commands.planner.create_cmd import create_planner
|
|
8
|
+
from erk.cli.commands.planner.list_cmd import list_planners
|
|
9
|
+
from erk.cli.commands.planner.register_cmd import register_planner
|
|
10
|
+
from erk.cli.commands.planner.set_default_cmd import set_default_planner
|
|
11
|
+
from erk.cli.commands.planner.unregister_cmd import unregister_planner
|
|
12
|
+
from erk.cli.help_formatter import ErkCommandGroup
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(
|
|
16
|
+
"planner", cls=ErkCommandGroup, grouped=False, invoke_without_command=True, hidden=True
|
|
17
|
+
)
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def planner_group(ctx: click.Context) -> None:
|
|
20
|
+
"""Manage planner boxes (GitHub Codespaces for remote planning).
|
|
21
|
+
|
|
22
|
+
A planner box is a GitHub Codespace pre-configured for remote planning
|
|
23
|
+
with Claude Code. Use 'erk planner register' to register an existing
|
|
24
|
+
codespace, then 'erk planner' to connect.
|
|
25
|
+
|
|
26
|
+
When invoked without a subcommand, connects to the default planner.
|
|
27
|
+
"""
|
|
28
|
+
# If no subcommand provided, invoke connect to default
|
|
29
|
+
if ctx.invoked_subcommand is None:
|
|
30
|
+
ctx.invoke(connect_planner, name=None)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Register subcommands
|
|
34
|
+
planner_group.add_command(connect_planner)
|
|
35
|
+
planner_group.add_command(create_planner)
|
|
36
|
+
planner_group.add_command(configure_planner)
|
|
37
|
+
planner_group.add_command(register_planner)
|
|
38
|
+
planner_group.add_command(unregister_planner)
|
|
39
|
+
planner_group.add_command(list_planners)
|
|
40
|
+
planner_group.add_command(set_default_planner)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Configure a planner box interactively."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
|
|
9
|
+
SETUP_CHECKLIST = """
|
|
10
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
11
|
+
│ Planner Setup Checklist │
|
|
12
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
13
|
+
│ │
|
|
14
|
+
│ 1. Claude Code Authentication │
|
|
15
|
+
│ └─ Run: claude │
|
|
16
|
+
│ Follow prompts to authenticate with Anthropic │
|
|
17
|
+
│ │
|
|
18
|
+
│ 2. Verify Setup │
|
|
19
|
+
│ └─ gh auth status (should show logged in) │
|
|
20
|
+
│ └─ claude doctor (should show all green) │
|
|
21
|
+
│ │
|
|
22
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.command("configure")
|
|
27
|
+
@click.argument("name")
|
|
28
|
+
@click.pass_obj
|
|
29
|
+
def configure_planner(ctx: ErkContext, name: str) -> None:
|
|
30
|
+
"""Configure a planner box with an interactive SSH session.
|
|
31
|
+
|
|
32
|
+
Opens an interactive SSH session to the codespace for manual setup
|
|
33
|
+
(installing tools, setting up auth, etc.). When you exit the session,
|
|
34
|
+
you'll be prompted to mark the planner as configured.
|
|
35
|
+
"""
|
|
36
|
+
planner = ctx.planner_registry.get(name)
|
|
37
|
+
if planner is None:
|
|
38
|
+
click.echo(f"Error: No planner named '{name}' found.", err=True)
|
|
39
|
+
click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
if planner.configured:
|
|
43
|
+
click.echo(f"Note: Planner '{name}' is already marked as configured.", err=True)
|
|
44
|
+
if not click.confirm("Continue with configuration session anyway?"):
|
|
45
|
+
raise SystemExit(0)
|
|
46
|
+
|
|
47
|
+
click.echo(SETUP_CHECKLIST, err=True)
|
|
48
|
+
click.echo(f"Opening interactive SSH session to '{name}'...", err=True)
|
|
49
|
+
click.echo(
|
|
50
|
+
"Complete the setup steps above, then exit the session (Ctrl+D or 'exit').", err=True
|
|
51
|
+
)
|
|
52
|
+
click.echo("", err=True)
|
|
53
|
+
|
|
54
|
+
# Run interactive SSH session (waits for completion)
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
["gh", "codespace", "ssh", "-c", planner.gh_name],
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
click.echo(f"SSH session ended with error (exit code {result.returncode}).", err=True)
|
|
62
|
+
# Still allow marking as configured if user wants
|
|
63
|
+
|
|
64
|
+
# Ask if configuration is complete
|
|
65
|
+
click.echo("", err=True)
|
|
66
|
+
if click.confirm(f"Mark planner '{name}' as configured?"):
|
|
67
|
+
ctx.planner_registry.mark_configured(name)
|
|
68
|
+
click.echo(f"Planner '{name}' marked as configured.", err=True)
|
|
69
|
+
else:
|
|
70
|
+
click.echo(
|
|
71
|
+
f"Planner '{name}' left unconfigured. Run 'erk planner configure {name}' again later.",
|
|
72
|
+
err=True,
|
|
73
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Connect to a planner box."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("connect")
|
|
11
|
+
@click.argument("name", required=False)
|
|
12
|
+
@click.option("--ssh", is_flag=True, help="Connect via SSH instead of VS Code")
|
|
13
|
+
@click.pass_obj
|
|
14
|
+
def connect_planner(ctx: ErkContext, name: str | None, ssh: bool) -> None:
|
|
15
|
+
"""Connect to a planner box.
|
|
16
|
+
|
|
17
|
+
If NAME is provided, connects to that planner. Otherwise, connects
|
|
18
|
+
to the default planner.
|
|
19
|
+
|
|
20
|
+
By default, opens VS Code desktop to prevent idle timeout. Use --ssh
|
|
21
|
+
to connect via SSH and launch Claude directly.
|
|
22
|
+
"""
|
|
23
|
+
# Get planner by name or default
|
|
24
|
+
if name is not None:
|
|
25
|
+
planner = ctx.planner_registry.get(name)
|
|
26
|
+
if planner is None:
|
|
27
|
+
click.echo(f"Error: No planner named '{name}' found.", err=True)
|
|
28
|
+
click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
|
|
29
|
+
raise SystemExit(1)
|
|
30
|
+
else:
|
|
31
|
+
planner = ctx.planner_registry.get_default()
|
|
32
|
+
if planner is None:
|
|
33
|
+
default_name = ctx.planner_registry.get_default_name()
|
|
34
|
+
if default_name is not None:
|
|
35
|
+
click.echo(f"Error: Default planner '{default_name}' not found.", err=True)
|
|
36
|
+
else:
|
|
37
|
+
click.echo("Error: No default planner set.", err=True)
|
|
38
|
+
click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
|
|
39
|
+
click.echo("Use 'erk planner set-default <name>' to set a default.", err=True)
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
# Check if configured
|
|
43
|
+
if not planner.configured:
|
|
44
|
+
click.echo(f"Warning: Planner '{planner.name}' has not been configured yet.", err=True)
|
|
45
|
+
click.echo(f"Run 'erk planner configure {planner.name}' for initial setup.", err=True)
|
|
46
|
+
|
|
47
|
+
# Update last connected timestamp
|
|
48
|
+
ctx.planner_registry.update_last_connected(planner.name, ctx.time.now())
|
|
49
|
+
|
|
50
|
+
if ssh:
|
|
51
|
+
# Connect via gh codespace ssh with claude command
|
|
52
|
+
click.echo(f"Connecting to planner '{planner.name}' via SSH...", err=True)
|
|
53
|
+
|
|
54
|
+
# Replace current process with ssh session
|
|
55
|
+
# -t: Force pseudo-terminal allocation (required for interactive TUI like claude)
|
|
56
|
+
# bash -l -c: Use login shell to ensure PATH is set up (claude installs to ~/.claude/local/)
|
|
57
|
+
# Launch Claude in interactive mode for planning workflows
|
|
58
|
+
#
|
|
59
|
+
# IMPORTANT: The entire remote command (bash -l -c '...') must be a single argument.
|
|
60
|
+
# SSH concatenates command arguments with spaces without preserving grouping.
|
|
61
|
+
# If passed as separate args ["bash", "-l", "-c", "cmd"], the remote receives:
|
|
62
|
+
# bash -l -c git pull && uv sync && ...
|
|
63
|
+
# Instead of:
|
|
64
|
+
# bash -l -c "git pull && uv sync && ..."
|
|
65
|
+
# This causes `bash -l -c git` to run `git` with no subcommand (exits with help).
|
|
66
|
+
setup_commands = "git pull && uv sync && source .venv/bin/activate"
|
|
67
|
+
claude_command = "claude --allow-dangerously-skip-permissions --verbose"
|
|
68
|
+
remote_command = f"bash -l -c '{setup_commands} && {claude_command}'"
|
|
69
|
+
|
|
70
|
+
# GH-API-AUDIT: REST - codespace SSH connection
|
|
71
|
+
os.execvp(
|
|
72
|
+
"gh",
|
|
73
|
+
[
|
|
74
|
+
"gh",
|
|
75
|
+
"codespace",
|
|
76
|
+
"ssh",
|
|
77
|
+
"-c",
|
|
78
|
+
planner.gh_name,
|
|
79
|
+
"--",
|
|
80
|
+
"-t",
|
|
81
|
+
remote_command,
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Default: Open VS Code desktop (prevents idle timeout)
|
|
86
|
+
click.echo("Opening VS Code...", err=True)
|
|
87
|
+
click.echo("", err=True)
|
|
88
|
+
click.echo("Run in VS Code terminal:", err=True)
|
|
89
|
+
click.echo(" git pull && uv sync && source .venv/bin/activate", err=True)
|
|
90
|
+
click.echo(
|
|
91
|
+
" claude --allow-dangerously-skip-permissions --verbose",
|
|
92
|
+
err=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# GH-API-AUDIT: REST - codespace VS Code connection
|
|
96
|
+
os.execvp("gh", ["gh", "codespace", "code", "-c", planner.gh_name])
|