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,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List Claude Code sessions for the current project with metadata.
|
|
3
|
+
|
|
4
|
+
This command discovers sessions in the Claude Code project directory,
|
|
5
|
+
extracts metadata (timestamps, summaries), and provides branch context
|
|
6
|
+
for intelligent session selection.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
erk exec list-sessions
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
JSON object with success status, branch context, and session list
|
|
13
|
+
|
|
14
|
+
Exit Codes:
|
|
15
|
+
0: Success
|
|
16
|
+
1: Error (project directory not found or other error)
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
$ erk exec list-sessions
|
|
20
|
+
{
|
|
21
|
+
"success": true,
|
|
22
|
+
"branch_context": {
|
|
23
|
+
"current_branch": "feature-xyz",
|
|
24
|
+
"trunk_branch": "master",
|
|
25
|
+
"is_on_trunk": false
|
|
26
|
+
},
|
|
27
|
+
"current_session_id": "abc123-def456",
|
|
28
|
+
"sessions": [...],
|
|
29
|
+
"project_dir": "claude-code-project"
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import time
|
|
35
|
+
from dataclasses import asdict, dataclass
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
import click
|
|
39
|
+
|
|
40
|
+
from erk_shared.context.helpers import require_claude_installation, require_cwd, require_git
|
|
41
|
+
from erk_shared.extraction.claude_installation import ClaudeInstallation, Session
|
|
42
|
+
from erk_shared.extraction.session_schema import extract_first_user_message_text
|
|
43
|
+
from erk_shared.git.abc import Git
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class BranchContext:
|
|
48
|
+
"""Git branch context for session selection behavior."""
|
|
49
|
+
|
|
50
|
+
current_branch: str
|
|
51
|
+
trunk_branch: str
|
|
52
|
+
is_on_trunk: bool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class SessionInfo:
|
|
57
|
+
"""Metadata for a session log file."""
|
|
58
|
+
|
|
59
|
+
session_id: str
|
|
60
|
+
mtime_display: str
|
|
61
|
+
mtime_relative: str
|
|
62
|
+
mtime_unix: float
|
|
63
|
+
size_bytes: int
|
|
64
|
+
summary: str
|
|
65
|
+
is_current: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ListSessionsResult:
|
|
70
|
+
"""Success result with session list and context."""
|
|
71
|
+
|
|
72
|
+
success: bool
|
|
73
|
+
branch_context: dict[str, str | bool]
|
|
74
|
+
current_session_id: str | None
|
|
75
|
+
sessions: list[dict[str, str | float | int | bool]]
|
|
76
|
+
project_dir: str
|
|
77
|
+
filtered_count: int # Count of sessions filtered by --min-size
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ListSessionsError:
|
|
82
|
+
"""Error result when listing sessions fails."""
|
|
83
|
+
|
|
84
|
+
success: bool
|
|
85
|
+
error: str
|
|
86
|
+
help: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_branch_context(git: Git, cwd: Path) -> BranchContext:
|
|
90
|
+
"""Get git branch context for determining session selection behavior.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
git: Git interface for branch operations
|
|
94
|
+
cwd: Current working directory
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
BranchContext with current branch, trunk branch, and trunk status
|
|
98
|
+
"""
|
|
99
|
+
current_branch = git.get_current_branch(cwd) or ""
|
|
100
|
+
trunk_branch = git.detect_trunk_branch(cwd)
|
|
101
|
+
|
|
102
|
+
return BranchContext(
|
|
103
|
+
current_branch=current_branch,
|
|
104
|
+
trunk_branch=trunk_branch,
|
|
105
|
+
is_on_trunk=current_branch == trunk_branch,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def format_relative_time(mtime: float) -> str:
|
|
110
|
+
"""Format modification time as human-readable relative time.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
mtime: Unix timestamp (seconds since epoch)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Human-readable relative time string
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
>>> format_relative_time(time.time() - 10)
|
|
120
|
+
'just now'
|
|
121
|
+
>>> format_relative_time(time.time() - 180)
|
|
122
|
+
'3m ago'
|
|
123
|
+
>>> format_relative_time(time.time() - 7200)
|
|
124
|
+
'2h ago'
|
|
125
|
+
"""
|
|
126
|
+
now = time.time()
|
|
127
|
+
delta = now - mtime
|
|
128
|
+
|
|
129
|
+
if delta < 30:
|
|
130
|
+
return "just now"
|
|
131
|
+
if delta < 3600: # < 1 hour
|
|
132
|
+
minutes = int(delta / 60)
|
|
133
|
+
return f"{minutes}m ago"
|
|
134
|
+
if delta < 86400: # < 24 hours
|
|
135
|
+
hours = int(delta / 3600)
|
|
136
|
+
return f"{hours}h ago"
|
|
137
|
+
if delta < 604800: # < 7 days
|
|
138
|
+
days = int(delta / 86400)
|
|
139
|
+
return f"{days}d ago"
|
|
140
|
+
# >= 7 days: show absolute date
|
|
141
|
+
return format_display_time(mtime)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def format_display_time(mtime: float) -> str:
|
|
145
|
+
"""Format modification time as display string.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
mtime: Unix timestamp (seconds since epoch)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Formatted date string like "Dec 3, 11:38 AM"
|
|
152
|
+
"""
|
|
153
|
+
import datetime
|
|
154
|
+
|
|
155
|
+
dt = datetime.datetime.fromtimestamp(mtime)
|
|
156
|
+
return dt.strftime("%b %-d, %-I:%M %p")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _list_sessions_from_store(
|
|
160
|
+
claude_installation: ClaudeInstallation,
|
|
161
|
+
cwd: Path,
|
|
162
|
+
current_session_id: str | None,
|
|
163
|
+
limit: int,
|
|
164
|
+
min_size: int,
|
|
165
|
+
) -> tuple[list[SessionInfo], int]:
|
|
166
|
+
"""List sessions from claude installation sorted by modification time.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
claude_installation: Claude installation to query
|
|
170
|
+
cwd: Current working directory (project identifier)
|
|
171
|
+
current_session_id: Current session ID (for marking)
|
|
172
|
+
limit: Maximum number of sessions to return
|
|
173
|
+
min_size: Minimum session size in bytes (filters out tiny sessions)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Tuple of (sessions list, count of sessions filtered by min_size)
|
|
177
|
+
"""
|
|
178
|
+
# Check if project exists
|
|
179
|
+
if not claude_installation.has_project(cwd):
|
|
180
|
+
return [], 0
|
|
181
|
+
|
|
182
|
+
# Get all sessions first to count filtered
|
|
183
|
+
all_sessions = claude_installation.find_sessions(
|
|
184
|
+
cwd,
|
|
185
|
+
current_session_id=current_session_id,
|
|
186
|
+
min_size=0,
|
|
187
|
+
limit=1000,
|
|
188
|
+
include_agents=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Filter by size
|
|
192
|
+
filtered_sessions: list[Session]
|
|
193
|
+
if min_size > 0:
|
|
194
|
+
filtered_sessions = [s for s in all_sessions if s.size_bytes >= min_size]
|
|
195
|
+
filtered_count = len(all_sessions) - len(filtered_sessions)
|
|
196
|
+
else:
|
|
197
|
+
filtered_sessions = all_sessions
|
|
198
|
+
filtered_count = 0
|
|
199
|
+
|
|
200
|
+
# Apply limit
|
|
201
|
+
limited_sessions = filtered_sessions[:limit]
|
|
202
|
+
|
|
203
|
+
# Convert to SessionInfo with summaries
|
|
204
|
+
session_infos: list[SessionInfo] = []
|
|
205
|
+
for session in limited_sessions:
|
|
206
|
+
# Read session content for summary extraction
|
|
207
|
+
content = claude_installation.read_session(cwd, session.session_id, include_agents=False)
|
|
208
|
+
summary = ""
|
|
209
|
+
if content is not None:
|
|
210
|
+
summary = extract_first_user_message_text(content.main_content, max_length=60)
|
|
211
|
+
|
|
212
|
+
# Determine if this is the current session
|
|
213
|
+
is_current = session.session_id == current_session_id
|
|
214
|
+
|
|
215
|
+
session_infos.append(
|
|
216
|
+
SessionInfo(
|
|
217
|
+
session_id=session.session_id,
|
|
218
|
+
mtime_display=format_display_time(session.modified_at),
|
|
219
|
+
mtime_relative=format_relative_time(session.modified_at),
|
|
220
|
+
mtime_unix=session.modified_at,
|
|
221
|
+
size_bytes=session.size_bytes,
|
|
222
|
+
summary=summary,
|
|
223
|
+
is_current=is_current,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return session_infos, filtered_count
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@click.command(name="list-sessions")
|
|
231
|
+
@click.option(
|
|
232
|
+
"--limit",
|
|
233
|
+
default=10,
|
|
234
|
+
type=int,
|
|
235
|
+
help="Maximum number of sessions to list",
|
|
236
|
+
)
|
|
237
|
+
@click.option(
|
|
238
|
+
"--min-size",
|
|
239
|
+
default=0,
|
|
240
|
+
type=int,
|
|
241
|
+
help="Minimum session size in bytes (filters out tiny sessions)",
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--session-id",
|
|
245
|
+
default=None,
|
|
246
|
+
type=str,
|
|
247
|
+
help="Current session ID (for marking the current session)",
|
|
248
|
+
)
|
|
249
|
+
@click.pass_context
|
|
250
|
+
def list_sessions(ctx: click.Context, limit: int, min_size: int, session_id: str | None) -> None:
|
|
251
|
+
"""List Claude Code sessions with metadata for the current project.
|
|
252
|
+
|
|
253
|
+
Discovers sessions in the project directory, extracts metadata
|
|
254
|
+
(timestamps, summaries), and provides branch context.
|
|
255
|
+
"""
|
|
256
|
+
git = require_git(ctx)
|
|
257
|
+
claude_installation = require_claude_installation(ctx)
|
|
258
|
+
cwd = require_cwd(ctx)
|
|
259
|
+
|
|
260
|
+
# Check if project exists
|
|
261
|
+
if not claude_installation.has_project(cwd):
|
|
262
|
+
error = ListSessionsError(
|
|
263
|
+
success=False,
|
|
264
|
+
error=f"No Claude Code project found for: {cwd}",
|
|
265
|
+
help="Make sure you're in a directory with Claude Code sessions",
|
|
266
|
+
)
|
|
267
|
+
click.echo(json.dumps(asdict(error), indent=2))
|
|
268
|
+
raise SystemExit(1)
|
|
269
|
+
|
|
270
|
+
# Get branch context
|
|
271
|
+
branch_context = get_branch_context(git, cwd)
|
|
272
|
+
|
|
273
|
+
# List sessions from store
|
|
274
|
+
sessions, filtered_count = _list_sessions_from_store(
|
|
275
|
+
claude_installation,
|
|
276
|
+
cwd,
|
|
277
|
+
session_id,
|
|
278
|
+
limit=limit,
|
|
279
|
+
min_size=min_size,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Build result
|
|
283
|
+
result = ListSessionsResult(
|
|
284
|
+
success=True,
|
|
285
|
+
branch_context={
|
|
286
|
+
"current_branch": branch_context.current_branch,
|
|
287
|
+
"trunk_branch": branch_context.trunk_branch,
|
|
288
|
+
"is_on_trunk": branch_context.is_on_trunk,
|
|
289
|
+
},
|
|
290
|
+
current_session_id=session_id,
|
|
291
|
+
sessions=[asdict(s) for s in sessions],
|
|
292
|
+
project_dir="claude-code-project", # Abstract - don't expose filesystem paths
|
|
293
|
+
filtered_count=filtered_count,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Mark implementation ended by updating GitHub issue metadata.
|
|
2
|
+
|
|
3
|
+
This exec command updates the plan-header metadata block in a GitHub issue
|
|
4
|
+
with the appropriate event fields based on the execution environment:
|
|
5
|
+
- Local machine: Updates last_local_impl_* fields (timestamp, event="ended", session, user)
|
|
6
|
+
- GitHub Actions: Updates last_remote_impl_at field
|
|
7
|
+
|
|
8
|
+
Also writes .impl/local-run-state.json for fast local access (no GitHub API needed).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
erk exec mark-impl-ended
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
JSON with success status or error information
|
|
15
|
+
Always exits with code 0 (graceful degradation for || true pattern)
|
|
16
|
+
|
|
17
|
+
Exit Codes:
|
|
18
|
+
0: Always (even on error, to support || true pattern)
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
$ erk exec mark-impl-ended
|
|
22
|
+
{"success": true, "issue_number": 123}
|
|
23
|
+
|
|
24
|
+
$ erk exec mark-impl-ended
|
|
25
|
+
{"success": false, "error_type": "no_issue_reference", "message": "..."}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import getpass
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from dataclasses import asdict, dataclass
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
from erk_shared.context.helpers import (
|
|
37
|
+
require_cwd,
|
|
38
|
+
require_repo_root,
|
|
39
|
+
)
|
|
40
|
+
from erk_shared.context.helpers import (
|
|
41
|
+
require_issues as require_github_issues,
|
|
42
|
+
)
|
|
43
|
+
from erk_shared.env import in_github_actions
|
|
44
|
+
from erk_shared.github.metadata.plan_header import (
|
|
45
|
+
update_plan_header_local_impl_event,
|
|
46
|
+
update_plan_header_remote_impl,
|
|
47
|
+
)
|
|
48
|
+
from erk_shared.impl_folder import read_issue_reference, write_local_run_state
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class MarkImplSuccess:
|
|
53
|
+
"""Success response for mark impl ended."""
|
|
54
|
+
|
|
55
|
+
success: bool
|
|
56
|
+
issue_number: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class MarkImplError:
|
|
61
|
+
"""Error response for mark impl ended."""
|
|
62
|
+
|
|
63
|
+
success: bool
|
|
64
|
+
error_type: str
|
|
65
|
+
message: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command(name="mark-impl-ended")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def mark_impl_ended(ctx: click.Context) -> None:
|
|
71
|
+
"""Update implementation ended event in GitHub issue and local state file.
|
|
72
|
+
|
|
73
|
+
Reads issue number from .impl/issue.json, fetches the issue from GitHub,
|
|
74
|
+
updates the plan-header block with current event metadata, and posts back.
|
|
75
|
+
|
|
76
|
+
Also writes .impl/local-run-state.json for fast local access.
|
|
77
|
+
|
|
78
|
+
Detects execution environment:
|
|
79
|
+
- Local machine: Updates last_local_impl_* fields (timestamp, event="ended", session, user)
|
|
80
|
+
- GitHub Actions: Updates last_remote_impl_at field
|
|
81
|
+
|
|
82
|
+
Gracefully fails with exit code 0 to support || true pattern in slash commands.
|
|
83
|
+
"""
|
|
84
|
+
# Get dependencies from context
|
|
85
|
+
repo_root = require_repo_root(ctx)
|
|
86
|
+
cwd = require_cwd(ctx)
|
|
87
|
+
|
|
88
|
+
# Read issue reference from .impl/issue.json
|
|
89
|
+
impl_dir = cwd / ".impl"
|
|
90
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
91
|
+
if issue_ref is None:
|
|
92
|
+
result = MarkImplError(
|
|
93
|
+
success=False,
|
|
94
|
+
error_type="no-issue-reference",
|
|
95
|
+
message="No issue reference found in .impl/issue.json",
|
|
96
|
+
)
|
|
97
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
98
|
+
raise SystemExit(0)
|
|
99
|
+
|
|
100
|
+
# Capture metadata
|
|
101
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
102
|
+
session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
|
|
103
|
+
user = getpass.getuser()
|
|
104
|
+
|
|
105
|
+
# Write local state file first (fast, no network)
|
|
106
|
+
try:
|
|
107
|
+
write_local_run_state(
|
|
108
|
+
impl_dir=impl_dir,
|
|
109
|
+
last_event="ended",
|
|
110
|
+
timestamp=timestamp,
|
|
111
|
+
user=user,
|
|
112
|
+
session_id=session_id,
|
|
113
|
+
)
|
|
114
|
+
except (FileNotFoundError, ValueError) as e:
|
|
115
|
+
result = MarkImplError(
|
|
116
|
+
success=False,
|
|
117
|
+
error_type="local-state-write-failed",
|
|
118
|
+
message=f"Failed to write local state: {e}",
|
|
119
|
+
)
|
|
120
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
121
|
+
raise SystemExit(0) from None
|
|
122
|
+
|
|
123
|
+
# Get GitHub Issues from context
|
|
124
|
+
try:
|
|
125
|
+
github_issues = require_github_issues(ctx)
|
|
126
|
+
except SystemExit:
|
|
127
|
+
result = MarkImplError(
|
|
128
|
+
success=False,
|
|
129
|
+
error_type="context-not-initialized",
|
|
130
|
+
message="Context not initialized",
|
|
131
|
+
)
|
|
132
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
133
|
+
raise SystemExit(0) from None
|
|
134
|
+
|
|
135
|
+
# Fetch current issue
|
|
136
|
+
try:
|
|
137
|
+
issue = github_issues.get_issue(repo_root, issue_ref.issue_number)
|
|
138
|
+
except RuntimeError as e:
|
|
139
|
+
result = MarkImplError(
|
|
140
|
+
success=False,
|
|
141
|
+
error_type="issue-not-found",
|
|
142
|
+
message=f"Issue #{issue_ref.issue_number} not found: {e}",
|
|
143
|
+
)
|
|
144
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
145
|
+
raise SystemExit(0) from None
|
|
146
|
+
|
|
147
|
+
# Update impl event based on environment
|
|
148
|
+
try:
|
|
149
|
+
if in_github_actions():
|
|
150
|
+
updated_body = update_plan_header_remote_impl(
|
|
151
|
+
issue_body=issue.body,
|
|
152
|
+
remote_impl_at=timestamp,
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
updated_body = update_plan_header_local_impl_event(
|
|
156
|
+
issue_body=issue.body,
|
|
157
|
+
local_impl_at=timestamp,
|
|
158
|
+
event="ended",
|
|
159
|
+
session_id=session_id,
|
|
160
|
+
user=user,
|
|
161
|
+
)
|
|
162
|
+
except ValueError as e:
|
|
163
|
+
# plan-header block not found (old format issue)
|
|
164
|
+
result = MarkImplError(
|
|
165
|
+
success=False,
|
|
166
|
+
error_type="no-plan-header-block",
|
|
167
|
+
message=str(e),
|
|
168
|
+
)
|
|
169
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
170
|
+
raise SystemExit(0) from None
|
|
171
|
+
|
|
172
|
+
# Update issue body
|
|
173
|
+
try:
|
|
174
|
+
github_issues.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
|
|
175
|
+
except RuntimeError as e:
|
|
176
|
+
result = MarkImplError(
|
|
177
|
+
success=False,
|
|
178
|
+
error_type="github-api-failed",
|
|
179
|
+
message=f"Failed to update issue body: {e}",
|
|
180
|
+
)
|
|
181
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
182
|
+
raise SystemExit(0) from None
|
|
183
|
+
|
|
184
|
+
result_success = MarkImplSuccess(
|
|
185
|
+
success=True,
|
|
186
|
+
issue_number=issue_ref.issue_number,
|
|
187
|
+
)
|
|
188
|
+
click.echo(json.dumps(asdict(result_success), indent=2))
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Mark implementation started by updating GitHub issue metadata.
|
|
2
|
+
|
|
3
|
+
This exec command updates the plan-header metadata block in a GitHub issue
|
|
4
|
+
with the appropriate event fields based on the execution environment:
|
|
5
|
+
- Local machine: Updates last_local_impl_* fields (timestamp, event, session, user)
|
|
6
|
+
- GitHub Actions: Updates last_remote_impl_at field
|
|
7
|
+
|
|
8
|
+
Also writes .impl/local-run-state.json for fast local access (no GitHub API needed).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
erk exec mark-impl-started
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
JSON with success status or error information
|
|
15
|
+
Always exits with code 0 (graceful degradation for || true pattern)
|
|
16
|
+
|
|
17
|
+
Exit Codes:
|
|
18
|
+
0: Always (even on error, to support || true pattern)
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
$ erk exec mark-impl-started
|
|
22
|
+
{"success": true, "issue_number": 123}
|
|
23
|
+
|
|
24
|
+
$ erk exec mark-impl-started
|
|
25
|
+
{"success": false, "error_type": "no_issue_reference", "message": "..."}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import getpass
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from dataclasses import asdict, dataclass
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
from erk_shared.context.helpers import (
|
|
37
|
+
require_cwd,
|
|
38
|
+
require_repo_root,
|
|
39
|
+
)
|
|
40
|
+
from erk_shared.context.helpers import (
|
|
41
|
+
require_issues as require_github_issues,
|
|
42
|
+
)
|
|
43
|
+
from erk_shared.env import in_github_actions
|
|
44
|
+
from erk_shared.github.metadata.plan_header import (
|
|
45
|
+
update_plan_header_local_impl_event,
|
|
46
|
+
update_plan_header_remote_impl,
|
|
47
|
+
)
|
|
48
|
+
from erk_shared.impl_folder import read_issue_reference, write_local_run_state
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class MarkImplSuccess:
|
|
53
|
+
"""Success response for mark impl started."""
|
|
54
|
+
|
|
55
|
+
success: bool
|
|
56
|
+
issue_number: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class MarkImplError:
|
|
61
|
+
"""Error response for mark impl started."""
|
|
62
|
+
|
|
63
|
+
success: bool
|
|
64
|
+
error_type: str
|
|
65
|
+
message: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command(name="mark-impl-started")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def mark_impl_started(ctx: click.Context) -> None:
|
|
71
|
+
"""Update implementation started event in GitHub issue and local state file.
|
|
72
|
+
|
|
73
|
+
Reads issue number from .impl/issue.json, fetches the issue from GitHub,
|
|
74
|
+
updates the plan-header block with current event metadata, and posts back.
|
|
75
|
+
|
|
76
|
+
Also writes .impl/local-run-state.json for fast local access.
|
|
77
|
+
|
|
78
|
+
Detects execution environment:
|
|
79
|
+
- Local machine: Updates last_local_impl_* fields (timestamp, event, session, user)
|
|
80
|
+
- GitHub Actions: Updates last_remote_impl_at field
|
|
81
|
+
|
|
82
|
+
Gracefully fails with exit code 0 to support || true pattern in slash commands.
|
|
83
|
+
"""
|
|
84
|
+
# Get dependencies from context
|
|
85
|
+
repo_root = require_repo_root(ctx)
|
|
86
|
+
cwd = require_cwd(ctx)
|
|
87
|
+
|
|
88
|
+
# Read issue reference from .impl/issue.json
|
|
89
|
+
impl_dir = cwd / ".impl"
|
|
90
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
91
|
+
if issue_ref is None:
|
|
92
|
+
result = MarkImplError(
|
|
93
|
+
success=False,
|
|
94
|
+
error_type="no-issue-reference",
|
|
95
|
+
message="No issue reference found in .impl/issue.json",
|
|
96
|
+
)
|
|
97
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
98
|
+
raise SystemExit(0)
|
|
99
|
+
|
|
100
|
+
# Capture metadata
|
|
101
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
102
|
+
session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
|
|
103
|
+
user = getpass.getuser()
|
|
104
|
+
|
|
105
|
+
# Write local state file first (fast, no network)
|
|
106
|
+
try:
|
|
107
|
+
write_local_run_state(
|
|
108
|
+
impl_dir=impl_dir,
|
|
109
|
+
last_event="started",
|
|
110
|
+
timestamp=timestamp,
|
|
111
|
+
user=user,
|
|
112
|
+
session_id=session_id,
|
|
113
|
+
)
|
|
114
|
+
except (FileNotFoundError, ValueError) as e:
|
|
115
|
+
result = MarkImplError(
|
|
116
|
+
success=False,
|
|
117
|
+
error_type="local-state-write-failed",
|
|
118
|
+
message=f"Failed to write local state: {e}",
|
|
119
|
+
)
|
|
120
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
121
|
+
raise SystemExit(0) from None
|
|
122
|
+
|
|
123
|
+
# Get GitHub Issues from context
|
|
124
|
+
try:
|
|
125
|
+
github_issues = require_github_issues(ctx)
|
|
126
|
+
except SystemExit:
|
|
127
|
+
result = MarkImplError(
|
|
128
|
+
success=False,
|
|
129
|
+
error_type="context-not-initialized",
|
|
130
|
+
message="Context not initialized",
|
|
131
|
+
)
|
|
132
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
133
|
+
raise SystemExit(0) from None
|
|
134
|
+
|
|
135
|
+
# Fetch current issue
|
|
136
|
+
try:
|
|
137
|
+
issue = github_issues.get_issue(repo_root, issue_ref.issue_number)
|
|
138
|
+
except RuntimeError as e:
|
|
139
|
+
result = MarkImplError(
|
|
140
|
+
success=False,
|
|
141
|
+
error_type="issue-not-found",
|
|
142
|
+
message=f"Issue #{issue_ref.issue_number} not found: {e}",
|
|
143
|
+
)
|
|
144
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
145
|
+
raise SystemExit(0) from None
|
|
146
|
+
|
|
147
|
+
# Update impl event based on environment
|
|
148
|
+
try:
|
|
149
|
+
if in_github_actions():
|
|
150
|
+
updated_body = update_plan_header_remote_impl(
|
|
151
|
+
issue_body=issue.body,
|
|
152
|
+
remote_impl_at=timestamp,
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
updated_body = update_plan_header_local_impl_event(
|
|
156
|
+
issue_body=issue.body,
|
|
157
|
+
local_impl_at=timestamp,
|
|
158
|
+
event="started",
|
|
159
|
+
session_id=session_id,
|
|
160
|
+
user=user,
|
|
161
|
+
)
|
|
162
|
+
except ValueError as e:
|
|
163
|
+
# plan-header block not found (old format issue)
|
|
164
|
+
result = MarkImplError(
|
|
165
|
+
success=False,
|
|
166
|
+
error_type="no-plan-header-block",
|
|
167
|
+
message=str(e),
|
|
168
|
+
)
|
|
169
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
170
|
+
raise SystemExit(0) from None
|
|
171
|
+
|
|
172
|
+
# Update issue body
|
|
173
|
+
try:
|
|
174
|
+
github_issues.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
|
|
175
|
+
except RuntimeError as e:
|
|
176
|
+
result = MarkImplError(
|
|
177
|
+
success=False,
|
|
178
|
+
error_type="github-api-failed",
|
|
179
|
+
message=f"Failed to update issue body: {e}",
|
|
180
|
+
)
|
|
181
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
182
|
+
raise SystemExit(0) from None
|
|
183
|
+
|
|
184
|
+
result_success = MarkImplSuccess(
|
|
185
|
+
success=True,
|
|
186
|
+
issue_number=issue_ref.issue_number,
|
|
187
|
+
)
|
|
188
|
+
click.echo(json.dumps(asdict(result_success), indent=2))
|