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,49 @@
|
|
|
1
|
+
"""Extract the latest plan from Claude session files.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec extract-latest-plan [--session-id SESSION_ID]
|
|
5
|
+
|
|
6
|
+
This command searches Claude session files for the most recent ExitPlanMode
|
|
7
|
+
tool use and extracts the plan text. It can search either the current session
|
|
8
|
+
(if --session-id is provided) or all sessions for the project.
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
Plan text on stdout
|
|
12
|
+
Error message on stderr with exit code 1 on failure
|
|
13
|
+
|
|
14
|
+
Exit Codes:
|
|
15
|
+
0: Success - plan found and output
|
|
16
|
+
1: Error - no plan found or other error
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from erk_shared.context.helpers import require_claude_installation, require_cwd
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.command(name="extract-latest-plan")
|
|
25
|
+
@click.option(
|
|
26
|
+
"--session-id",
|
|
27
|
+
help="Session ID to search within (optional, searches all sessions if not provided)",
|
|
28
|
+
)
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def extract_latest_plan(ctx: click.Context, session_id: str | None) -> None:
|
|
31
|
+
"""Extract the latest plan from Claude session files.
|
|
32
|
+
|
|
33
|
+
Searches for the most recent ExitPlanMode tool use and extracts the plan text.
|
|
34
|
+
"""
|
|
35
|
+
# Get dependencies from context
|
|
36
|
+
cwd = require_cwd(ctx)
|
|
37
|
+
claude_installation = require_claude_installation(ctx)
|
|
38
|
+
|
|
39
|
+
# Extract latest plan
|
|
40
|
+
plan_text = claude_installation.get_latest_plan(cwd, session_id=session_id)
|
|
41
|
+
|
|
42
|
+
if not plan_text:
|
|
43
|
+
click.echo(
|
|
44
|
+
click.style("Error: ", fg="red") + "No plan found in Claude session files", err=True
|
|
45
|
+
)
|
|
46
|
+
raise SystemExit(1)
|
|
47
|
+
|
|
48
|
+
# Output plan text to stdout
|
|
49
|
+
click.echo(plan_text)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Extract session XML content from a GitHub issue's comments.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec extract-session-from-issue <issue-number> [--output <path>]
|
|
5
|
+
erk exec extract-session-from-issue <issue-number> --stdout
|
|
6
|
+
|
|
7
|
+
This command:
|
|
8
|
+
1. Fetches all comments from the specified GitHub issue
|
|
9
|
+
2. Parses session-content metadata blocks from the comments
|
|
10
|
+
3. Handles chunked content by combining in order
|
|
11
|
+
4. Writes the combined XML to the output path (or stdout with --stdout)
|
|
12
|
+
5. Returns metadata about the extracted session
|
|
13
|
+
|
|
14
|
+
Output:
|
|
15
|
+
Default: JSON with success status, session_file path, session_ids, and chunk_count
|
|
16
|
+
With --stdout: Session XML to stdout, metadata JSON to stderr
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from erk_shared.context.helpers import require_issues as require_github_issues
|
|
25
|
+
from erk_shared.context.helpers import require_repo_root
|
|
26
|
+
from erk_shared.github.metadata.session import extract_session_content_from_comments
|
|
27
|
+
from erk_shared.scratch.scratch import write_scratch_file
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.command(name="extract-session-from-issue")
|
|
31
|
+
@click.argument("issue_number", type=int)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--output",
|
|
34
|
+
type=click.Path(path_type=Path),
|
|
35
|
+
default=None,
|
|
36
|
+
help="Output path for the session XML (default: auto-generated in .erk/scratch/)",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--session-id",
|
|
40
|
+
type=str,
|
|
41
|
+
default=None,
|
|
42
|
+
help="Session ID for scratch directory (used if --output not provided)",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--stdout",
|
|
46
|
+
"use_stdout",
|
|
47
|
+
is_flag=True,
|
|
48
|
+
default=False,
|
|
49
|
+
help="Output session XML to stdout instead of writing to a file",
|
|
50
|
+
)
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def extract_session_from_issue(
|
|
53
|
+
ctx: click.Context,
|
|
54
|
+
issue_number: int,
|
|
55
|
+
output: Path | None,
|
|
56
|
+
session_id: str | None,
|
|
57
|
+
use_stdout: bool,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Extract session XML from GitHub issue comments.
|
|
60
|
+
|
|
61
|
+
Reads all comments from the specified issue, finds session-content
|
|
62
|
+
metadata blocks, combines chunked content in order, and writes
|
|
63
|
+
the result to the output path.
|
|
64
|
+
|
|
65
|
+
ISSUE_NUMBER is the GitHub issue number to extract session data from.
|
|
66
|
+
"""
|
|
67
|
+
# Get required context
|
|
68
|
+
github = require_github_issues(ctx)
|
|
69
|
+
repo_root = require_repo_root(ctx)
|
|
70
|
+
|
|
71
|
+
# Fetch issue comments
|
|
72
|
+
comments = github.get_issue_comments(repo_root, issue_number)
|
|
73
|
+
|
|
74
|
+
# Extract session content
|
|
75
|
+
session_xml, session_ids = extract_session_content_from_comments(comments)
|
|
76
|
+
|
|
77
|
+
if session_xml is None:
|
|
78
|
+
click.echo(
|
|
79
|
+
json.dumps(
|
|
80
|
+
{
|
|
81
|
+
"success": False,
|
|
82
|
+
"error": f"No session content found in issue #{issue_number}",
|
|
83
|
+
"issue_number": issue_number,
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
raise SystemExit(1)
|
|
88
|
+
|
|
89
|
+
# Handle --stdout: output XML to stdout, metadata to stderr
|
|
90
|
+
if use_stdout:
|
|
91
|
+
click.echo(session_xml)
|
|
92
|
+
click.echo(
|
|
93
|
+
json.dumps(
|
|
94
|
+
{
|
|
95
|
+
"success": True,
|
|
96
|
+
"issue_number": issue_number,
|
|
97
|
+
"session_ids": session_ids,
|
|
98
|
+
"chunk_count": len(session_ids) if session_ids else 1,
|
|
99
|
+
}
|
|
100
|
+
),
|
|
101
|
+
err=True,
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Determine output path
|
|
106
|
+
if output is not None:
|
|
107
|
+
# Use explicit output path
|
|
108
|
+
output_path = output
|
|
109
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
output.write_text(session_xml, encoding="utf-8")
|
|
111
|
+
elif session_id is not None:
|
|
112
|
+
# Use scratch directory with provided session ID
|
|
113
|
+
output_path = write_scratch_file(
|
|
114
|
+
session_xml,
|
|
115
|
+
session_id=session_id,
|
|
116
|
+
suffix=".xml",
|
|
117
|
+
prefix="session-from-issue-",
|
|
118
|
+
repo_root=repo_root,
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
# Generate session ID from first extracted session ID, or use issue number
|
|
122
|
+
fallback_session_id = session_ids[0] if session_ids else f"issue-{issue_number}"
|
|
123
|
+
output_path = write_scratch_file(
|
|
124
|
+
session_xml,
|
|
125
|
+
session_id=fallback_session_id,
|
|
126
|
+
suffix=".xml",
|
|
127
|
+
prefix="session-from-issue-",
|
|
128
|
+
repo_root=repo_root,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Calculate chunk count from the original comments
|
|
132
|
+
chunk_count = len([s for s in session_ids]) # Approximate based on unique session IDs
|
|
133
|
+
if chunk_count == 0:
|
|
134
|
+
chunk_count = 1 # At least one chunk if we got content
|
|
135
|
+
|
|
136
|
+
click.echo(
|
|
137
|
+
json.dumps(
|
|
138
|
+
{
|
|
139
|
+
"success": True,
|
|
140
|
+
"issue_number": issue_number,
|
|
141
|
+
"session_file": str(output_path),
|
|
142
|
+
"session_ids": session_ids,
|
|
143
|
+
"chunk_count": chunk_count,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
extract_session_from_issue()
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Find Claude Code project directory for a given filesystem path.
|
|
3
|
+
|
|
4
|
+
This command provides deterministic mapping between filesystem paths and
|
|
5
|
+
Claude Code project directories in ~/.claude/projects/.
|
|
6
|
+
|
|
7
|
+
Claude Code encodes filesystem paths using a simple rule:
|
|
8
|
+
- Replace "/" with "-"
|
|
9
|
+
- Replace "." with "-"
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
/Users/foo/bar → -Users-foo-bar
|
|
13
|
+
/Users/foo/.config/bar → -Users-foo--config-bar (double dash for dot)
|
|
14
|
+
|
|
15
|
+
This command returns the project directory path and metadata about session logs.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
# Find project directory for current directory
|
|
19
|
+
erk exec find-project-dir
|
|
20
|
+
|
|
21
|
+
# Find project directory for specific path
|
|
22
|
+
erk exec find-project-dir --path /some/path
|
|
23
|
+
|
|
24
|
+
# JSON output for scripting
|
|
25
|
+
erk exec find-project-dir --json
|
|
26
|
+
|
|
27
|
+
Output:
|
|
28
|
+
JSON object with success status and project information
|
|
29
|
+
|
|
30
|
+
Exit Codes:
|
|
31
|
+
0: Success (project directory found)
|
|
32
|
+
1: Error (project directory not found or other error)
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
$ erk exec find-project-dir
|
|
36
|
+
{
|
|
37
|
+
"success": true,
|
|
38
|
+
"project_dir": "/Users/foo/.claude/projects/-Users-foo-code-erk",
|
|
39
|
+
"cwd": "/Users/foo/code/erk",
|
|
40
|
+
"encoded_path": "-Users-foo-code-erk",
|
|
41
|
+
"session_logs": ["abc123.jsonl", "agent-17cfd3f4.jsonl"],
|
|
42
|
+
"latest_session_id": "abc123"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$ erk exec find-project-dir --path /nonexistent
|
|
46
|
+
{
|
|
47
|
+
"success": false,
|
|
48
|
+
"error": "Project directory not found",
|
|
49
|
+
"help": "No Claude Code project found for /nonexistent",
|
|
50
|
+
"context": {
|
|
51
|
+
"path": "/nonexistent",
|
|
52
|
+
"encoded_path": "-nonexistent"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
from dataclasses import asdict, dataclass
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
import click
|
|
63
|
+
|
|
64
|
+
from erk_shared.context.helpers import require_claude_installation
|
|
65
|
+
from erk_shared.extraction.claude_installation import ClaudeInstallation
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ProjectInfo:
|
|
70
|
+
"""Success result with project information."""
|
|
71
|
+
|
|
72
|
+
success: bool
|
|
73
|
+
project_dir: str
|
|
74
|
+
cwd: str
|
|
75
|
+
encoded_path: str
|
|
76
|
+
session_logs: list[str]
|
|
77
|
+
latest_session_id: str | None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ProjectError:
|
|
82
|
+
"""Error result when project directory not found."""
|
|
83
|
+
|
|
84
|
+
success: bool
|
|
85
|
+
error: str
|
|
86
|
+
help: str
|
|
87
|
+
context: dict[str, str]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def encode_path_to_project_folder(path: Path) -> str:
|
|
91
|
+
"""Encode filesystem path to Claude Code project folder name.
|
|
92
|
+
|
|
93
|
+
Claude Code uses a simple encoding scheme:
|
|
94
|
+
- Replace "/" with "-"
|
|
95
|
+
- Replace "." with "-"
|
|
96
|
+
|
|
97
|
+
This creates deterministic folder names in ~/.claude/projects/.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
path: Filesystem path to encode
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Encoded path suitable for project folder name
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
>>> encode_path_to_project_folder(Path("/Users/foo/bar"))
|
|
107
|
+
'-Users-foo-bar'
|
|
108
|
+
>>> encode_path_to_project_folder(Path("/Users/foo/.config"))
|
|
109
|
+
'-Users-foo--config'
|
|
110
|
+
"""
|
|
111
|
+
return str(path).replace("/", "-").replace(".", "-")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_project_info(path: Path, installation: ClaudeInstallation) -> ProjectInfo | ProjectError:
|
|
115
|
+
"""Find Claude Code project directory and metadata for given path.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: Filesystem path to find project for
|
|
119
|
+
installation: ClaudeInstallation gateway for accessing projects directory
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
ProjectInfo on success, ProjectError if not found
|
|
123
|
+
"""
|
|
124
|
+
projects_dir = installation.get_projects_dir_path()
|
|
125
|
+
if not installation.projects_dir_exists():
|
|
126
|
+
return ProjectError(
|
|
127
|
+
success=False,
|
|
128
|
+
error="Claude Code projects directory not found",
|
|
129
|
+
help="~/.claude/projects/ does not exist. Is Claude Code installed?",
|
|
130
|
+
context={
|
|
131
|
+
"path": str(path),
|
|
132
|
+
"projects_dir": str(projects_dir),
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Encode path and find project directory
|
|
137
|
+
encoded_path = encode_path_to_project_folder(path)
|
|
138
|
+
project_dir = projects_dir / encoded_path
|
|
139
|
+
|
|
140
|
+
if not project_dir.exists():
|
|
141
|
+
return ProjectError(
|
|
142
|
+
success=False,
|
|
143
|
+
error="Project directory not found",
|
|
144
|
+
help=f"No Claude Code project found for {path}",
|
|
145
|
+
context={
|
|
146
|
+
"path": str(path),
|
|
147
|
+
"encoded_path": encoded_path,
|
|
148
|
+
"expected_dir": str(project_dir),
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Find all session logs (main sessions and agent logs)
|
|
153
|
+
session_logs = []
|
|
154
|
+
latest_session: tuple[str, float] | None = None
|
|
155
|
+
|
|
156
|
+
for log_file in project_dir.iterdir():
|
|
157
|
+
if log_file.is_file() and log_file.suffix == ".jsonl":
|
|
158
|
+
session_logs.append(log_file.name)
|
|
159
|
+
|
|
160
|
+
# Track latest main session (not agent logs)
|
|
161
|
+
if not log_file.name.startswith("agent-"):
|
|
162
|
+
mtime = log_file.stat().st_mtime
|
|
163
|
+
if latest_session is None or mtime > latest_session[1]:
|
|
164
|
+
# Extract session ID (filename without .jsonl)
|
|
165
|
+
session_id = log_file.stem
|
|
166
|
+
latest_session = (session_id, mtime)
|
|
167
|
+
|
|
168
|
+
# Sort logs for consistent output
|
|
169
|
+
session_logs.sort()
|
|
170
|
+
|
|
171
|
+
return ProjectInfo(
|
|
172
|
+
success=True,
|
|
173
|
+
project_dir=str(project_dir),
|
|
174
|
+
cwd=str(path),
|
|
175
|
+
encoded_path=encoded_path,
|
|
176
|
+
session_logs=session_logs,
|
|
177
|
+
latest_session_id=latest_session[0] if latest_session else None,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@click.command(name="find-project-dir")
|
|
182
|
+
@click.option(
|
|
183
|
+
"--path",
|
|
184
|
+
type=click.Path(exists=True, path_type=Path),
|
|
185
|
+
help="Path to find project for (defaults to current directory)",
|
|
186
|
+
)
|
|
187
|
+
@click.option(
|
|
188
|
+
"--json",
|
|
189
|
+
"json_output",
|
|
190
|
+
is_flag=True,
|
|
191
|
+
help="Output in JSON format",
|
|
192
|
+
)
|
|
193
|
+
@click.pass_context
|
|
194
|
+
def find_project_dir(ctx: click.Context, path: Path | None, json_output: bool) -> None:
|
|
195
|
+
"""Find Claude Code project directory for a filesystem path.
|
|
196
|
+
|
|
197
|
+
This command maps filesystem paths to Claude Code project directories
|
|
198
|
+
in ~/.claude/projects/ using deterministic encoding rules.
|
|
199
|
+
"""
|
|
200
|
+
installation = require_claude_installation(ctx)
|
|
201
|
+
|
|
202
|
+
# Default to current directory if no path specified
|
|
203
|
+
if path is None:
|
|
204
|
+
path = Path(os.getcwd())
|
|
205
|
+
|
|
206
|
+
# Find project information
|
|
207
|
+
result = find_project_info(path, installation)
|
|
208
|
+
|
|
209
|
+
# Always output JSON (the --json flag is for future extensibility)
|
|
210
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
211
|
+
|
|
212
|
+
# Exit with error code if not found
|
|
213
|
+
if isinstance(result, ProjectError):
|
|
214
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Generate PR summary from PR diff.
|
|
2
|
+
|
|
3
|
+
This exec command generates a PR summary by analyzing the PR diff
|
|
4
|
+
using Claude. It uses the same prompt as commit message generation but
|
|
5
|
+
does NOT include commit messages (which may contain misleading info
|
|
6
|
+
about .worker-impl/ deletions).
|
|
7
|
+
|
|
8
|
+
This is used by the GitHub Actions workflow when updating PR bodies
|
|
9
|
+
after implementation.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
erk exec generate-pr-summary --pr-number 123
|
|
13
|
+
|
|
14
|
+
Output:
|
|
15
|
+
PR summary text (title on first line, body follows)
|
|
16
|
+
|
|
17
|
+
Exit Codes:
|
|
18
|
+
0: Success
|
|
19
|
+
1: Error (missing pr-number, no diff, Claude failure)
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
$ erk exec generate-pr-summary --pr-number 1895
|
|
23
|
+
Fix authentication flow for OAuth providers
|
|
24
|
+
|
|
25
|
+
This PR fixes the OAuth authentication flow...
|
|
26
|
+
...
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
|
|
31
|
+
from erk_shared.context.helpers import (
|
|
32
|
+
require_git,
|
|
33
|
+
require_github,
|
|
34
|
+
require_prompt_executor,
|
|
35
|
+
require_repo_root,
|
|
36
|
+
)
|
|
37
|
+
from erk_shared.gateway.gt.prompts import COMMIT_MESSAGE_SYSTEM_PROMPT, truncate_diff
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_prompt(diff_content: str, current_branch: str, parent_branch: str) -> str:
|
|
41
|
+
"""Build prompt for PR summary generation.
|
|
42
|
+
|
|
43
|
+
Note: We deliberately do NOT include commit messages here, unlike
|
|
44
|
+
CommitMessageGenerator. The commit messages may contain info about
|
|
45
|
+
.worker-impl/ deletions that don't appear in the final PR diff.
|
|
46
|
+
"""
|
|
47
|
+
context_section = f"""## Context
|
|
48
|
+
|
|
49
|
+
- Current branch: {current_branch}
|
|
50
|
+
- Parent branch: {parent_branch}"""
|
|
51
|
+
|
|
52
|
+
return f"""{COMMIT_MESSAGE_SYSTEM_PROMPT}
|
|
53
|
+
|
|
54
|
+
{context_section}
|
|
55
|
+
|
|
56
|
+
## Diff
|
|
57
|
+
|
|
58
|
+
```diff
|
|
59
|
+
{diff_content}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Generate a commit message for this diff:"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.command(name="generate-pr-summary")
|
|
66
|
+
@click.option("--pr-number", type=int, required=True, help="PR number to summarize")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def generate_pr_summary(ctx: click.Context, pr_number: int) -> None:
|
|
69
|
+
"""Generate PR summary from PR diff using Claude.
|
|
70
|
+
|
|
71
|
+
Analyzes the PR diff (what GitHub shows) and generates a summary.
|
|
72
|
+
Does NOT use commit messages, which may contain misleading info
|
|
73
|
+
about files that net to zero in the final diff.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
pr_number: The PR number to analyze
|
|
77
|
+
"""
|
|
78
|
+
repo_root = require_repo_root(ctx)
|
|
79
|
+
github = require_github(ctx)
|
|
80
|
+
git = require_git(ctx)
|
|
81
|
+
executor = require_prompt_executor(ctx)
|
|
82
|
+
|
|
83
|
+
# Get PR diff
|
|
84
|
+
try:
|
|
85
|
+
pr_diff = github.get_pr_diff(repo_root, pr_number)
|
|
86
|
+
except RuntimeError as e:
|
|
87
|
+
click.echo(f"Error: Failed to get PR diff: {e}", err=True)
|
|
88
|
+
raise SystemExit(1) from e
|
|
89
|
+
|
|
90
|
+
if not pr_diff.strip():
|
|
91
|
+
click.echo("Error: PR diff is empty", err=True)
|
|
92
|
+
raise SystemExit(1)
|
|
93
|
+
|
|
94
|
+
# Truncate if needed
|
|
95
|
+
diff_content, was_truncated = truncate_diff(pr_diff)
|
|
96
|
+
if was_truncated:
|
|
97
|
+
click.echo("Warning: Diff truncated for size", err=True)
|
|
98
|
+
|
|
99
|
+
# Get branch context using injected Git
|
|
100
|
+
current_branch = git.get_current_branch(repo_root) or f"pr-{pr_number}"
|
|
101
|
+
parent_branch = git.detect_trunk_branch(repo_root)
|
|
102
|
+
|
|
103
|
+
# Build prompt and run Claude via injected executor
|
|
104
|
+
prompt = _build_prompt(diff_content, current_branch, parent_branch)
|
|
105
|
+
result = executor.execute_prompt(prompt, model="haiku", cwd=repo_root)
|
|
106
|
+
|
|
107
|
+
if not result.success:
|
|
108
|
+
click.echo(f"Error: Claude execution failed: {result.error}", err=True)
|
|
109
|
+
raise SystemExit(1) from None
|
|
110
|
+
|
|
111
|
+
# Output the summary (no trailing newline, let caller handle formatting)
|
|
112
|
+
click.echo(result.output, nl=False)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Get closing text for PR body based on .impl/issue.json or branch name.
|
|
3
|
+
|
|
4
|
+
This command determines the issue number from .impl/issue.json or the branch
|
|
5
|
+
name (P{issue_number}-... pattern) and outputs the appropriate closing text.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
erk exec get-closing-text
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
Plain text "Closes #N" (same-repo) or "Closes owner/repo#N" (cross-repo)
|
|
12
|
+
Empty output if no issue reference found
|
|
13
|
+
|
|
14
|
+
Exit Codes:
|
|
15
|
+
0: Success (whether issue reference exists or not)
|
|
16
|
+
1: Error (branch/issue.json mismatch)
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
$ erk exec get-closing-text
|
|
20
|
+
Closes #776
|
|
21
|
+
|
|
22
|
+
$ erk exec get-closing-text # Cross-repo plans
|
|
23
|
+
Closes owner/plans-repo#776
|
|
24
|
+
|
|
25
|
+
$ erk exec get-closing-text # No .impl/issue.json but branch is P123-feature
|
|
26
|
+
Closes #123
|
|
27
|
+
|
|
28
|
+
$ erk exec get-closing-text # No .impl/ and branch is feature-branch
|
|
29
|
+
(no output)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
|
|
36
|
+
from erk.cli.config import load_config
|
|
37
|
+
from erk_shared.context.helpers import get_current_branch, require_cwd
|
|
38
|
+
from erk_shared.impl_folder import validate_issue_linkage
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _find_repo_root(start: Path) -> Path | None:
|
|
42
|
+
"""Find repository root by looking for .git directory."""
|
|
43
|
+
current = start
|
|
44
|
+
while current != current.parent:
|
|
45
|
+
if (current / ".git").exists():
|
|
46
|
+
return current
|
|
47
|
+
current = current.parent
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.command(name="get-closing-text")
|
|
52
|
+
@click.pass_context
|
|
53
|
+
def get_closing_text(ctx: click.Context) -> None:
|
|
54
|
+
"""Get closing text for PR body based on .impl/issue.json or branch name.
|
|
55
|
+
|
|
56
|
+
Validates that branch name and .impl/issue.json agree (if both present).
|
|
57
|
+
Falls back to branch name if no .impl/ folder exists.
|
|
58
|
+
|
|
59
|
+
Outputs nothing and exits successfully if no issue number is discoverable.
|
|
60
|
+
"""
|
|
61
|
+
cwd = require_cwd(ctx)
|
|
62
|
+
|
|
63
|
+
# Get current branch name for validation and fallback
|
|
64
|
+
branch_name = get_current_branch(ctx)
|
|
65
|
+
if branch_name is None:
|
|
66
|
+
# Not on a branch (detached HEAD) - can't determine issue number
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Check .impl/ first, then .worker-impl/
|
|
70
|
+
impl_dir = cwd / ".impl"
|
|
71
|
+
if not impl_dir.exists():
|
|
72
|
+
impl_dir = cwd / ".worker-impl"
|
|
73
|
+
|
|
74
|
+
# Validate linkage and get issue number (branch fallback if no .impl/)
|
|
75
|
+
try:
|
|
76
|
+
issue_number = validate_issue_linkage(impl_dir, branch_name)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
click.echo(f"Error: {e}", err=True)
|
|
79
|
+
raise SystemExit(1) from None
|
|
80
|
+
|
|
81
|
+
if issue_number is None:
|
|
82
|
+
# No issue to close (neither branch nor .impl/ has one)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Load config to check for cross-repo plans
|
|
86
|
+
repo_root = _find_repo_root(cwd)
|
|
87
|
+
plans_repo: str | None = None
|
|
88
|
+
if repo_root is not None:
|
|
89
|
+
config = load_config(repo_root)
|
|
90
|
+
plans_repo = config.plans_repo
|
|
91
|
+
|
|
92
|
+
# Format closing text
|
|
93
|
+
if plans_repo is None:
|
|
94
|
+
closing_text = f"Closes #{issue_number}"
|
|
95
|
+
else:
|
|
96
|
+
closing_text = f"Closes {plans_repo}#{issue_number}"
|
|
97
|
+
|
|
98
|
+
click.echo(closing_text)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Get embedded prompt content from bundled prompts.
|
|
3
|
+
|
|
4
|
+
This command reads prompt files bundled with the erk package and outputs
|
|
5
|
+
their content. Useful for GitHub Actions workflows that need prompt content.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
erk exec get-embedded-prompt <prompt-name>
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
The prompt content (markdown)
|
|
12
|
+
|
|
13
|
+
Exit Codes:
|
|
14
|
+
0: Success
|
|
15
|
+
1: Prompt not found
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
$ erk exec get-embedded-prompt dignified-python-review
|
|
19
|
+
# Dignified Python Review Prompt
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
$ erk exec get-embedded-prompt dignified-python-review > /tmp/prompt.md
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from erk.artifacts.sync import get_bundled_github_dir
|
|
28
|
+
|
|
29
|
+
# Available prompts that can be retrieved
|
|
30
|
+
AVAILABLE_PROMPTS = frozenset(
|
|
31
|
+
{
|
|
32
|
+
"ci-autofix",
|
|
33
|
+
"dignified-python-review",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command(name="get-embedded-prompt")
|
|
39
|
+
@click.argument("prompt_name")
|
|
40
|
+
def get_embedded_prompt(prompt_name: str) -> None:
|
|
41
|
+
"""Get embedded prompt content from bundled prompts.
|
|
42
|
+
|
|
43
|
+
Reads the specified prompt from the erk package's bundled prompts
|
|
44
|
+
and outputs its content to stdout.
|
|
45
|
+
|
|
46
|
+
PROMPT_NAME is the name of the prompt (without .md extension).
|
|
47
|
+
"""
|
|
48
|
+
if prompt_name not in AVAILABLE_PROMPTS:
|
|
49
|
+
available = ", ".join(sorted(AVAILABLE_PROMPTS))
|
|
50
|
+
click.echo(f"Unknown prompt: {prompt_name}", err=True)
|
|
51
|
+
click.echo(f"Available prompts: {available}", err=True)
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
bundled_github_dir = get_bundled_github_dir()
|
|
55
|
+
prompt_path = bundled_github_dir / "prompts" / f"{prompt_name}.md"
|
|
56
|
+
|
|
57
|
+
if not prompt_path.exists():
|
|
58
|
+
click.echo(f"Prompt file not found: {prompt_path}", err=True)
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
content = prompt_path.read_text(encoding="utf-8")
|
|
62
|
+
click.echo(content, nl=False)
|