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,440 @@
|
|
|
1
|
+
"""Command to display chronological event log for a plan."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal, TypeAlias, TypedDict
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from erk.cli.core import discover_repo_context
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
13
|
+
from erk_shared.github.metadata.core import parse_metadata_blocks
|
|
14
|
+
from erk_shared.output.output import user_output
|
|
15
|
+
|
|
16
|
+
# Event type literals
|
|
17
|
+
EventType: TypeAlias = Literal[
|
|
18
|
+
"plan-created",
|
|
19
|
+
"submission-queued",
|
|
20
|
+
"workflow-started",
|
|
21
|
+
"implementation-status",
|
|
22
|
+
"plan-retry",
|
|
23
|
+
"worktree-created",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Event metadata types
|
|
28
|
+
class PlanCreatedMetadata(TypedDict, total=False):
|
|
29
|
+
"""Metadata for plan_created event."""
|
|
30
|
+
|
|
31
|
+
worktree_name: str
|
|
32
|
+
issue_number: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SubmissionQueuedMetadata(TypedDict, total=False):
|
|
36
|
+
"""Metadata for submission_queued event."""
|
|
37
|
+
|
|
38
|
+
status: str
|
|
39
|
+
submitted_by: str
|
|
40
|
+
expected_workflow: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WorkflowStartedMetadata(TypedDict, total=False):
|
|
44
|
+
"""Metadata for workflow_started event."""
|
|
45
|
+
|
|
46
|
+
status: str
|
|
47
|
+
workflow_run_id: str
|
|
48
|
+
workflow_run_url: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ImplementationStatusMetadata(TypedDict, total=False):
|
|
52
|
+
"""Metadata for implementation_status event."""
|
|
53
|
+
|
|
54
|
+
status: str
|
|
55
|
+
completed_steps: int
|
|
56
|
+
total_steps: int
|
|
57
|
+
step_description: str
|
|
58
|
+
worktree: str
|
|
59
|
+
branch: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PlanRetryMetadata(TypedDict, total=False):
|
|
63
|
+
"""Metadata for plan_retry event."""
|
|
64
|
+
|
|
65
|
+
retry_count: int
|
|
66
|
+
triggered_by: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WorktreeCreatedMetadata(TypedDict, total=False):
|
|
70
|
+
"""Metadata for worktree_created event."""
|
|
71
|
+
|
|
72
|
+
worktree_name: str
|
|
73
|
+
branch_name: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Union type for all metadata types
|
|
77
|
+
EventMetadata: TypeAlias = (
|
|
78
|
+
PlanCreatedMetadata
|
|
79
|
+
| SubmissionQueuedMetadata
|
|
80
|
+
| WorkflowStartedMetadata
|
|
81
|
+
| ImplementationStatusMetadata
|
|
82
|
+
| PlanRetryMetadata
|
|
83
|
+
| WorktreeCreatedMetadata
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Event(TypedDict):
|
|
88
|
+
"""Structured event with timestamp, type, and metadata."""
|
|
89
|
+
|
|
90
|
+
timestamp: str
|
|
91
|
+
event_type: EventType
|
|
92
|
+
metadata: EventMetadata
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Type alias for event extractor functions
|
|
96
|
+
EventExtractor: TypeAlias = Callable[[dict], Event | None]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@click.command("log")
|
|
100
|
+
@click.argument("identifier", type=str)
|
|
101
|
+
@click.option(
|
|
102
|
+
"--json",
|
|
103
|
+
"output_json",
|
|
104
|
+
is_flag=True,
|
|
105
|
+
help="Output events as JSON instead of human-readable timeline",
|
|
106
|
+
)
|
|
107
|
+
@click.pass_obj
|
|
108
|
+
def plan_log(ctx: ErkContext, identifier: str, output_json: bool) -> None:
|
|
109
|
+
"""Display chronological event log for a plan.
|
|
110
|
+
|
|
111
|
+
Shows all events from plan creation through submission, workflow execution,
|
|
112
|
+
implementation progress, and completion. Events are displayed in chronological
|
|
113
|
+
order (oldest first).
|
|
114
|
+
|
|
115
|
+
IDENTIFIER can be an issue number (e.g., "42") or a worktree name.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
|
|
119
|
+
\b
|
|
120
|
+
# View timeline for plan 42
|
|
121
|
+
$ erk plan log 42
|
|
122
|
+
|
|
123
|
+
# View events as JSON for scripting
|
|
124
|
+
$ erk plan log 42 --json
|
|
125
|
+
|
|
126
|
+
# View by worktree name
|
|
127
|
+
$ erk plan log erk-add-feature
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
131
|
+
ensure_erk_metadata_dir(repo)
|
|
132
|
+
repo_root = repo.root
|
|
133
|
+
|
|
134
|
+
# Resolve plan identifier to issue number
|
|
135
|
+
plan = ctx.plan_store.get_plan(repo_root, identifier)
|
|
136
|
+
|
|
137
|
+
# Convert plan identifier to issue number (GitHub: issue number as string)
|
|
138
|
+
if not plan.plan_identifier.isdigit():
|
|
139
|
+
user_output(
|
|
140
|
+
click.style("Error: ", fg="red")
|
|
141
|
+
+ f"Invalid plan identifier '{plan.plan_identifier}': not a valid issue number"
|
|
142
|
+
)
|
|
143
|
+
raise SystemExit(1)
|
|
144
|
+
|
|
145
|
+
issue_number = int(plan.plan_identifier)
|
|
146
|
+
|
|
147
|
+
# Fetch all comments for the plan issue
|
|
148
|
+
comment_bodies = ctx.issues.get_issue_comments(repo_root, issue_number)
|
|
149
|
+
|
|
150
|
+
# Extract events from all comments
|
|
151
|
+
events = _extract_events_from_comments(comment_bodies)
|
|
152
|
+
|
|
153
|
+
# Sort events chronologically (oldest first)
|
|
154
|
+
events.sort(key=lambda e: e["timestamp"])
|
|
155
|
+
|
|
156
|
+
# Output events
|
|
157
|
+
if output_json:
|
|
158
|
+
_output_json(events)
|
|
159
|
+
else:
|
|
160
|
+
_output_timeline(events, issue_number)
|
|
161
|
+
|
|
162
|
+
except (RuntimeError, ValueError) as e:
|
|
163
|
+
user_output(click.style("Error: ", fg="red") + str(e))
|
|
164
|
+
raise SystemExit(1) from e
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _extract_events_from_comments(comment_bodies: list[str]) -> list[Event]:
|
|
168
|
+
"""Extract all events from comment metadata blocks.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
comment_bodies: List of GitHub issue comment bodies
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of Event objects with timestamp, event_type, and metadata fields
|
|
175
|
+
"""
|
|
176
|
+
events: list[Event] = []
|
|
177
|
+
|
|
178
|
+
for comment_body in comment_bodies:
|
|
179
|
+
blocks = parse_metadata_blocks(comment_body)
|
|
180
|
+
|
|
181
|
+
for block in blocks:
|
|
182
|
+
event = _block_to_event(block.key, block.data)
|
|
183
|
+
if event is not None:
|
|
184
|
+
events.append(event)
|
|
185
|
+
|
|
186
|
+
return events
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _block_to_event(key: str, data: dict) -> Event | None:
|
|
190
|
+
"""Convert a metadata block to an Event.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
key: Metadata block key (e.g., "erk-plan", "submission-queued")
|
|
194
|
+
data: Metadata block data
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Event object or None if block type is not recognized
|
|
198
|
+
"""
|
|
199
|
+
# Map block types to event extractors
|
|
200
|
+
extractors: dict[str, EventExtractor] = {
|
|
201
|
+
"erk-plan": _extract_plan_created_event,
|
|
202
|
+
"submission-queued": _extract_submission_queued_event,
|
|
203
|
+
"workflow-started": _extract_workflow_started_event,
|
|
204
|
+
"erk-implementation-status": _extract_implementation_status_event,
|
|
205
|
+
"plan-retry": _extract_plan_retry_event,
|
|
206
|
+
"erk-worktree-creation": _extract_worktree_creation_event,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extractor = extractors.get(key)
|
|
210
|
+
if extractor is None:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
return extractor(data)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _extract_plan_created_event(data: dict) -> Event | None:
|
|
217
|
+
"""Extract plan creation event from erk-plan block."""
|
|
218
|
+
timestamp = data.get("timestamp")
|
|
219
|
+
if not timestamp:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
metadata: PlanCreatedMetadata = {}
|
|
223
|
+
if "worktree_name" in data:
|
|
224
|
+
metadata["worktree_name"] = data["worktree_name"]
|
|
225
|
+
if "issue_number" in data:
|
|
226
|
+
metadata["issue_number"] = data["issue_number"]
|
|
227
|
+
|
|
228
|
+
return Event(
|
|
229
|
+
timestamp=timestamp,
|
|
230
|
+
event_type="plan-created",
|
|
231
|
+
metadata=metadata,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _extract_submission_queued_event(data: dict) -> Event | None:
|
|
236
|
+
"""Extract submission queued event from submission-queued block."""
|
|
237
|
+
timestamp = data.get("queued_at")
|
|
238
|
+
if not timestamp:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
metadata: SubmissionQueuedMetadata = {"status": "queued"}
|
|
242
|
+
if "submitted_by" in data:
|
|
243
|
+
metadata["submitted_by"] = data["submitted_by"]
|
|
244
|
+
if "expected_workflow" in data:
|
|
245
|
+
metadata["expected_workflow"] = data["expected_workflow"]
|
|
246
|
+
|
|
247
|
+
return Event(
|
|
248
|
+
timestamp=timestamp,
|
|
249
|
+
event_type="submission-queued",
|
|
250
|
+
metadata=metadata,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _extract_workflow_started_event(data: dict) -> Event | None:
|
|
255
|
+
"""Extract workflow started event from workflow-started block."""
|
|
256
|
+
timestamp = data.get("started_at")
|
|
257
|
+
if not timestamp:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
metadata: WorkflowStartedMetadata = {"status": "started"}
|
|
261
|
+
if "workflow_run_id" in data:
|
|
262
|
+
metadata["workflow_run_id"] = data["workflow_run_id"]
|
|
263
|
+
if "workflow_run_url" in data:
|
|
264
|
+
metadata["workflow_run_url"] = data["workflow_run_url"]
|
|
265
|
+
|
|
266
|
+
return Event(
|
|
267
|
+
timestamp=timestamp,
|
|
268
|
+
event_type="workflow-started",
|
|
269
|
+
metadata=metadata,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _extract_implementation_status_event(data: dict) -> Event | None:
|
|
274
|
+
"""Extract implementation status event from erk-implementation-status block."""
|
|
275
|
+
timestamp = data.get("timestamp")
|
|
276
|
+
if not timestamp:
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
status = data.get("status")
|
|
280
|
+
if not status:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
metadata: ImplementationStatusMetadata = {"status": status}
|
|
284
|
+
|
|
285
|
+
if "completed_steps" in data:
|
|
286
|
+
metadata["completed_steps"] = data["completed_steps"]
|
|
287
|
+
if "total_steps" in data:
|
|
288
|
+
metadata["total_steps"] = data["total_steps"]
|
|
289
|
+
if "step_description" in data:
|
|
290
|
+
metadata["step_description"] = data["step_description"]
|
|
291
|
+
if "worktree" in data:
|
|
292
|
+
metadata["worktree"] = data["worktree"]
|
|
293
|
+
if "branch" in data:
|
|
294
|
+
metadata["branch"] = data["branch"]
|
|
295
|
+
|
|
296
|
+
return Event(
|
|
297
|
+
timestamp=timestamp,
|
|
298
|
+
event_type="implementation-status",
|
|
299
|
+
metadata=metadata,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _extract_plan_retry_event(data: dict) -> Event | None:
|
|
304
|
+
"""Extract plan retry event from plan-retry block."""
|
|
305
|
+
timestamp = data.get("retry_timestamp")
|
|
306
|
+
if not timestamp:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
metadata: PlanRetryMetadata = {}
|
|
310
|
+
if "retry_count" in data:
|
|
311
|
+
metadata["retry_count"] = data["retry_count"]
|
|
312
|
+
if "triggered_by" in data:
|
|
313
|
+
metadata["triggered_by"] = data["triggered_by"]
|
|
314
|
+
|
|
315
|
+
return Event(
|
|
316
|
+
timestamp=timestamp,
|
|
317
|
+
event_type="plan-retry",
|
|
318
|
+
metadata=metadata,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _extract_worktree_creation_event(data: dict) -> Event | None:
|
|
323
|
+
"""Extract worktree creation event from erk-worktree-creation block."""
|
|
324
|
+
timestamp = data.get("timestamp")
|
|
325
|
+
if not timestamp:
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
metadata: WorktreeCreatedMetadata = {}
|
|
329
|
+
if "worktree_name" in data:
|
|
330
|
+
metadata["worktree_name"] = data["worktree_name"]
|
|
331
|
+
if "branch_name" in data:
|
|
332
|
+
metadata["branch_name"] = data["branch_name"]
|
|
333
|
+
|
|
334
|
+
return Event(
|
|
335
|
+
timestamp=timestamp,
|
|
336
|
+
event_type="worktree-created",
|
|
337
|
+
metadata=metadata,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _output_json(events: list[Event]) -> None:
|
|
342
|
+
"""Output events as JSON array."""
|
|
343
|
+
user_output(json.dumps(events, indent=2))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _output_timeline(events: list[Event], issue_number: int) -> None:
|
|
347
|
+
"""Output events as human-readable timeline.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
events: List of Event objects sorted chronologically
|
|
351
|
+
issue_number: GitHub issue number for the plan
|
|
352
|
+
"""
|
|
353
|
+
if not events:
|
|
354
|
+
user_output(f"No events found for plan #{issue_number}")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
user_output(f"Plan #{issue_number} Event Timeline\n")
|
|
358
|
+
|
|
359
|
+
for event in events:
|
|
360
|
+
# Format timestamp as human-readable
|
|
361
|
+
timestamp_str = _format_timestamp(event["timestamp"])
|
|
362
|
+
|
|
363
|
+
# Format event description
|
|
364
|
+
description = _format_event_description(event)
|
|
365
|
+
|
|
366
|
+
# Output timeline entry
|
|
367
|
+
user_output(f"[{timestamp_str}] {description}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _format_timestamp(iso_timestamp: str) -> str:
|
|
371
|
+
"""Format ISO 8601 timestamp as human-readable string.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
iso_timestamp: ISO 8601 timestamp string
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Formatted timestamp like "2024-01-15 12:30:45 UTC"
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
|
|
381
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
382
|
+
except (ValueError, AttributeError):
|
|
383
|
+
# Fallback: return original if parsing fails
|
|
384
|
+
return iso_timestamp
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _format_event_description(event: Event) -> str:
|
|
388
|
+
"""Format event as human-readable description.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
event: Event object with event_type and metadata
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Formatted description string
|
|
395
|
+
"""
|
|
396
|
+
event_type = event["event_type"]
|
|
397
|
+
metadata = event["metadata"]
|
|
398
|
+
|
|
399
|
+
if event_type == "plan-created":
|
|
400
|
+
worktree = metadata.get("worktree_name", "unknown")
|
|
401
|
+
return f"Plan created: worktree '{worktree}' assigned"
|
|
402
|
+
|
|
403
|
+
if event_type == "submission-queued":
|
|
404
|
+
submitted_by = metadata.get("submitted_by", "unknown")
|
|
405
|
+
return f"Queued for execution by {submitted_by}"
|
|
406
|
+
|
|
407
|
+
if event_type == "workflow-started":
|
|
408
|
+
workflow_url = metadata.get("workflow_run_url", "")
|
|
409
|
+
return f"GitHub Actions workflow started: {workflow_url}"
|
|
410
|
+
|
|
411
|
+
if event_type == "implementation-status":
|
|
412
|
+
status = metadata.get("status", "unknown")
|
|
413
|
+
|
|
414
|
+
if status == "starting":
|
|
415
|
+
worktree = metadata.get("worktree", "unknown")
|
|
416
|
+
return f"Implementation starting in worktree '{worktree}'"
|
|
417
|
+
|
|
418
|
+
if status == "in_progress":
|
|
419
|
+
return "Implementation in progress"
|
|
420
|
+
|
|
421
|
+
if status == "complete":
|
|
422
|
+
return "Implementation complete"
|
|
423
|
+
|
|
424
|
+
if status == "failed":
|
|
425
|
+
return "Implementation failed"
|
|
426
|
+
|
|
427
|
+
return f"Status: {status}"
|
|
428
|
+
|
|
429
|
+
if event_type == "plan-retry":
|
|
430
|
+
retry_count = metadata.get("retry_count", "unknown")
|
|
431
|
+
triggered_by = metadata.get("triggered_by", "unknown")
|
|
432
|
+
return f"Retry #{retry_count} triggered by {triggered_by}"
|
|
433
|
+
|
|
434
|
+
if event_type == "worktree-created":
|
|
435
|
+
worktree = metadata.get("worktree_name", "unknown")
|
|
436
|
+
branch = metadata.get("branch_name", "unknown")
|
|
437
|
+
return f"Worktree created: '{worktree}' (branch: {branch})"
|
|
438
|
+
|
|
439
|
+
# Fallback for unknown event types
|
|
440
|
+
return f"Event: {event_type}"
|