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,277 @@
|
|
|
1
|
+
"""Slot check command - check pool state consistency with disk and git."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from erk.cli.commands.slot.common import generate_slot_name
|
|
10
|
+
from erk.cli.core import discover_repo_context
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
from erk.core.worktree_pool import PoolState, SlotAssignment, load_pool_state
|
|
13
|
+
from erk_shared.git.abc import Git, WorktreeInfo
|
|
14
|
+
from erk_shared.output.output import user_output
|
|
15
|
+
|
|
16
|
+
# Type alias for sync issue codes - using Literal for type safety
|
|
17
|
+
SyncIssueCode = Literal[
|
|
18
|
+
"orphan-state",
|
|
19
|
+
"orphan-dir",
|
|
20
|
+
"missing-branch",
|
|
21
|
+
"branch-mismatch",
|
|
22
|
+
"git-registry-missing",
|
|
23
|
+
"untracked-worktree",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SyncIssue:
|
|
29
|
+
"""A sync diagnostic issue found during pool state check."""
|
|
30
|
+
|
|
31
|
+
code: SyncIssueCode
|
|
32
|
+
message: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _find_erk_managed_dirs(worktrees_dir: Path, git: Git) -> set[str]:
|
|
36
|
+
"""Find directories in worktrees_dir matching erk-managed-wt-* pattern.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
worktrees_dir: Path to the worktrees directory
|
|
40
|
+
git: Git abstraction for path_exists and is_dir checks
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Set of slot names (e.g., {"erk-managed-wt-01", "erk-managed-wt-02"})
|
|
44
|
+
"""
|
|
45
|
+
if not git.path_exists(worktrees_dir):
|
|
46
|
+
return set()
|
|
47
|
+
|
|
48
|
+
result: set[str] = set()
|
|
49
|
+
# Iterate over worktrees_dir contents
|
|
50
|
+
# Use path_exists to validate before iterdir
|
|
51
|
+
for entry in worktrees_dir.iterdir():
|
|
52
|
+
if entry.name.startswith("erk-managed-wt-") and git.is_dir(entry):
|
|
53
|
+
result.add(entry.name)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_git_managed_slots(
|
|
58
|
+
worktrees: list[WorktreeInfo], worktrees_dir: Path
|
|
59
|
+
) -> dict[str, WorktreeInfo]:
|
|
60
|
+
"""Get worktrees that are erk-managed pool slots.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
worktrees: List of all git worktrees
|
|
64
|
+
worktrees_dir: Path to the worktrees directory
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict mapping slot name to WorktreeInfo for erk-managed slots
|
|
68
|
+
"""
|
|
69
|
+
result: dict[str, WorktreeInfo] = {}
|
|
70
|
+
for wt in worktrees:
|
|
71
|
+
if wt.path.parent == worktrees_dir and wt.path.name.startswith("erk-managed-wt-"):
|
|
72
|
+
result[wt.path.name] = wt
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_orphan_states(
|
|
77
|
+
assignments: tuple[SlotAssignment, ...],
|
|
78
|
+
ctx: ErkContext,
|
|
79
|
+
) -> list[SyncIssue]:
|
|
80
|
+
"""Check for assignments where the worktree directory doesn't exist.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
assignments: Current pool assignments
|
|
84
|
+
ctx: Erk context (for git.path_exists)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of (issue_type, message) tuples
|
|
88
|
+
"""
|
|
89
|
+
issues: list[SyncIssue] = []
|
|
90
|
+
for assignment in assignments:
|
|
91
|
+
if not ctx.git.path_exists(assignment.worktree_path):
|
|
92
|
+
issues.append(
|
|
93
|
+
SyncIssue(
|
|
94
|
+
code="orphan-state",
|
|
95
|
+
message=f"Slot {assignment.slot_name}: directory does not exist",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return issues
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_orphan_dirs(
|
|
102
|
+
state: PoolState,
|
|
103
|
+
fs_slots: set[str],
|
|
104
|
+
) -> list[SyncIssue]:
|
|
105
|
+
"""Check for directories that exist on filesystem but not in pool state.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
state: Pool state (to check against known slots)
|
|
109
|
+
fs_slots: Set of slot names found on filesystem
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of (issue_type, message) tuples
|
|
113
|
+
"""
|
|
114
|
+
# Generate known slots from pool_size (same logic as slot list command)
|
|
115
|
+
known_slots = {generate_slot_name(i) for i in range(1, state.pool_size + 1)}
|
|
116
|
+
|
|
117
|
+
issues: list[SyncIssue] = []
|
|
118
|
+
for slot_name in fs_slots:
|
|
119
|
+
if slot_name not in known_slots:
|
|
120
|
+
issues.append(
|
|
121
|
+
SyncIssue(
|
|
122
|
+
code="orphan-dir",
|
|
123
|
+
message=f"Directory {slot_name}: not in pool state",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return issues
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_missing_branches(
|
|
130
|
+
assignments: tuple[SlotAssignment, ...],
|
|
131
|
+
ctx: ErkContext,
|
|
132
|
+
repo_root: Path,
|
|
133
|
+
) -> list[SyncIssue]:
|
|
134
|
+
"""Check for assignments where the branch no longer exists in git.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
assignments: Current pool assignments
|
|
138
|
+
ctx: Erk context (for git.get_branch_head)
|
|
139
|
+
repo_root: Path to the repository root
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
List of (issue_type, message) tuples
|
|
143
|
+
"""
|
|
144
|
+
issues: list[SyncIssue] = []
|
|
145
|
+
for assignment in assignments:
|
|
146
|
+
# Check if branch exists by getting its head commit
|
|
147
|
+
if ctx.git.get_branch_head(repo_root, assignment.branch_name) is None:
|
|
148
|
+
msg = f"Slot {assignment.slot_name}: branch '{assignment.branch_name}' deleted"
|
|
149
|
+
issues.append(SyncIssue(code="missing-branch", message=msg))
|
|
150
|
+
return issues
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_git_worktree_mismatch(
|
|
154
|
+
state: PoolState,
|
|
155
|
+
git_slots: dict[str, WorktreeInfo],
|
|
156
|
+
) -> list[SyncIssue]:
|
|
157
|
+
"""Check for mismatches between pool state and git worktree registry.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
state: Pool state (assignments and known slots)
|
|
161
|
+
git_slots: Dict of slot names to WorktreeInfo from git
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of (issue_type, message) tuples
|
|
165
|
+
"""
|
|
166
|
+
issues: list[SyncIssue] = []
|
|
167
|
+
|
|
168
|
+
# Check assignments against git registry
|
|
169
|
+
for assignment in state.assignments:
|
|
170
|
+
if assignment.slot_name in git_slots:
|
|
171
|
+
wt = git_slots[assignment.slot_name]
|
|
172
|
+
# Check if branch matches
|
|
173
|
+
if wt.branch != assignment.branch_name:
|
|
174
|
+
msg = (
|
|
175
|
+
f"Slot {assignment.slot_name}: pool says '{assignment.branch_name}', "
|
|
176
|
+
f"git says '{wt.branch}'"
|
|
177
|
+
)
|
|
178
|
+
issues.append(SyncIssue(code="branch-mismatch", message=msg))
|
|
179
|
+
else:
|
|
180
|
+
# Slot is in pool.json but not in git worktree registry
|
|
181
|
+
issues.append(
|
|
182
|
+
SyncIssue(
|
|
183
|
+
code="git-registry-missing",
|
|
184
|
+
message=f"Slot {assignment.slot_name}: not in git worktree registry",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Check git registry for slots not in pool state
|
|
189
|
+
# Generate known slots from pool_size (same logic as slot list command)
|
|
190
|
+
known_slots = {generate_slot_name(i) for i in range(1, state.pool_size + 1)}
|
|
191
|
+
for slot_name, wt in git_slots.items():
|
|
192
|
+
if slot_name not in known_slots:
|
|
193
|
+
msg = f"Slot {slot_name}: in git registry (branch '{wt.branch}') but not in pool state"
|
|
194
|
+
issues.append(SyncIssue(code="untracked-worktree", message=msg))
|
|
195
|
+
|
|
196
|
+
return issues
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run_sync_diagnostics(ctx: ErkContext, state: PoolState, repo_root: Path) -> list[SyncIssue]:
|
|
200
|
+
"""Run all sync diagnostics and return issues found.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
ctx: Erk context
|
|
204
|
+
state: Pool state to check
|
|
205
|
+
repo_root: Repository root path
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List of (issue_type, message) tuples
|
|
209
|
+
"""
|
|
210
|
+
repo = discover_repo_context(ctx, repo_root)
|
|
211
|
+
|
|
212
|
+
# Get git worktrees
|
|
213
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
214
|
+
git_slots = _get_git_managed_slots(worktrees, repo.worktrees_dir)
|
|
215
|
+
|
|
216
|
+
# Get filesystem state
|
|
217
|
+
fs_slots = _find_erk_managed_dirs(repo.worktrees_dir, ctx.git)
|
|
218
|
+
|
|
219
|
+
# Run all checks
|
|
220
|
+
issues: list[SyncIssue] = []
|
|
221
|
+
issues.extend(_check_orphan_states(state.assignments, ctx))
|
|
222
|
+
issues.extend(_check_orphan_dirs(state, fs_slots))
|
|
223
|
+
issues.extend(_check_missing_branches(state.assignments, ctx, repo.root))
|
|
224
|
+
issues.extend(_check_git_worktree_mismatch(state, git_slots))
|
|
225
|
+
|
|
226
|
+
return issues
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@click.command("check")
|
|
230
|
+
@click.pass_obj
|
|
231
|
+
def slot_check(ctx: ErkContext) -> None:
|
|
232
|
+
"""Check pool state consistency with disk and git.
|
|
233
|
+
|
|
234
|
+
Reports drift between:
|
|
235
|
+
- Pool state (pool.json)
|
|
236
|
+
- Filesystem (worktree directories)
|
|
237
|
+
- Git worktree registry
|
|
238
|
+
|
|
239
|
+
This is a diagnostic command - it does not modify anything.
|
|
240
|
+
"""
|
|
241
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
242
|
+
|
|
243
|
+
# Load pool state
|
|
244
|
+
state = load_pool_state(repo.pool_json_path)
|
|
245
|
+
if state is None:
|
|
246
|
+
user_output("Error: No pool configured. Run `erk slot create` first.")
|
|
247
|
+
raise SystemExit(1) from None
|
|
248
|
+
|
|
249
|
+
# Get git worktrees
|
|
250
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
251
|
+
git_slots = _get_git_managed_slots(worktrees, repo.worktrees_dir)
|
|
252
|
+
|
|
253
|
+
# Get filesystem state
|
|
254
|
+
fs_slots = _find_erk_managed_dirs(repo.worktrees_dir, ctx.git)
|
|
255
|
+
|
|
256
|
+
# Run all checks
|
|
257
|
+
issues: list[SyncIssue] = []
|
|
258
|
+
issues.extend(_check_orphan_states(state.assignments, ctx))
|
|
259
|
+
issues.extend(_check_orphan_dirs(state, fs_slots))
|
|
260
|
+
issues.extend(_check_missing_branches(state.assignments, ctx, repo.root))
|
|
261
|
+
issues.extend(_check_git_worktree_mismatch(state, git_slots))
|
|
262
|
+
|
|
263
|
+
# Print report
|
|
264
|
+
user_output("Pool Check Report")
|
|
265
|
+
user_output("================")
|
|
266
|
+
user_output("")
|
|
267
|
+
user_output(f"Pool state: {len(state.assignments)} assignments")
|
|
268
|
+
user_output(f"Git worktrees: {len(worktrees)} registered ({len(git_slots)} erk-managed)")
|
|
269
|
+
user_output(f"Filesystem: {len(fs_slots)} slot directories")
|
|
270
|
+
user_output("")
|
|
271
|
+
|
|
272
|
+
if issues:
|
|
273
|
+
user_output("Issues Found:")
|
|
274
|
+
for issue in issues:
|
|
275
|
+
user_output(f" [{issue.code}] {issue.message}")
|
|
276
|
+
else:
|
|
277
|
+
user_output(click.style("✓ No issues found", fg="green"))
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Shared utilities for slot commands."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
from erk.core.worktree_pool import PoolState, SlotAssignment
|
|
9
|
+
from erk_shared.git.abc import Git
|
|
10
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
11
|
+
|
|
12
|
+
# Default pool configuration
|
|
13
|
+
DEFAULT_POOL_SIZE = 4
|
|
14
|
+
SLOT_NAME_PREFIX = "erk-managed-wt"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_slot_number(slot_name: str) -> str | None:
|
|
18
|
+
"""Extract slot number from slot name.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
slot_name: Slot name like "erk-managed-wt-03"
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Two-digit slot number (e.g., "03") or None if not in expected format
|
|
25
|
+
"""
|
|
26
|
+
if not slot_name.startswith(SLOT_NAME_PREFIX + "-"):
|
|
27
|
+
return None
|
|
28
|
+
suffix = slot_name[len(SLOT_NAME_PREFIX) + 1 :]
|
|
29
|
+
if len(suffix) != 2 or not suffix.isdigit():
|
|
30
|
+
return None
|
|
31
|
+
return suffix
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_placeholder_branch_name(slot_name: str) -> str | None:
|
|
35
|
+
"""Get placeholder branch name for a slot.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
slot_name: Slot name like "erk-managed-wt-03"
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Placeholder branch name like "__erk-slot-03-placeholder__",
|
|
42
|
+
or None if slot_name is not in expected format
|
|
43
|
+
"""
|
|
44
|
+
slot_number = extract_slot_number(slot_name)
|
|
45
|
+
if slot_number is None:
|
|
46
|
+
return None
|
|
47
|
+
return f"__erk-slot-{slot_number}-placeholder__"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_placeholder_branch(branch_name: str) -> bool:
|
|
51
|
+
"""Check if a branch name is an erk slot placeholder branch.
|
|
52
|
+
|
|
53
|
+
Placeholder branches have the format: __erk-slot-XX-placeholder__
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
branch_name: Branch name to check
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if branch_name matches the placeholder pattern
|
|
60
|
+
"""
|
|
61
|
+
return bool(re.match(r"^__erk-slot-\d+-placeholder__$", branch_name))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_pool_size(ctx: ErkContext) -> int:
|
|
65
|
+
"""Get effective pool size from config or default.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
ctx: Current erk context with local_config
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Configured pool size or DEFAULT_POOL_SIZE if not set
|
|
72
|
+
"""
|
|
73
|
+
if ctx.local_config is not None and ctx.local_config.pool_size is not None:
|
|
74
|
+
return ctx.local_config.pool_size
|
|
75
|
+
return DEFAULT_POOL_SIZE
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def generate_slot_name(slot_number: int) -> str:
|
|
79
|
+
"""Generate a slot name from a slot number.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
slot_number: 1-based slot number
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Formatted slot name like "erk-managed-wt-01"
|
|
86
|
+
"""
|
|
87
|
+
return f"{SLOT_NAME_PREFIX}-{slot_number:02d}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def find_next_available_slot(state: PoolState, worktrees_dir: Path | None) -> int | None:
|
|
91
|
+
"""Find the next available slot number for on-demand worktree creation.
|
|
92
|
+
|
|
93
|
+
This function finds a slot number that is:
|
|
94
|
+
1. Not currently assigned to a branch (not in state.assignments)
|
|
95
|
+
2. Not already initialized as a worktree (not in state.slots)
|
|
96
|
+
3. Does not have an orphaned directory on disk (if worktrees_dir provided)
|
|
97
|
+
|
|
98
|
+
This ensures on-demand creation only targets slots where no worktree
|
|
99
|
+
exists on disk.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
state: Current pool state
|
|
103
|
+
worktrees_dir: Directory containing worktrees, or None to skip disk check
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
1-based slot number if available, None if pool is full
|
|
107
|
+
"""
|
|
108
|
+
assigned_slots = {a.slot_name for a in state.assignments}
|
|
109
|
+
initialized_slots = {s.name for s in state.slots}
|
|
110
|
+
|
|
111
|
+
for slot_num in range(1, state.pool_size + 1):
|
|
112
|
+
slot_name = generate_slot_name(slot_num)
|
|
113
|
+
if slot_name not in assigned_slots and slot_name not in initialized_slots:
|
|
114
|
+
# Check if directory exists on disk (orphaned worktree)
|
|
115
|
+
if worktrees_dir is not None:
|
|
116
|
+
slot_path = worktrees_dir / slot_name
|
|
117
|
+
if slot_path.exists():
|
|
118
|
+
continue # Skip - directory exists but not tracked
|
|
119
|
+
return slot_num
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def find_inactive_slot(
|
|
125
|
+
state: PoolState,
|
|
126
|
+
git: Git,
|
|
127
|
+
repo_root: Path,
|
|
128
|
+
) -> tuple[str, Path] | None:
|
|
129
|
+
"""Find an available managed slot for reuse.
|
|
130
|
+
|
|
131
|
+
Searches for worktrees that exist but are not assigned.
|
|
132
|
+
Uses git as source of truth for which worktrees exist.
|
|
133
|
+
Prefers slots in order (lowest slot number first).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
state: Current pool state
|
|
137
|
+
git: Git gateway for worktree operations
|
|
138
|
+
repo_root: Repository root path
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Tuple of (slot_name, worktree_path) for an available slot,
|
|
142
|
+
or None if no inactive slot found
|
|
143
|
+
"""
|
|
144
|
+
assigned_slots = {a.slot_name for a in state.assignments}
|
|
145
|
+
|
|
146
|
+
# Get all worktrees from git (source of truth)
|
|
147
|
+
worktrees = git.list_worktrees(repo_root)
|
|
148
|
+
|
|
149
|
+
# Build lookup of slot_name -> worktree_path for managed slots
|
|
150
|
+
managed_worktrees: dict[str, Path] = {}
|
|
151
|
+
for wt in worktrees:
|
|
152
|
+
slot_name = wt.path.name
|
|
153
|
+
if extract_slot_number(slot_name) is not None:
|
|
154
|
+
managed_worktrees[slot_name] = wt.path
|
|
155
|
+
|
|
156
|
+
# Find first unassigned slot (by slot number order)
|
|
157
|
+
for slot_num in range(1, state.pool_size + 1):
|
|
158
|
+
slot_name = generate_slot_name(slot_num)
|
|
159
|
+
if slot_name in managed_worktrees and slot_name not in assigned_slots:
|
|
160
|
+
return (slot_name, managed_worktrees[slot_name])
|
|
161
|
+
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_slot_initialized(state: PoolState, slot_name: str) -> bool:
|
|
166
|
+
"""Check if a slot has been initialized.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
state: Current pool state
|
|
170
|
+
slot_name: Name of the slot to check
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if slot is in the initialized slots list
|
|
174
|
+
"""
|
|
175
|
+
return any(slot.name == slot_name for slot in state.slots)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def find_branch_assignment(state: PoolState, branch_name: str) -> SlotAssignment | None:
|
|
179
|
+
"""Find if a branch is already assigned to a slot.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
state: Current pool state
|
|
183
|
+
branch_name: Branch to search for
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
SlotAssignment if found, None otherwise
|
|
187
|
+
"""
|
|
188
|
+
for assignment in state.assignments:
|
|
189
|
+
if assignment.branch_name == branch_name:
|
|
190
|
+
return assignment
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def find_assignment_by_worktree(state: PoolState, git: Git, cwd: Path) -> SlotAssignment | None:
|
|
195
|
+
"""Find if cwd is within a managed slot using git.
|
|
196
|
+
|
|
197
|
+
Uses git to determine the worktree root of cwd, then matches exactly
|
|
198
|
+
against known slot assignments. This is more reliable than path
|
|
199
|
+
comparisons which can fail with symlinks, relative paths, etc.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
state: Current pool state
|
|
203
|
+
git: Git gateway for repository operations
|
|
204
|
+
cwd: Current working directory
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
SlotAssignment if cwd is within a managed slot, None otherwise
|
|
208
|
+
"""
|
|
209
|
+
worktree_root = git.get_repository_root(cwd)
|
|
210
|
+
for assignment in state.assignments:
|
|
211
|
+
if assignment.worktree_path == worktree_root:
|
|
212
|
+
return assignment
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def find_oldest_assignment(state: PoolState) -> SlotAssignment | None:
|
|
217
|
+
"""Find the oldest assignment by assigned_at timestamp.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
state: Current pool state
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The oldest SlotAssignment, or None if no assignments
|
|
224
|
+
"""
|
|
225
|
+
if not state.assignments:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
oldest: SlotAssignment | None = None
|
|
229
|
+
for assignment in state.assignments:
|
|
230
|
+
if oldest is None or assignment.assigned_at < oldest.assigned_at:
|
|
231
|
+
oldest = assignment
|
|
232
|
+
return oldest
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def display_pool_assignments(state: PoolState) -> None:
|
|
236
|
+
"""Display current pool assignments to user.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
state: Current pool state
|
|
240
|
+
"""
|
|
241
|
+
user_output("\nCurrent pool assignments:")
|
|
242
|
+
for assignment in sorted(state.assignments, key=lambda a: a.assigned_at):
|
|
243
|
+
slot = assignment.slot_name
|
|
244
|
+
branch = assignment.branch_name
|
|
245
|
+
assigned = assignment.assigned_at
|
|
246
|
+
user_output(f" {slot}: {branch} (assigned {assigned})")
|
|
247
|
+
user_output("")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def handle_pool_full_interactive(
|
|
251
|
+
state: PoolState,
|
|
252
|
+
force: bool,
|
|
253
|
+
is_tty: bool,
|
|
254
|
+
) -> SlotAssignment | None:
|
|
255
|
+
"""Handle pool-full condition: prompt to unassign oldest or error.
|
|
256
|
+
|
|
257
|
+
When the pool is full:
|
|
258
|
+
- If --force: auto-unassign the oldest assignment
|
|
259
|
+
- If interactive (TTY): show assignments and prompt user
|
|
260
|
+
- If non-interactive (no TTY): error with instructions
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
state: Current pool state
|
|
264
|
+
force: If True, auto-unassign oldest without prompting
|
|
265
|
+
is_tty: Whether running in an interactive terminal
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
SlotAssignment to unassign, or None if user declined/error
|
|
269
|
+
"""
|
|
270
|
+
oldest = find_oldest_assignment(state)
|
|
271
|
+
if oldest is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
if force:
|
|
275
|
+
user_output(f"Pool is full. --force specified, unassigning oldest: {oldest.branch_name}")
|
|
276
|
+
return oldest
|
|
277
|
+
|
|
278
|
+
if not is_tty:
|
|
279
|
+
user_output(
|
|
280
|
+
f"Error: Pool is full ({state.pool_size} slots). "
|
|
281
|
+
"Use --force to auto-unassign the oldest branch, "
|
|
282
|
+
"or run `erk slot list` to see assignments."
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Interactive mode: show assignments and prompt
|
|
287
|
+
display_pool_assignments(state)
|
|
288
|
+
user_output(f"Pool is full ({state.pool_size} slots).")
|
|
289
|
+
user_output(f"Oldest assignment: {oldest.branch_name} ({oldest.slot_name})")
|
|
290
|
+
|
|
291
|
+
if user_confirm(f"Unassign '{oldest.branch_name}' to make room?", default=False):
|
|
292
|
+
return oldest
|
|
293
|
+
|
|
294
|
+
user_output("Aborted.")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cleanup_worktree_artifacts(worktree_path: Path) -> None:
|
|
299
|
+
"""Remove stale artifacts from a worktree before reuse.
|
|
300
|
+
|
|
301
|
+
Cleans up .impl/ and .erk/scratch/ folders which persist across
|
|
302
|
+
branch switches since they are in .gitignore.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
worktree_path: Path to the worktree to clean up
|
|
306
|
+
"""
|
|
307
|
+
impl_folder = worktree_path / ".impl"
|
|
308
|
+
scratch_folder = worktree_path / ".erk" / "scratch"
|
|
309
|
+
|
|
310
|
+
if impl_folder.exists():
|
|
311
|
+
shutil.rmtree(impl_folder)
|
|
312
|
+
|
|
313
|
+
if scratch_folder.exists():
|
|
314
|
+
shutil.rmtree(scratch_folder)
|