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,163 @@
|
|
|
1
|
+
"""Marker file operations for inter-process communication.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec marker create --session-id SESSION_ID <name>
|
|
5
|
+
erk exec marker exists --session-id SESSION_ID <name>
|
|
6
|
+
erk exec marker delete --session-id SESSION_ID <name>
|
|
7
|
+
|
|
8
|
+
Marker files are stored in `.erk/scratch/sessions/<session-id>/` and are used for
|
|
9
|
+
inter-process communication between hooks and commands. Session ID can be provided
|
|
10
|
+
via `--session-id` flag or `$CLAUDE_CODE_SESSION_ID` environment variable.
|
|
11
|
+
|
|
12
|
+
The `--session-id` flag takes precedence over the environment variable.
|
|
13
|
+
|
|
14
|
+
Exit codes:
|
|
15
|
+
create: 0 = created, 1 = error (missing session ID)
|
|
16
|
+
exists: 0 = exists, 1 = does not exist
|
|
17
|
+
delete: 0 = deleted (or didn't exist), 1 = error (missing session ID)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from erk_shared.context.helpers import require_repo_root
|
|
26
|
+
from erk_shared.scratch.scratch import get_scratch_dir
|
|
27
|
+
|
|
28
|
+
MARKER_EXTENSION = ".marker"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_session_id(session_id: str | None) -> str | None:
|
|
32
|
+
"""Resolve session ID from explicit argument or environment variable.
|
|
33
|
+
|
|
34
|
+
Priority:
|
|
35
|
+
1. Explicit session_id argument (if provided)
|
|
36
|
+
2. CLAUDE_CODE_SESSION_ID environment variable
|
|
37
|
+
3. None (if neither available)
|
|
38
|
+
"""
|
|
39
|
+
if session_id is not None:
|
|
40
|
+
return session_id
|
|
41
|
+
return os.environ.get("CLAUDE_CODE_SESSION_ID")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _output_json(success: bool, message: str) -> None:
|
|
45
|
+
"""Output JSON response."""
|
|
46
|
+
click.echo(json.dumps({"success": success, "message": message}))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.group(name="marker")
|
|
50
|
+
def marker() -> None:
|
|
51
|
+
"""Manage marker files for inter-process communication."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@marker.command(name="create")
|
|
55
|
+
@click.argument("name")
|
|
56
|
+
@click.option(
|
|
57
|
+
"--session-id",
|
|
58
|
+
default=None,
|
|
59
|
+
help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--associated-objective",
|
|
63
|
+
type=int,
|
|
64
|
+
default=None,
|
|
65
|
+
help="Associated objective issue number (stored in marker file)",
|
|
66
|
+
)
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def marker_create(
|
|
69
|
+
ctx: click.Context, name: str, session_id: str | None, associated_objective: int | None
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Create a marker file.
|
|
72
|
+
|
|
73
|
+
NAME is the marker name (e.g., 'incremental-plan').
|
|
74
|
+
The '.marker' extension is added automatically.
|
|
75
|
+
|
|
76
|
+
If --associated-objective is provided, the issue number is stored
|
|
77
|
+
in the marker file content. Otherwise, an empty file is created.
|
|
78
|
+
"""
|
|
79
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
80
|
+
if resolved_session_id is None:
|
|
81
|
+
msg = (
|
|
82
|
+
"Missing session ID: provide --session-id or set "
|
|
83
|
+
"CLAUDE_CODE_SESSION_ID environment variable"
|
|
84
|
+
)
|
|
85
|
+
_output_json(False, msg)
|
|
86
|
+
raise SystemExit(1) from None
|
|
87
|
+
|
|
88
|
+
repo_root = require_repo_root(ctx)
|
|
89
|
+
scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
|
|
90
|
+
marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
|
|
91
|
+
if associated_objective is not None:
|
|
92
|
+
marker_file.write_text(str(associated_objective), encoding="utf-8")
|
|
93
|
+
else:
|
|
94
|
+
marker_file.touch()
|
|
95
|
+
_output_json(True, f"Created marker: {name}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@marker.command(name="exists")
|
|
99
|
+
@click.argument("name")
|
|
100
|
+
@click.option(
|
|
101
|
+
"--session-id",
|
|
102
|
+
default=None,
|
|
103
|
+
help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
|
|
104
|
+
)
|
|
105
|
+
@click.pass_context
|
|
106
|
+
def marker_exists(ctx: click.Context, name: str, session_id: str | None) -> None:
|
|
107
|
+
"""Check if a marker file exists.
|
|
108
|
+
|
|
109
|
+
NAME is the marker name (e.g., 'incremental-plan').
|
|
110
|
+
Exit code 0 if exists, 1 if not.
|
|
111
|
+
"""
|
|
112
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
113
|
+
if resolved_session_id is None:
|
|
114
|
+
msg = (
|
|
115
|
+
"Missing session ID: provide --session-id or set "
|
|
116
|
+
"CLAUDE_CODE_SESSION_ID environment variable"
|
|
117
|
+
)
|
|
118
|
+
_output_json(False, msg)
|
|
119
|
+
raise SystemExit(1) from None
|
|
120
|
+
|
|
121
|
+
repo_root = require_repo_root(ctx)
|
|
122
|
+
scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
|
|
123
|
+
marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
|
|
124
|
+
|
|
125
|
+
if marker_file.exists():
|
|
126
|
+
_output_json(True, f"Marker exists: {name}")
|
|
127
|
+
else:
|
|
128
|
+
_output_json(False, f"Marker does not exist: {name}")
|
|
129
|
+
raise SystemExit(1) from None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@marker.command(name="delete")
|
|
133
|
+
@click.argument("name")
|
|
134
|
+
@click.option(
|
|
135
|
+
"--session-id",
|
|
136
|
+
default=None,
|
|
137
|
+
help="Session ID for marker storage (default: $CLAUDE_CODE_SESSION_ID)",
|
|
138
|
+
)
|
|
139
|
+
@click.pass_context
|
|
140
|
+
def marker_delete(ctx: click.Context, name: str, session_id: str | None) -> None:
|
|
141
|
+
"""Delete a marker file.
|
|
142
|
+
|
|
143
|
+
NAME is the marker name (e.g., 'incremental-plan').
|
|
144
|
+
Succeeds even if marker doesn't exist (idempotent).
|
|
145
|
+
"""
|
|
146
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
147
|
+
if resolved_session_id is None:
|
|
148
|
+
msg = (
|
|
149
|
+
"Missing session ID: provide --session-id or set "
|
|
150
|
+
"CLAUDE_CODE_SESSION_ID environment variable"
|
|
151
|
+
)
|
|
152
|
+
_output_json(False, msg)
|
|
153
|
+
raise SystemExit(1) from None
|
|
154
|
+
|
|
155
|
+
repo_root = require_repo_root(ctx)
|
|
156
|
+
scratch_dir = get_scratch_dir(resolved_session_id, repo_root=repo_root)
|
|
157
|
+
marker_file = scratch_dir / f"{name}{MARKER_EXTENSION}"
|
|
158
|
+
|
|
159
|
+
if marker_file.exists():
|
|
160
|
+
marker_file.unlink()
|
|
161
|
+
_output_json(True, f"Deleted marker: {name}")
|
|
162
|
+
else:
|
|
163
|
+
_output_json(True, f"Marker already deleted: {name}")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Save plan as objective GitHub issue.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec objective-save-to-issue [OPTIONS]
|
|
5
|
+
|
|
6
|
+
This command extracts a plan and creates a GitHub issue with:
|
|
7
|
+
- erk-plan + erk-objective labels (like extraction has erk-plan + erk-extraction)
|
|
8
|
+
- No title suffix
|
|
9
|
+
- Plan content directly in body (no metadata block)
|
|
10
|
+
- No commands section
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--session-id ID: Session ID for scoped plan lookup
|
|
14
|
+
--format: json (default) or display
|
|
15
|
+
|
|
16
|
+
Exit Codes:
|
|
17
|
+
0: Success - objective issue created
|
|
18
|
+
1: Error - no plan found, gh failure, etc.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from erk_shared.context.helpers import (
|
|
26
|
+
require_claude_installation,
|
|
27
|
+
require_cwd,
|
|
28
|
+
require_repo_root,
|
|
29
|
+
)
|
|
30
|
+
from erk_shared.context.helpers import (
|
|
31
|
+
require_issues as require_github_issues,
|
|
32
|
+
)
|
|
33
|
+
from erk_shared.github.plan_issues import create_objective_issue
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.command(name="objective-save-to-issue")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--format",
|
|
39
|
+
"output_format",
|
|
40
|
+
type=click.Choice(["json", "display"]),
|
|
41
|
+
default="json",
|
|
42
|
+
help="Output format: json (default) or display (formatted text)",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--session-id",
|
|
46
|
+
default=None,
|
|
47
|
+
help="Session ID for scoped plan lookup",
|
|
48
|
+
)
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def objective_save_to_issue(ctx: click.Context, output_format: str, session_id: str | None) -> None:
|
|
51
|
+
"""Save plan as objective GitHub issue.
|
|
52
|
+
|
|
53
|
+
Creates a GitHub issue with erk-plan + erk-objective labels and plan content in body.
|
|
54
|
+
"""
|
|
55
|
+
# Get dependencies from context
|
|
56
|
+
github = require_github_issues(ctx)
|
|
57
|
+
repo_root = require_repo_root(ctx)
|
|
58
|
+
cwd = require_cwd(ctx)
|
|
59
|
+
claude_installation = require_claude_installation(ctx)
|
|
60
|
+
|
|
61
|
+
# Get plan content
|
|
62
|
+
plan = claude_installation.get_latest_plan(cwd, session_id=session_id)
|
|
63
|
+
|
|
64
|
+
if not plan:
|
|
65
|
+
if output_format == "display":
|
|
66
|
+
click.echo("Error: No plan found in ~/.claude/plans/", err=True)
|
|
67
|
+
click.echo("\nTo fix:", err=True)
|
|
68
|
+
click.echo("1. Create a plan (enter Plan mode if needed)", err=True)
|
|
69
|
+
click.echo("2. Exit Plan mode using ExitPlanMode tool", err=True)
|
|
70
|
+
click.echo("3. Run this command again", err=True)
|
|
71
|
+
else:
|
|
72
|
+
click.echo(json.dumps({"success": False, "error": "No plan found in ~/.claude/plans/"}))
|
|
73
|
+
raise SystemExit(1)
|
|
74
|
+
|
|
75
|
+
# Create objective issue
|
|
76
|
+
result = create_objective_issue(
|
|
77
|
+
github_issues=github,
|
|
78
|
+
repo_root=repo_root,
|
|
79
|
+
plan_content=plan,
|
|
80
|
+
title=None,
|
|
81
|
+
extra_labels=None,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not result.success:
|
|
85
|
+
if output_format == "display":
|
|
86
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
87
|
+
else:
|
|
88
|
+
click.echo(json.dumps({"success": False, "error": result.error}))
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
|
|
91
|
+
# Guard for type narrowing
|
|
92
|
+
if result.issue_number is None:
|
|
93
|
+
raise RuntimeError("Unexpected: issue_number is None after success")
|
|
94
|
+
|
|
95
|
+
if output_format == "display":
|
|
96
|
+
click.echo(f"Objective saved to GitHub issue #{result.issue_number}")
|
|
97
|
+
click.echo(f"Title: {result.title}")
|
|
98
|
+
click.echo(f"URL: {result.issue_url}")
|
|
99
|
+
else:
|
|
100
|
+
click.echo(
|
|
101
|
+
json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"success": True,
|
|
104
|
+
"issue_number": result.issue_number,
|
|
105
|
+
"issue_url": result.issue_url,
|
|
106
|
+
"title": result.title,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Extract plan from ~/.claude/plans/ and create GitHub issue in one operation.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec plan-save-to-issue [OPTIONS]
|
|
5
|
+
|
|
6
|
+
This command combines plan extraction and issue creation:
|
|
7
|
+
1. Extract plan from specified file, session-scoped lookup, or latest from ~/.claude/plans/
|
|
8
|
+
2. Create GitHub issue with plan content
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--plan-file PATH: Use specific plan file (highest priority)
|
|
12
|
+
--session-id ID: Use session-scoped lookup to find plan by slug
|
|
13
|
+
(neither): Fall back to most recent plan by modification time
|
|
14
|
+
|
|
15
|
+
Output:
|
|
16
|
+
--format json (default): {"success": true, "issue_number": N, ...}
|
|
17
|
+
--format display: Formatted text ready for display
|
|
18
|
+
|
|
19
|
+
Exit Codes:
|
|
20
|
+
0: Success - plan extracted and issue created
|
|
21
|
+
1: Error - no plan found, gh failure, etc.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from erk_shared.context.helpers import (
|
|
30
|
+
get_repo_identifier,
|
|
31
|
+
require_claude_installation,
|
|
32
|
+
require_cwd,
|
|
33
|
+
require_local_config,
|
|
34
|
+
require_repo_root,
|
|
35
|
+
)
|
|
36
|
+
from erk_shared.context.helpers import (
|
|
37
|
+
require_issues as require_github_issues,
|
|
38
|
+
)
|
|
39
|
+
from erk_shared.github.plan_issues import create_plan_issue
|
|
40
|
+
from erk_shared.output.next_steps import format_next_steps_plain
|
|
41
|
+
from erk_shared.scratch.plan_snapshots import snapshot_plan_for_session
|
|
42
|
+
from erk_shared.scratch.scratch import get_scratch_dir
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _create_plan_saved_marker(session_id: str, repo_root: Path) -> None:
|
|
46
|
+
"""Create marker file to indicate plan was saved to GitHub.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
session_id: The session ID for the scratch directory.
|
|
50
|
+
repo_root: The repository root path.
|
|
51
|
+
"""
|
|
52
|
+
marker_dir = get_scratch_dir(session_id, repo_root=repo_root)
|
|
53
|
+
marker_file = marker_dir / "exit-plan-mode-hook.plan-saved.marker"
|
|
54
|
+
marker_file.write_text(
|
|
55
|
+
"Created by: exit-plan-mode-hook (via /erk:plan-save)\n"
|
|
56
|
+
"Trigger: Plan was successfully saved to GitHub\n"
|
|
57
|
+
"Effect: Next ExitPlanMode call will be BLOCKED (remain in plan mode, session complete)\n"
|
|
58
|
+
"Lifecycle: Deleted after being read by next hook invocation\n",
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@click.command(name="plan-save-to-issue")
|
|
64
|
+
@click.option(
|
|
65
|
+
"--format",
|
|
66
|
+
"output_format",
|
|
67
|
+
type=click.Choice(["json", "display"]),
|
|
68
|
+
default="json",
|
|
69
|
+
help="Output format: json (default) or display (formatted text)",
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--plan-file",
|
|
73
|
+
type=click.Path(exists=True, path_type=Path),
|
|
74
|
+
default=None,
|
|
75
|
+
help="Path to specific plan file (highest priority)",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--session-id",
|
|
79
|
+
default=None,
|
|
80
|
+
help="Session ID for scoped plan lookup (uses slug from session logs)",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--objective-issue",
|
|
84
|
+
type=int,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Link plan to parent objective issue number",
|
|
87
|
+
)
|
|
88
|
+
@click.pass_context
|
|
89
|
+
def plan_save_to_issue(
|
|
90
|
+
ctx: click.Context,
|
|
91
|
+
output_format: str,
|
|
92
|
+
plan_file: Path | None,
|
|
93
|
+
session_id: str | None,
|
|
94
|
+
objective_issue: int | None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Extract plan from ~/.claude/plans/ and create GitHub issue.
|
|
97
|
+
|
|
98
|
+
Combines plan extraction and issue creation in a single operation.
|
|
99
|
+
"""
|
|
100
|
+
# Get dependencies from context
|
|
101
|
+
github = require_github_issues(ctx)
|
|
102
|
+
repo_root = require_repo_root(ctx)
|
|
103
|
+
cwd = require_cwd(ctx)
|
|
104
|
+
claude_installation = require_claude_installation(ctx)
|
|
105
|
+
|
|
106
|
+
# session_id comes from --session-id CLI option (or None if not provided)
|
|
107
|
+
effective_session_id = session_id
|
|
108
|
+
|
|
109
|
+
# Step 1: Extract plan (priority: plan_file > session_id > most recent)
|
|
110
|
+
if plan_file:
|
|
111
|
+
plan = plan_file.read_text(encoding="utf-8")
|
|
112
|
+
else:
|
|
113
|
+
plan = claude_installation.get_latest_plan(cwd, session_id=effective_session_id)
|
|
114
|
+
|
|
115
|
+
if not plan:
|
|
116
|
+
if output_format == "display":
|
|
117
|
+
click.echo("Error: No plan found in ~/.claude/plans/", err=True)
|
|
118
|
+
click.echo("\nTo fix:", err=True)
|
|
119
|
+
click.echo("1. Create a plan (enter Plan mode if needed)", err=True)
|
|
120
|
+
click.echo("2. Exit Plan mode using ExitPlanMode tool", err=True)
|
|
121
|
+
click.echo("3. Run this command again", err=True)
|
|
122
|
+
else:
|
|
123
|
+
click.echo(json.dumps({"success": False, "error": "No plan found in ~/.claude/plans/"}))
|
|
124
|
+
raise SystemExit(1)
|
|
125
|
+
|
|
126
|
+
# Determine source_repo for cross-repo plans
|
|
127
|
+
# When plans_repo is configured, plans are stored in a separate repo
|
|
128
|
+
# and source_repo records where implementation will happen
|
|
129
|
+
source_repo: str | None = None
|
|
130
|
+
config = require_local_config(ctx)
|
|
131
|
+
if config.plans_repo is not None:
|
|
132
|
+
source_repo = get_repo_identifier(ctx)
|
|
133
|
+
|
|
134
|
+
# Use consolidated create_plan_issue for the entire workflow
|
|
135
|
+
result = create_plan_issue(
|
|
136
|
+
github_issues=github,
|
|
137
|
+
repo_root=repo_root,
|
|
138
|
+
plan_content=plan,
|
|
139
|
+
title=None,
|
|
140
|
+
plan_type=None,
|
|
141
|
+
extra_labels=None,
|
|
142
|
+
title_suffix=None,
|
|
143
|
+
source_plan_issues=None,
|
|
144
|
+
extraction_session_ids=None,
|
|
145
|
+
source_repo=source_repo,
|
|
146
|
+
objective_issue=objective_issue,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not result.success:
|
|
150
|
+
if result.issue_number is not None:
|
|
151
|
+
# Partial success - issue created but comment failed
|
|
152
|
+
if output_format == "display":
|
|
153
|
+
click.echo(f"Warning: {result.error}", err=True)
|
|
154
|
+
click.echo(f"Please manually add plan content to: {result.issue_url}", err=True)
|
|
155
|
+
else:
|
|
156
|
+
click.echo(
|
|
157
|
+
json.dumps(
|
|
158
|
+
{
|
|
159
|
+
"success": False,
|
|
160
|
+
"error": result.error,
|
|
161
|
+
"issue_number": result.issue_number,
|
|
162
|
+
"issue_url": result.issue_url,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
if output_format == "display":
|
|
168
|
+
click.echo(f"Error: {result.error}", err=True)
|
|
169
|
+
else:
|
|
170
|
+
click.echo(json.dumps({"success": False, "error": result.error}))
|
|
171
|
+
raise SystemExit(1)
|
|
172
|
+
|
|
173
|
+
# DISABLED: Session context embedding is temporarily disabled while rethinking extraction plans
|
|
174
|
+
# To re-enable, uncomment the following block and restore imports:
|
|
175
|
+
# from erk_shared.context.helpers import require_git
|
|
176
|
+
# from erk_shared.extraction.session_context import collect_session_context
|
|
177
|
+
# from erk_shared.github.metadata import render_session_content_blocks
|
|
178
|
+
#
|
|
179
|
+
# git = require_git(ctx)
|
|
180
|
+
# session_result = collect_session_context(
|
|
181
|
+
# git=git,
|
|
182
|
+
# cwd=cwd,
|
|
183
|
+
# session_store=session_store,
|
|
184
|
+
# current_session_id=effective_session_id,
|
|
185
|
+
# min_size=1024,
|
|
186
|
+
# limit=20,
|
|
187
|
+
# )
|
|
188
|
+
#
|
|
189
|
+
# if session_result is not None and result.issue_number is not None:
|
|
190
|
+
# # Render and post as comments
|
|
191
|
+
# session_label = session_result.branch_context.current_branch or "planning-session"
|
|
192
|
+
# content_blocks = render_session_content_blocks(
|
|
193
|
+
# content=session_result.combined_xml,
|
|
194
|
+
# session_label=session_label,
|
|
195
|
+
# extraction_hints=["Planning session context for downstream analysis"],
|
|
196
|
+
# )
|
|
197
|
+
#
|
|
198
|
+
# # Post each block as a comment (failures are non-blocking)
|
|
199
|
+
# for block in content_blocks:
|
|
200
|
+
# try:
|
|
201
|
+
# github.add_comment(repo_root, result.issue_number, block)
|
|
202
|
+
# session_context_chunks += 1
|
|
203
|
+
# except RuntimeError:
|
|
204
|
+
# # Session context is supplementary - don't fail the command
|
|
205
|
+
# pass
|
|
206
|
+
#
|
|
207
|
+
# session_ids = session_result.session_ids
|
|
208
|
+
|
|
209
|
+
# Output JSON still includes these for backwards compatibility
|
|
210
|
+
session_context_chunks = 0
|
|
211
|
+
session_ids: list[str] = []
|
|
212
|
+
|
|
213
|
+
# Step 9: Create marker file to indicate plan was saved
|
|
214
|
+
snapshot_result = None
|
|
215
|
+
if effective_session_id:
|
|
216
|
+
_create_plan_saved_marker(effective_session_id, repo_root)
|
|
217
|
+
|
|
218
|
+
# Step 9.1: Snapshot the plan file to session-scoped storage
|
|
219
|
+
# Determine plan file path
|
|
220
|
+
if plan_file:
|
|
221
|
+
snapshot_path = plan_file
|
|
222
|
+
else:
|
|
223
|
+
# Look up slug from session to find plan file
|
|
224
|
+
snapshot_path = claude_installation.find_plan_for_session(cwd, effective_session_id)
|
|
225
|
+
|
|
226
|
+
if snapshot_path is not None and snapshot_path.exists():
|
|
227
|
+
snapshot_result = snapshot_plan_for_session(
|
|
228
|
+
session_id=effective_session_id,
|
|
229
|
+
plan_file_path=snapshot_path,
|
|
230
|
+
project_cwd=cwd,
|
|
231
|
+
claude_installation=claude_installation,
|
|
232
|
+
repo_root=repo_root,
|
|
233
|
+
)
|
|
234
|
+
# NOTE: Plan file deletion moved to impl_signal.py on 'started' event
|
|
235
|
+
# This allows the user to modify and re-save the plan before implementing
|
|
236
|
+
|
|
237
|
+
# Step 10: Output success
|
|
238
|
+
# Detect enrichment status for informational output
|
|
239
|
+
is_enriched = "## Enrichment Details" in plan
|
|
240
|
+
|
|
241
|
+
# At this point result.success is True, so issue_number must be set
|
|
242
|
+
# Guard for type narrowing
|
|
243
|
+
if result.issue_number is None:
|
|
244
|
+
raise RuntimeError("Unexpected: issue_number is None after successful create_plan_issue")
|
|
245
|
+
|
|
246
|
+
if output_format == "display":
|
|
247
|
+
click.echo(f"Plan saved to GitHub issue #{result.issue_number}")
|
|
248
|
+
click.echo(f"Title: {result.title}")
|
|
249
|
+
click.echo(f"URL: {result.issue_url}")
|
|
250
|
+
click.echo(f"Enrichment: {'Yes' if is_enriched else 'No'}")
|
|
251
|
+
if session_context_chunks > 0:
|
|
252
|
+
click.echo(f"Session context: {session_context_chunks} chunks")
|
|
253
|
+
if snapshot_result is not None:
|
|
254
|
+
click.echo(f"Archived: {snapshot_result.snapshot_dir}")
|
|
255
|
+
click.echo()
|
|
256
|
+
click.echo(format_next_steps_plain(result.issue_number))
|
|
257
|
+
else:
|
|
258
|
+
output_data = {
|
|
259
|
+
"success": True,
|
|
260
|
+
"issue_number": result.issue_number,
|
|
261
|
+
"issue_url": result.issue_url,
|
|
262
|
+
"title": result.title,
|
|
263
|
+
"enriched": is_enriched,
|
|
264
|
+
"session_context_chunks": session_context_chunks,
|
|
265
|
+
"session_ids": session_ids,
|
|
266
|
+
}
|
|
267
|
+
if snapshot_result is not None:
|
|
268
|
+
output_data["archived_to"] = str(snapshot_result.snapshot_dir)
|
|
269
|
+
click.echo(json.dumps(output_data))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Update an existing GitHub issue's plan comment with new content.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec plan-update-issue --issue-number N [OPTIONS]
|
|
5
|
+
|
|
6
|
+
This command updates the plan content comment on an existing GitHub issue:
|
|
7
|
+
1. Find plan file (from session scratch, --plan-path, or ~/.claude/plans/)
|
|
8
|
+
2. Get the first comment ID from the issue (where plan body lives)
|
|
9
|
+
3. Update that comment with new plan content
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--issue-number N: GitHub issue number to update (required)
|
|
13
|
+
--session-id ID: Session ID to find plan file in scratch storage
|
|
14
|
+
--plan-path PATH: Direct path to plan file (overrides session lookup)
|
|
15
|
+
|
|
16
|
+
Output:
|
|
17
|
+
--format json (default): {"success": true, ...}
|
|
18
|
+
--format display: Formatted text
|
|
19
|
+
|
|
20
|
+
Exit Codes:
|
|
21
|
+
0: Success - plan comment updated
|
|
22
|
+
1: Error - issue not found, no plan found, no comments, etc.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from erk_shared.context.helpers import (
|
|
31
|
+
require_claude_installation,
|
|
32
|
+
require_cwd,
|
|
33
|
+
require_repo_root,
|
|
34
|
+
)
|
|
35
|
+
from erk_shared.context.helpers import (
|
|
36
|
+
require_issues as require_github_issues,
|
|
37
|
+
)
|
|
38
|
+
from erk_shared.github.metadata.plan_header import format_plan_content_comment
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.command(name="plan-update-issue")
|
|
42
|
+
@click.option(
|
|
43
|
+
"--issue-number",
|
|
44
|
+
type=int,
|
|
45
|
+
required=True,
|
|
46
|
+
help="GitHub issue number to update",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--format",
|
|
50
|
+
"output_format",
|
|
51
|
+
type=click.Choice(["json", "display"]),
|
|
52
|
+
default="json",
|
|
53
|
+
help="Output format: json (default) or display (formatted text)",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--plan-path",
|
|
57
|
+
type=click.Path(exists=True, path_type=Path),
|
|
58
|
+
help="Direct path to plan file (overrides session lookup)",
|
|
59
|
+
)
|
|
60
|
+
@click.option(
|
|
61
|
+
"--session-id",
|
|
62
|
+
help="Session ID to find plan file in scratch storage",
|
|
63
|
+
)
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def plan_update_issue(
|
|
66
|
+
ctx: click.Context,
|
|
67
|
+
issue_number: int,
|
|
68
|
+
output_format: str,
|
|
69
|
+
plan_path: Path | None,
|
|
70
|
+
session_id: str | None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Update an existing GitHub issue's plan comment with new content."""
|
|
73
|
+
# Get dependencies from context
|
|
74
|
+
github = require_github_issues(ctx)
|
|
75
|
+
repo_root = require_repo_root(ctx)
|
|
76
|
+
cwd = require_cwd(ctx)
|
|
77
|
+
claude_installation = require_claude_installation(ctx)
|
|
78
|
+
|
|
79
|
+
# Step 1: Find plan content (priority: plan_path > session > latest)
|
|
80
|
+
if plan_path is not None:
|
|
81
|
+
plan_content = plan_path.read_text(encoding="utf-8")
|
|
82
|
+
else:
|
|
83
|
+
plan_content = claude_installation.get_latest_plan(cwd, session_id=session_id)
|
|
84
|
+
|
|
85
|
+
if not plan_content:
|
|
86
|
+
error_msg = "No plan found in ~/.claude/plans/"
|
|
87
|
+
if output_format == "display":
|
|
88
|
+
click.echo(f"Error: {error_msg}", err=True)
|
|
89
|
+
else:
|
|
90
|
+
click.echo(json.dumps({"success": False, "error": error_msg}))
|
|
91
|
+
raise SystemExit(1)
|
|
92
|
+
|
|
93
|
+
# Step 2: Get existing issue to verify it exists
|
|
94
|
+
try:
|
|
95
|
+
issue = github.get_issue(repo_root, issue_number)
|
|
96
|
+
except RuntimeError as e:
|
|
97
|
+
error_msg = f"Failed to get issue #{issue_number}: {e}"
|
|
98
|
+
if output_format == "display":
|
|
99
|
+
click.echo(f"Error: {error_msg}", err=True)
|
|
100
|
+
else:
|
|
101
|
+
click.echo(json.dumps({"success": False, "error": error_msg}))
|
|
102
|
+
raise SystemExit(1) from e
|
|
103
|
+
|
|
104
|
+
# Step 3: Get first comment ID (where plan body lives in Schema v2)
|
|
105
|
+
comments = github.get_issue_comments_with_urls(repo_root, issue_number)
|
|
106
|
+
if not comments:
|
|
107
|
+
error_msg = f"Issue #{issue_number} has no comments - cannot update plan content"
|
|
108
|
+
if output_format == "display":
|
|
109
|
+
click.echo(f"Error: {error_msg}", err=True)
|
|
110
|
+
else:
|
|
111
|
+
click.echo(json.dumps({"success": False, "error": error_msg}))
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
first_comment = comments[0]
|
|
115
|
+
comment_id = first_comment.id
|
|
116
|
+
|
|
117
|
+
# Step 4: Format plan content and update comment
|
|
118
|
+
formatted_plan = format_plan_content_comment(plan_content.strip())
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
github.update_comment(repo_root, comment_id, formatted_plan)
|
|
122
|
+
except RuntimeError as e:
|
|
123
|
+
error_msg = f"Failed to update comment: {e}"
|
|
124
|
+
if output_format == "display":
|
|
125
|
+
click.echo(f"Error: {error_msg}", err=True)
|
|
126
|
+
else:
|
|
127
|
+
click.echo(json.dumps({"success": False, "error": error_msg}))
|
|
128
|
+
raise SystemExit(1) from e
|
|
129
|
+
|
|
130
|
+
# Step 5: Output success
|
|
131
|
+
if output_format == "display":
|
|
132
|
+
click.echo(f"Plan updated on issue #{issue_number}")
|
|
133
|
+
click.echo(f"Title: {issue.title}")
|
|
134
|
+
click.echo(f"URL: {issue.url}")
|
|
135
|
+
click.echo(f"Comment: {first_comment.url}")
|
|
136
|
+
else:
|
|
137
|
+
click.echo(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"success": True,
|
|
141
|
+
"issue_number": issue_number,
|
|
142
|
+
"issue_url": issue.url,
|
|
143
|
+
"comment_id": comment_id,
|
|
144
|
+
"comment_url": first_comment.url,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
)
|