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,404 @@
|
|
|
1
|
+
"""Data models for status information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class WorktreeDisplayInfo:
|
|
11
|
+
"""Worktree information for display/presentation purposes.
|
|
12
|
+
|
|
13
|
+
This represents worktree data for status rendering and display.
|
|
14
|
+
For infrastructure-layer worktree data, see erk.core.gitops.WorktreeInfo.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
path: Path
|
|
19
|
+
branch: str | None
|
|
20
|
+
is_root: bool
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def root(path: Path, branch: str = "main", name: str = "root") -> WorktreeDisplayInfo:
|
|
24
|
+
"""Create root worktree for test display.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path: Path to the root worktree
|
|
28
|
+
branch: Branch name (default: "main")
|
|
29
|
+
name: Display name (default: "root")
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
WorktreeDisplayInfo with is_root=True
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
Before (4 lines):
|
|
36
|
+
worktree = WorktreeDisplayInfo(
|
|
37
|
+
name="root", path=repo_root, branch="main", is_root=True
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
After (1 line):
|
|
41
|
+
worktree = WorktreeDisplayInfo.root(repo_root)
|
|
42
|
+
"""
|
|
43
|
+
return WorktreeDisplayInfo(path=path, branch=branch, is_root=True, name=name)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def feature(path: Path, branch: str, name: str | None = None) -> WorktreeDisplayInfo:
|
|
47
|
+
"""Create feature worktree for test display.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: Path to the feature worktree
|
|
51
|
+
branch: Branch name
|
|
52
|
+
name: Display name (default: uses path.name)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
WorktreeDisplayInfo with is_root=False
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
Before (4 lines):
|
|
59
|
+
worktree = WorktreeDisplayInfo(
|
|
60
|
+
name="my-feature", path=feature_wt, branch="feature", is_root=False
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
After (1 line):
|
|
64
|
+
worktree = WorktreeDisplayInfo.feature(feature_wt, "feature")
|
|
65
|
+
"""
|
|
66
|
+
display_name = name if name else path.name
|
|
67
|
+
return WorktreeDisplayInfo(path=path, branch=branch, is_root=False, name=display_name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class CommitInfo:
|
|
72
|
+
"""Information about a git commit."""
|
|
73
|
+
|
|
74
|
+
sha: str
|
|
75
|
+
message: str
|
|
76
|
+
author: str
|
|
77
|
+
date: str
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def test_commit(
|
|
81
|
+
sha: str = "abc1234",
|
|
82
|
+
message: str = "Test commit",
|
|
83
|
+
author: str = "Test User",
|
|
84
|
+
date: str = "1 hour ago",
|
|
85
|
+
) -> CommitInfo:
|
|
86
|
+
"""Create a commit for tests with sensible defaults.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
sha: Commit SHA (default: "abc1234")
|
|
90
|
+
message: Commit message (default: "Test commit")
|
|
91
|
+
author: Commit author (default: "Test User")
|
|
92
|
+
date: Commit date (default: "1 hour ago")
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
CommitInfo with all fields populated
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
Before (5 lines):
|
|
99
|
+
recent_commits = [
|
|
100
|
+
CommitInfo(sha="abc1234", message="Initial commit",
|
|
101
|
+
author="Test User", date="1 hour ago"),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
After (1 line):
|
|
105
|
+
recent_commits = [CommitInfo.test_commit()]
|
|
106
|
+
"""
|
|
107
|
+
return CommitInfo(sha=sha, message=message, author=author, date=date)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class GitStatus:
|
|
112
|
+
"""Git repository status information."""
|
|
113
|
+
|
|
114
|
+
branch: str | None
|
|
115
|
+
clean: bool
|
|
116
|
+
ahead: int
|
|
117
|
+
behind: int
|
|
118
|
+
staged_files: list[str]
|
|
119
|
+
modified_files: list[str]
|
|
120
|
+
untracked_files: list[str]
|
|
121
|
+
recent_commits: list[CommitInfo]
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def clean_status(branch: str, ahead: int = 0, behind: int = 0) -> GitStatus:
|
|
125
|
+
"""Create clean status (no changes) for tests.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
branch: Branch name
|
|
129
|
+
ahead: Commits ahead of remote (default: 0)
|
|
130
|
+
behind: Commits behind remote (default: 0)
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
GitStatus with clean=True and empty file lists
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
Before (8 lines):
|
|
137
|
+
status = GitStatus(
|
|
138
|
+
branch="test",
|
|
139
|
+
clean=True,
|
|
140
|
+
ahead=0,
|
|
141
|
+
behind=0,
|
|
142
|
+
staged_files=[],
|
|
143
|
+
modified_files=[],
|
|
144
|
+
untracked_files=[],
|
|
145
|
+
recent_commits=[],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
After (1 line):
|
|
149
|
+
status = GitStatus.clean_status("test")
|
|
150
|
+
"""
|
|
151
|
+
return GitStatus(
|
|
152
|
+
branch=branch,
|
|
153
|
+
clean=True,
|
|
154
|
+
ahead=ahead,
|
|
155
|
+
behind=behind,
|
|
156
|
+
staged_files=[],
|
|
157
|
+
modified_files=[],
|
|
158
|
+
untracked_files=[],
|
|
159
|
+
recent_commits=[],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def dirty_status(
|
|
164
|
+
branch: str,
|
|
165
|
+
*,
|
|
166
|
+
modified: list[str] | None = None,
|
|
167
|
+
staged: list[str] | None = None,
|
|
168
|
+
untracked: list[str] | None = None,
|
|
169
|
+
ahead: int = 0,
|
|
170
|
+
behind: int = 0,
|
|
171
|
+
) -> GitStatus:
|
|
172
|
+
"""Create dirty status (with changes) for tests.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
branch: Branch name
|
|
176
|
+
modified: Modified files (default: [])
|
|
177
|
+
staged: Staged files (default: [])
|
|
178
|
+
untracked: Untracked files (default: [])
|
|
179
|
+
ahead: Commits ahead of remote (default: 0)
|
|
180
|
+
behind: Commits behind remote (default: 0)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
GitStatus with clean=False and specified file lists
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
Before (9 lines):
|
|
187
|
+
status = GitStatus(
|
|
188
|
+
branch="feature",
|
|
189
|
+
clean=False,
|
|
190
|
+
ahead=0,
|
|
191
|
+
behind=0,
|
|
192
|
+
staged_files=[],
|
|
193
|
+
modified_files=["file.py"],
|
|
194
|
+
untracked_files=[],
|
|
195
|
+
recent_commits=[],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
After (1 line):
|
|
199
|
+
status = GitStatus.dirty_status("feature", modified=["file.py"])
|
|
200
|
+
"""
|
|
201
|
+
return GitStatus(
|
|
202
|
+
branch=branch,
|
|
203
|
+
clean=False,
|
|
204
|
+
ahead=ahead,
|
|
205
|
+
behind=behind,
|
|
206
|
+
staged_files=staged or [],
|
|
207
|
+
modified_files=modified or [],
|
|
208
|
+
untracked_files=untracked or [],
|
|
209
|
+
recent_commits=[],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def with_commits(branch: str, commits: list[CommitInfo], clean: bool = True) -> GitStatus:
|
|
214
|
+
"""Create status with commit history for tests.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
branch: Branch name
|
|
218
|
+
commits: List of recent commits
|
|
219
|
+
clean: Whether working tree is clean (default: True)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
GitStatus with commits populated
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
Before (9 lines):
|
|
226
|
+
status = GitStatus(
|
|
227
|
+
branch="main",
|
|
228
|
+
clean=True,
|
|
229
|
+
ahead=0,
|
|
230
|
+
behind=0,
|
|
231
|
+
staged_files=[],
|
|
232
|
+
modified_files=[],
|
|
233
|
+
untracked_files=[],
|
|
234
|
+
recent_commits=[commit1, commit2],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
After (1 line):
|
|
238
|
+
status = GitStatus.with_commits("main", [commit1, commit2])
|
|
239
|
+
"""
|
|
240
|
+
return GitStatus(
|
|
241
|
+
branch=branch,
|
|
242
|
+
clean=clean,
|
|
243
|
+
ahead=0,
|
|
244
|
+
behind=0,
|
|
245
|
+
staged_files=[],
|
|
246
|
+
modified_files=[],
|
|
247
|
+
untracked_files=[],
|
|
248
|
+
recent_commits=commits,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True)
|
|
253
|
+
class StackPosition:
|
|
254
|
+
"""Worktree stack position information."""
|
|
255
|
+
|
|
256
|
+
stack: list[str]
|
|
257
|
+
current_branch: str
|
|
258
|
+
parent_branch: str | None
|
|
259
|
+
children_branches: list[str]
|
|
260
|
+
is_trunk: bool
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True)
|
|
264
|
+
class PullRequestStatus:
|
|
265
|
+
"""Pull request status information."""
|
|
266
|
+
|
|
267
|
+
number: int
|
|
268
|
+
title: str | None # May not be available from all data sources
|
|
269
|
+
state: str
|
|
270
|
+
is_draft: bool
|
|
271
|
+
url: str
|
|
272
|
+
checks_passing: bool | None
|
|
273
|
+
reviews: list[str] | None # May not be available from all data sources
|
|
274
|
+
ready_to_merge: bool
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass(frozen=True)
|
|
278
|
+
class EnvironmentStatus:
|
|
279
|
+
"""Environment variables status."""
|
|
280
|
+
|
|
281
|
+
variables: dict[str, str]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass(frozen=True)
|
|
285
|
+
class DependencyStatus:
|
|
286
|
+
"""Dependency status for various language ecosystems."""
|
|
287
|
+
|
|
288
|
+
language: str
|
|
289
|
+
up_to_date: bool
|
|
290
|
+
outdated_count: int
|
|
291
|
+
details: str | None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass(frozen=True)
|
|
295
|
+
class PlanStatus:
|
|
296
|
+
"""Status of .impl/ folder and enriched plans."""
|
|
297
|
+
|
|
298
|
+
exists: bool
|
|
299
|
+
path: Path | None
|
|
300
|
+
summary: str | None
|
|
301
|
+
line_count: int
|
|
302
|
+
first_lines: list[str]
|
|
303
|
+
format: str # "folder" or "none"
|
|
304
|
+
enriched_plan_path: Path | None = None # Path to enriched plan file
|
|
305
|
+
enriched_plan_filename: str | None = None # Filename of enriched plan
|
|
306
|
+
issue_number: int | None = None # GitHub issue number if linked
|
|
307
|
+
issue_url: str | None = None # GitHub issue URL if linked
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass(frozen=True)
|
|
311
|
+
class StatusData:
|
|
312
|
+
"""Container for all status information."""
|
|
313
|
+
|
|
314
|
+
worktree_info: WorktreeDisplayInfo
|
|
315
|
+
git_status: GitStatus | None
|
|
316
|
+
stack_position: StackPosition | None
|
|
317
|
+
pr_status: PullRequestStatus | None
|
|
318
|
+
environment: EnvironmentStatus | None
|
|
319
|
+
dependencies: DependencyStatus | None
|
|
320
|
+
plan: PlanStatus | None
|
|
321
|
+
related_worktrees: list[WorktreeDisplayInfo]
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def minimal(worktree_info: WorktreeDisplayInfo) -> StatusData:
|
|
325
|
+
"""Create minimal status data (all optional fields None) for tests.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
worktree_info: Worktree display information
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
StatusData with only worktree_info set, all other fields None/empty
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
Before (9 lines):
|
|
335
|
+
status_data = StatusData(
|
|
336
|
+
worktree_info=WorktreeDisplayInfo(
|
|
337
|
+
name="my-feature", path=wt_path, branch="feature", is_root=False
|
|
338
|
+
),
|
|
339
|
+
git_status=None,
|
|
340
|
+
stack_position=None,
|
|
341
|
+
pr_status=None,
|
|
342
|
+
environment=None,
|
|
343
|
+
dependencies=None,
|
|
344
|
+
plan=None,
|
|
345
|
+
related_worktrees=[],
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
After (2 lines):
|
|
349
|
+
worktree_info = WorktreeDisplayInfo.feature(wt_path, "feature")
|
|
350
|
+
status_data = StatusData.minimal(worktree_info)
|
|
351
|
+
"""
|
|
352
|
+
return StatusData(
|
|
353
|
+
worktree_info=worktree_info,
|
|
354
|
+
git_status=None,
|
|
355
|
+
stack_position=None,
|
|
356
|
+
pr_status=None,
|
|
357
|
+
environment=None,
|
|
358
|
+
dependencies=None,
|
|
359
|
+
plan=None,
|
|
360
|
+
related_worktrees=[],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def with_git_status(worktree_info: WorktreeDisplayInfo, git_status: GitStatus) -> StatusData:
|
|
365
|
+
"""Create status data with git status populated.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
worktree_info: Worktree display information
|
|
369
|
+
git_status: Git status information
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
StatusData with worktree_info and git_status set, other fields None/empty
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
Before (11 lines):
|
|
376
|
+
status_data = StatusData(
|
|
377
|
+
worktree_info=WorktreeDisplayInfo(
|
|
378
|
+
name="root", path=repo_root, branch="main", is_root=True
|
|
379
|
+
),
|
|
380
|
+
git_status=GitStatus.clean_status("main"),
|
|
381
|
+
stack_position=None,
|
|
382
|
+
pr_status=None,
|
|
383
|
+
environment=None,
|
|
384
|
+
dependencies=None,
|
|
385
|
+
plan=None,
|
|
386
|
+
related_worktrees=[],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
After (2 lines):
|
|
390
|
+
worktree_info = WorktreeDisplayInfo.root(repo_root)
|
|
391
|
+
status_data = StatusData.with_git_status(
|
|
392
|
+
worktree_info, GitStatus.clean_status("main")
|
|
393
|
+
)
|
|
394
|
+
"""
|
|
395
|
+
return StatusData(
|
|
396
|
+
worktree_info=worktree_info,
|
|
397
|
+
git_status=git_status,
|
|
398
|
+
stack_position=None,
|
|
399
|
+
pr_status=None,
|
|
400
|
+
environment=None,
|
|
401
|
+
dependencies=None,
|
|
402
|
+
plan=None,
|
|
403
|
+
related_worktrees=[],
|
|
404
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Orchestrator for collecting and assembling status information."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
from erk.status.collectors.base import StatusCollector
|
|
9
|
+
from erk.status.models.status_data import StatusData, WorktreeDisplayInfo
|
|
10
|
+
from erk_shared.gateway.parallel.abc import ParallelTaskRunner
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StatusOrchestrator:
|
|
16
|
+
"""Coordinates all status collectors and assembles final data.
|
|
17
|
+
|
|
18
|
+
The orchestrator runs collectors in parallel with timeouts to ensure
|
|
19
|
+
responsive output even if some collectors are slow or fail.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
collectors: list[StatusCollector],
|
|
25
|
+
*,
|
|
26
|
+
timeout_seconds: float = 2.0,
|
|
27
|
+
runner: ParallelTaskRunner,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Create a status orchestrator.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
collectors: List of status collectors to run
|
|
33
|
+
timeout_seconds: Maximum time to wait for each collector (default: 2.0)
|
|
34
|
+
runner: Parallel task runner for executing collectors
|
|
35
|
+
"""
|
|
36
|
+
self.collectors = collectors
|
|
37
|
+
self.timeout_seconds = timeout_seconds
|
|
38
|
+
self.runner = runner
|
|
39
|
+
|
|
40
|
+
def collect_status(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> StatusData:
|
|
41
|
+
"""Collect all status information in parallel.
|
|
42
|
+
|
|
43
|
+
Each collector runs in its own thread with a timeout. Failed or slow
|
|
44
|
+
collectors will return None for their section.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ctx: Erk context with operations
|
|
48
|
+
worktree_path: Path to the worktree
|
|
49
|
+
repo_root: Path to repository root
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
StatusData with all collected information
|
|
53
|
+
"""
|
|
54
|
+
# Determine worktree info
|
|
55
|
+
worktree_info = self._get_worktree_info(ctx, worktree_path, repo_root)
|
|
56
|
+
|
|
57
|
+
# Build tasks for available collectors
|
|
58
|
+
tasks: dict[str, Callable[[], object]] = {}
|
|
59
|
+
for collector in self.collectors:
|
|
60
|
+
if collector.is_available(ctx, worktree_path):
|
|
61
|
+
# Create closure that captures current values
|
|
62
|
+
def make_task(c=collector):
|
|
63
|
+
return lambda: c.collect(ctx, worktree_path, repo_root)
|
|
64
|
+
|
|
65
|
+
tasks[collector.name] = make_task()
|
|
66
|
+
|
|
67
|
+
# Run collectors in parallel via runner
|
|
68
|
+
results = self.runner.run_parallel(tasks, self.timeout_seconds)
|
|
69
|
+
|
|
70
|
+
# Get related worktrees
|
|
71
|
+
related_worktrees = self._get_related_worktrees(ctx, repo_root, worktree_path)
|
|
72
|
+
|
|
73
|
+
# Assemble StatusData - cast results to expected types
|
|
74
|
+
# Results are either the correct type or None (from collector failures)
|
|
75
|
+
from erk.status.models.status_data import (
|
|
76
|
+
DependencyStatus,
|
|
77
|
+
EnvironmentStatus,
|
|
78
|
+
GitStatus,
|
|
79
|
+
PlanStatus,
|
|
80
|
+
PullRequestStatus,
|
|
81
|
+
StackPosition,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
git_result = results.get("git")
|
|
85
|
+
stack_result = results.get("stack")
|
|
86
|
+
pr_result = results.get("pr")
|
|
87
|
+
env_result = results.get("environment")
|
|
88
|
+
deps_result = results.get("dependencies")
|
|
89
|
+
plan_result = results.get("plan")
|
|
90
|
+
|
|
91
|
+
return StatusData(
|
|
92
|
+
worktree_info=worktree_info,
|
|
93
|
+
git_status=git_result if isinstance(git_result, GitStatus) else None,
|
|
94
|
+
stack_position=stack_result if isinstance(stack_result, StackPosition) else None,
|
|
95
|
+
pr_status=pr_result if isinstance(pr_result, PullRequestStatus) else None,
|
|
96
|
+
environment=env_result if isinstance(env_result, EnvironmentStatus) else None,
|
|
97
|
+
dependencies=deps_result if isinstance(deps_result, DependencyStatus) else None,
|
|
98
|
+
plan=plan_result if isinstance(plan_result, PlanStatus) else None,
|
|
99
|
+
related_worktrees=related_worktrees,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _get_worktree_info(
|
|
103
|
+
self, ctx: ErkContext, worktree_path: Path, repo_root: Path
|
|
104
|
+
) -> WorktreeDisplayInfo:
|
|
105
|
+
"""Get basic worktree information.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
ctx: Erk context
|
|
109
|
+
worktree_path: Path to worktree
|
|
110
|
+
repo_root: Path to repository root
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
WorktreeDisplayInfo with basic information
|
|
114
|
+
"""
|
|
115
|
+
# Check paths exist before resolution to avoid OSError
|
|
116
|
+
is_root = False
|
|
117
|
+
if worktree_path.exists() and repo_root.exists():
|
|
118
|
+
is_root = worktree_path.resolve() == repo_root.resolve()
|
|
119
|
+
|
|
120
|
+
name = "root" if is_root else worktree_path.name
|
|
121
|
+
branch = ctx.git.get_current_branch(worktree_path)
|
|
122
|
+
|
|
123
|
+
return WorktreeDisplayInfo(name=name, path=worktree_path, branch=branch, is_root=is_root)
|
|
124
|
+
|
|
125
|
+
def _get_related_worktrees(
|
|
126
|
+
self, ctx: ErkContext, repo_root: Path, current_path: Path
|
|
127
|
+
) -> list[WorktreeDisplayInfo]:
|
|
128
|
+
"""Get list of other worktrees in the repository.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
ctx: Erk context
|
|
132
|
+
repo_root: Path to repository root
|
|
133
|
+
current_path: Path to current worktree (excluded from results)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of WorktreeDisplayInfo for other worktrees
|
|
137
|
+
"""
|
|
138
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
139
|
+
|
|
140
|
+
# Check paths exist before resolution to avoid OSError
|
|
141
|
+
if not current_path.exists():
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
current_resolved = current_path.resolve()
|
|
145
|
+
|
|
146
|
+
related = []
|
|
147
|
+
for wt in worktrees:
|
|
148
|
+
# Skip if worktree path doesn't exist
|
|
149
|
+
if not wt.path.exists():
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
wt_resolved = wt.path.resolve()
|
|
153
|
+
|
|
154
|
+
# Skip current worktree
|
|
155
|
+
if wt_resolved == current_resolved:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# Determine if this is the root worktree
|
|
159
|
+
is_root = False
|
|
160
|
+
if repo_root.exists():
|
|
161
|
+
is_root = wt_resolved == repo_root.resolve()
|
|
162
|
+
|
|
163
|
+
name = "root" if is_root else wt.path.name
|
|
164
|
+
|
|
165
|
+
related.append(
|
|
166
|
+
WorktreeDisplayInfo(name=name, path=wt.path, branch=wt.branch, is_root=is_root)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return related
|