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,375 @@
|
|
|
1
|
+
"""Signal implementation events (started/ended) to GitHub.
|
|
2
|
+
|
|
3
|
+
This exec command wraps the start/end signaling operations:
|
|
4
|
+
- "started": Combines post-start-comment and mark-impl-started
|
|
5
|
+
- "ended": Runs mark-impl-ended
|
|
6
|
+
|
|
7
|
+
Provides a single entry point for /erk:plan-implement to signal events
|
|
8
|
+
with graceful failure (always exits 0 for || true pattern).
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
erk exec impl-signal started
|
|
12
|
+
erk exec impl-signal ended
|
|
13
|
+
|
|
14
|
+
Output:
|
|
15
|
+
JSON with success status or error information
|
|
16
|
+
Always exits with code 0 (graceful degradation for || true pattern)
|
|
17
|
+
|
|
18
|
+
Exit Codes:
|
|
19
|
+
0: Always (even on error, to support || true pattern)
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
$ erk exec impl-signal started
|
|
23
|
+
{"success": true, "event": "started", "issue_number": 123}
|
|
24
|
+
|
|
25
|
+
$ erk exec impl-signal ended
|
|
26
|
+
{"success": true, "event": "ended", "issue_number": 123}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import getpass
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import subprocess
|
|
33
|
+
from dataclasses import asdict, dataclass
|
|
34
|
+
from datetime import UTC, datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
import click
|
|
38
|
+
|
|
39
|
+
from erk_shared.context.helpers import (
|
|
40
|
+
require_claude_installation,
|
|
41
|
+
require_repo_root,
|
|
42
|
+
)
|
|
43
|
+
from erk_shared.context.helpers import (
|
|
44
|
+
require_issues as require_github_issues,
|
|
45
|
+
)
|
|
46
|
+
from erk_shared.env import in_github_actions
|
|
47
|
+
from erk_shared.github.metadata.core import render_erk_issue_event
|
|
48
|
+
from erk_shared.github.metadata.plan_header import (
|
|
49
|
+
update_plan_header_local_impl_event,
|
|
50
|
+
update_plan_header_remote_impl,
|
|
51
|
+
)
|
|
52
|
+
from erk_shared.impl_folder import (
|
|
53
|
+
read_issue_reference,
|
|
54
|
+
write_local_run_state,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class SignalSuccess:
|
|
60
|
+
"""Success response for signal command."""
|
|
61
|
+
|
|
62
|
+
success: bool
|
|
63
|
+
event: str
|
|
64
|
+
issue_number: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class SignalError:
|
|
69
|
+
"""Error response for signal command."""
|
|
70
|
+
|
|
71
|
+
success: bool
|
|
72
|
+
event: str
|
|
73
|
+
error_type: str
|
|
74
|
+
message: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _output_error(event: str, error_type: str, message: str) -> None:
|
|
78
|
+
"""Output error JSON and exit gracefully."""
|
|
79
|
+
result = SignalError(
|
|
80
|
+
success=False,
|
|
81
|
+
event=event,
|
|
82
|
+
error_type=error_type,
|
|
83
|
+
message=message,
|
|
84
|
+
)
|
|
85
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
86
|
+
raise SystemExit(0)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _delete_claude_plan_file(ctx: click.Context, session_id: str, cwd: Path) -> bool:
|
|
90
|
+
"""Delete the Claude plan file for the given session.
|
|
91
|
+
|
|
92
|
+
This is called when implementation starts to clean up the plan file.
|
|
93
|
+
The plan content has already been saved to GitHub and snapshotted.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
ctx: Click context for dependency injection.
|
|
97
|
+
session_id: The session ID to look up the plan slug.
|
|
98
|
+
cwd: Current working directory for hint.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if file was deleted, False if not found or error.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
installation = require_claude_installation(ctx)
|
|
105
|
+
except SystemExit:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
slugs = installation.extract_slugs_from_session(cwd, session_id)
|
|
109
|
+
if not slugs:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
plan_file = installation.get_plans_dir_path() / f"{slugs[-1]}.md"
|
|
113
|
+
if plan_file.exists():
|
|
114
|
+
plan_file.unlink()
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_worktree_name() -> str | None:
|
|
120
|
+
"""Get current worktree name from git worktree list."""
|
|
121
|
+
try:
|
|
122
|
+
result = subprocess.run(
|
|
123
|
+
["git", "worktree", "list", "--porcelain"],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
check=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
current_dir = Path.cwd().resolve()
|
|
130
|
+
lines = result.stdout.strip().split("\n")
|
|
131
|
+
|
|
132
|
+
for line in lines:
|
|
133
|
+
if line.startswith("worktree "):
|
|
134
|
+
worktree_path = Path(line[len("worktree ") :])
|
|
135
|
+
if current_dir == worktree_path or current_dir.is_relative_to(worktree_path):
|
|
136
|
+
return worktree_path.name
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
except subprocess.CalledProcessError:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _get_branch_name() -> str | None:
|
|
144
|
+
"""Get current git branch name."""
|
|
145
|
+
try:
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
["git", "branch", "--show-current"],
|
|
148
|
+
capture_output=True,
|
|
149
|
+
text=True,
|
|
150
|
+
check=True,
|
|
151
|
+
)
|
|
152
|
+
branch = result.stdout.strip()
|
|
153
|
+
if branch:
|
|
154
|
+
return branch
|
|
155
|
+
return None
|
|
156
|
+
except subprocess.CalledProcessError:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _signal_started(ctx: click.Context, session_id: str | None) -> None:
|
|
161
|
+
"""Handle 'started' event - post comment and update metadata."""
|
|
162
|
+
event = "started"
|
|
163
|
+
|
|
164
|
+
# Find impl directory (.impl/ or .worker-impl/) - check BEFORE context access
|
|
165
|
+
impl_dir = Path.cwd() / ".impl"
|
|
166
|
+
if not impl_dir.exists():
|
|
167
|
+
impl_dir = Path.cwd() / ".worker-impl"
|
|
168
|
+
|
|
169
|
+
# Read issue reference FIRST (doesn't require context)
|
|
170
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
171
|
+
if issue_ref is None:
|
|
172
|
+
_output_error(event, "no-issue-reference", "No issue reference found in issue.json")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Delete Claude plan file if session_id provided
|
|
176
|
+
# The plan has been saved to GitHub and snapshotted, so it's safe to delete
|
|
177
|
+
if session_id is not None:
|
|
178
|
+
_delete_claude_plan_file(ctx, session_id, Path.cwd())
|
|
179
|
+
|
|
180
|
+
# Now get context dependencies (after confirming we need them)
|
|
181
|
+
try:
|
|
182
|
+
repo_root = require_repo_root(ctx)
|
|
183
|
+
except SystemExit:
|
|
184
|
+
_output_error(event, "context-not-initialized", "Context not initialized")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# Get worktree and branch names
|
|
188
|
+
worktree_name = _get_worktree_name()
|
|
189
|
+
if worktree_name is None:
|
|
190
|
+
_output_error(event, "worktree-detection-failed", "Could not determine worktree name")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
branch_name = _get_branch_name()
|
|
194
|
+
if branch_name is None:
|
|
195
|
+
_output_error(event, "branch-detection-failed", "Could not determine branch name")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Capture metadata
|
|
199
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
200
|
+
session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
|
|
201
|
+
user = getpass.getuser()
|
|
202
|
+
|
|
203
|
+
# Write local state file first (fast, no network)
|
|
204
|
+
try:
|
|
205
|
+
write_local_run_state(
|
|
206
|
+
impl_dir=impl_dir,
|
|
207
|
+
last_event="started",
|
|
208
|
+
timestamp=timestamp,
|
|
209
|
+
user=user,
|
|
210
|
+
session_id=session_id,
|
|
211
|
+
)
|
|
212
|
+
except (FileNotFoundError, ValueError) as e:
|
|
213
|
+
_output_error(event, "local-state-write-failed", f"Failed to write local state: {e}")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Get GitHub Issues from context
|
|
217
|
+
try:
|
|
218
|
+
github = require_github_issues(ctx)
|
|
219
|
+
except SystemExit:
|
|
220
|
+
_output_error(event, "context-not-initialized", "Context not initialized")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Post start comment
|
|
224
|
+
try:
|
|
225
|
+
description = f"""**Worktree:** `{worktree_name}`
|
|
226
|
+
**Branch:** `{branch_name}`"""
|
|
227
|
+
|
|
228
|
+
comment_body = render_erk_issue_event(
|
|
229
|
+
title="🚀 Starting implementation",
|
|
230
|
+
metadata=None,
|
|
231
|
+
description=description,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
github.add_comment(repo_root, issue_ref.issue_number, comment_body)
|
|
235
|
+
except RuntimeError as e:
|
|
236
|
+
_output_error(event, "github-comment-failed", f"Failed to post comment: {e}")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Update issue metadata
|
|
240
|
+
try:
|
|
241
|
+
issue = github.get_issue(repo_root, issue_ref.issue_number)
|
|
242
|
+
|
|
243
|
+
if in_github_actions():
|
|
244
|
+
updated_body = update_plan_header_remote_impl(
|
|
245
|
+
issue_body=issue.body,
|
|
246
|
+
remote_impl_at=timestamp,
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
updated_body = update_plan_header_local_impl_event(
|
|
250
|
+
issue_body=issue.body,
|
|
251
|
+
local_impl_at=timestamp,
|
|
252
|
+
event="started",
|
|
253
|
+
session_id=session_id,
|
|
254
|
+
user=user,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
github.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
|
|
258
|
+
except (RuntimeError, ValueError):
|
|
259
|
+
# Non-fatal - comment was posted, metadata update failed
|
|
260
|
+
# Continue successfully
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
result = SignalSuccess(
|
|
264
|
+
success=True,
|
|
265
|
+
event=event,
|
|
266
|
+
issue_number=issue_ref.issue_number,
|
|
267
|
+
)
|
|
268
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
269
|
+
raise SystemExit(0)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _signal_ended(ctx: click.Context) -> None:
|
|
273
|
+
"""Handle 'ended' event - update metadata."""
|
|
274
|
+
event = "ended"
|
|
275
|
+
|
|
276
|
+
# Find impl directory - check BEFORE context access
|
|
277
|
+
impl_dir = Path.cwd() / ".impl"
|
|
278
|
+
if not impl_dir.exists():
|
|
279
|
+
impl_dir = Path.cwd() / ".worker-impl"
|
|
280
|
+
|
|
281
|
+
# Read issue reference FIRST (doesn't require context)
|
|
282
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
283
|
+
if issue_ref is None:
|
|
284
|
+
_output_error(event, "no-issue-reference", "No issue reference found in issue.json")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# Now get context dependencies (after confirming we need them)
|
|
288
|
+
try:
|
|
289
|
+
repo_root = require_repo_root(ctx)
|
|
290
|
+
except SystemExit:
|
|
291
|
+
_output_error(event, "context-not-initialized", "Context not initialized")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Capture metadata
|
|
295
|
+
timestamp = datetime.now(UTC).isoformat()
|
|
296
|
+
session_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
|
|
297
|
+
user = getpass.getuser()
|
|
298
|
+
|
|
299
|
+
# Write local state file first
|
|
300
|
+
try:
|
|
301
|
+
write_local_run_state(
|
|
302
|
+
impl_dir=impl_dir,
|
|
303
|
+
last_event="ended",
|
|
304
|
+
timestamp=timestamp,
|
|
305
|
+
user=user,
|
|
306
|
+
session_id=session_id,
|
|
307
|
+
)
|
|
308
|
+
except (FileNotFoundError, ValueError) as e:
|
|
309
|
+
_output_error(event, "local-state-write-failed", f"Failed to write local state: {e}")
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Get GitHub Issues from context
|
|
313
|
+
try:
|
|
314
|
+
github = require_github_issues(ctx)
|
|
315
|
+
except SystemExit:
|
|
316
|
+
_output_error(event, "context-not-initialized", "Context not initialized")
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
# Update issue metadata
|
|
320
|
+
try:
|
|
321
|
+
issue = github.get_issue(repo_root, issue_ref.issue_number)
|
|
322
|
+
|
|
323
|
+
if in_github_actions():
|
|
324
|
+
updated_body = update_plan_header_remote_impl(
|
|
325
|
+
issue_body=issue.body,
|
|
326
|
+
remote_impl_at=timestamp,
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
updated_body = update_plan_header_local_impl_event(
|
|
330
|
+
issue_body=issue.body,
|
|
331
|
+
local_impl_at=timestamp,
|
|
332
|
+
event="ended",
|
|
333
|
+
session_id=session_id,
|
|
334
|
+
user=user,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
github.update_issue_body(repo_root, issue_ref.issue_number, updated_body)
|
|
338
|
+
except (RuntimeError, ValueError) as e:
|
|
339
|
+
_output_error(event, "github-api-failed", f"Failed to update issue: {e}")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
result = SignalSuccess(
|
|
343
|
+
success=True,
|
|
344
|
+
event=event,
|
|
345
|
+
issue_number=issue_ref.issue_number,
|
|
346
|
+
)
|
|
347
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
348
|
+
raise SystemExit(0)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@click.command(name="impl-signal")
|
|
352
|
+
@click.argument("event", type=click.Choice(["started", "ended"]))
|
|
353
|
+
@click.option(
|
|
354
|
+
"--session-id",
|
|
355
|
+
default=None,
|
|
356
|
+
help="Session ID for plan file deletion on 'started' event",
|
|
357
|
+
)
|
|
358
|
+
@click.pass_context
|
|
359
|
+
def impl_signal(ctx: click.Context, event: str, session_id: str | None) -> None:
|
|
360
|
+
"""Signal implementation events to GitHub.
|
|
361
|
+
|
|
362
|
+
EVENT can be 'started' or 'ended'.
|
|
363
|
+
|
|
364
|
+
'started' posts a start comment and updates issue metadata.
|
|
365
|
+
'ended' updates issue metadata with ended event.
|
|
366
|
+
|
|
367
|
+
When --session-id is provided on 'started', also deletes the Claude plan file
|
|
368
|
+
(the content has been saved to GitHub and snapshotted).
|
|
369
|
+
|
|
370
|
+
Always exits with code 0 for graceful degradation (|| true pattern).
|
|
371
|
+
"""
|
|
372
|
+
if event == "started":
|
|
373
|
+
_signal_started(ctx, session_id)
|
|
374
|
+
else:
|
|
375
|
+
_signal_ended(ctx)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Verify .impl/ folder still exists after implementation.
|
|
2
|
+
|
|
3
|
+
This exec command is a guardrail for /erk:plan-implement to ensure the agent
|
|
4
|
+
did not delete .impl/ during implementation. The .impl/ folder MUST be preserved
|
|
5
|
+
for user review.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
erk exec impl-verify
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
JSON with validation status
|
|
12
|
+
|
|
13
|
+
Exit Codes:
|
|
14
|
+
0: .impl/ folder exists
|
|
15
|
+
1: .impl/ folder was deleted (violation of instructions)
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
$ erk exec impl-verify
|
|
19
|
+
{"valid": true, "impl_dir": "/path/to/.impl"}
|
|
20
|
+
|
|
21
|
+
$ erk exec impl-verify # when .impl/ is missing
|
|
22
|
+
{"valid": false, "error": ".impl/ folder was deleted during implementation...", ...}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from erk_shared.context.helpers import require_cwd
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.command(name="impl-verify")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
def impl_verify(ctx: click.Context) -> None:
|
|
35
|
+
"""Verify .impl/ folder still exists after implementation."""
|
|
36
|
+
cwd = require_cwd(ctx)
|
|
37
|
+
impl_dir = cwd / ".impl"
|
|
38
|
+
|
|
39
|
+
if not impl_dir.exists():
|
|
40
|
+
# Hard error - agent violated instructions
|
|
41
|
+
result = {
|
|
42
|
+
"valid": False,
|
|
43
|
+
"error": ".impl/ folder was deleted during implementation. This violates instructions.",
|
|
44
|
+
"action": "The .impl/ folder must be preserved for user review.",
|
|
45
|
+
}
|
|
46
|
+
click.echo(json.dumps(result))
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
|
|
49
|
+
click.echo(json.dumps({"valid": True, "impl_dir": str(impl_dir)}))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Convert plan title to filename.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec issue-title-to-filename "Plan Title"
|
|
5
|
+
|
|
6
|
+
Single source of truth for filename transformation for /erk:plan-save.
|
|
7
|
+
|
|
8
|
+
Output:
|
|
9
|
+
Filename on stdout (e.g., "my-feature-plan.md")
|
|
10
|
+
Error message on stderr with exit code 1 on failure
|
|
11
|
+
|
|
12
|
+
Exit Codes:
|
|
13
|
+
0: Success
|
|
14
|
+
1: Error (empty title)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from erk_shared.naming import generate_filename_from_title
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command(name="issue-title-to-filename")
|
|
23
|
+
@click.argument("title")
|
|
24
|
+
def issue_title_to_filename(title: str) -> None:
|
|
25
|
+
"""Convert plan title to filename.
|
|
26
|
+
|
|
27
|
+
TITLE: Plan title to convert
|
|
28
|
+
"""
|
|
29
|
+
if not title or not title.strip():
|
|
30
|
+
click.echo(click.style("Error: ", fg="red") + "Plan title cannot be empty", err=True)
|
|
31
|
+
raise SystemExit(1)
|
|
32
|
+
|
|
33
|
+
filename = generate_filename_from_title(title)
|
|
34
|
+
click.echo(filename)
|