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,32 @@
|
|
|
1
|
+
"""Quick commit all changes and submit with Graphite CLI command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from erk_shared.gateway.gt.cli import render_events
|
|
10
|
+
from erk_shared.gateway.gt.operations.quick_submit import execute_quick_submit
|
|
11
|
+
from erk_shared.gateway.gt.real import RealGtKit
|
|
12
|
+
from erk_shared.gateway.gt.types import QuickSubmitError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("quick-submit")
|
|
16
|
+
def quick_submit() -> None:
|
|
17
|
+
"""Quick commit all changes and submit with Graphite.
|
|
18
|
+
|
|
19
|
+
Stages all changes, commits with "update" message if there are changes,
|
|
20
|
+
then runs gt submit. This is a fast iteration shortcut.
|
|
21
|
+
|
|
22
|
+
For proper commit messages, use the pr-submit command instead.
|
|
23
|
+
"""
|
|
24
|
+
cwd = Path.cwd()
|
|
25
|
+
ops = RealGtKit(cwd)
|
|
26
|
+
result = render_events(execute_quick_submit(ops, cwd))
|
|
27
|
+
|
|
28
|
+
# Output JSON result
|
|
29
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
30
|
+
|
|
31
|
+
if isinstance(result, QuickSubmitError):
|
|
32
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rebase onto trunk and resolve conflicts with Claude.
|
|
3
|
+
|
|
4
|
+
This command handles merge conflicts during CI workflows by:
|
|
5
|
+
1. Fetching the trunk branch
|
|
6
|
+
2. Checking if the current branch is behind
|
|
7
|
+
3. Starting a rebase
|
|
8
|
+
4. Using Claude to resolve any conflicts
|
|
9
|
+
5. Force pushing after successful rebase
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
erk exec rebase-with-conflict-resolution \
|
|
13
|
+
--trunk-branch master \
|
|
14
|
+
--branch-name feature-branch \
|
|
15
|
+
--model claude-sonnet-4-5
|
|
16
|
+
|
|
17
|
+
Output:
|
|
18
|
+
JSON object with success status
|
|
19
|
+
|
|
20
|
+
Exit Codes:
|
|
21
|
+
0: Success (rebase completed and pushed, or already up-to-date)
|
|
22
|
+
1: Error (conflict resolution failed after max attempts)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
$ erk exec rebase-with-conflict-resolution --trunk-branch main --branch-name my-feature
|
|
26
|
+
{
|
|
27
|
+
"success": true,
|
|
28
|
+
"action": "rebased",
|
|
29
|
+
"commits_behind": 3
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$ erk exec rebase-with-conflict-resolution --trunk-branch main --branch-name my-feature
|
|
33
|
+
{
|
|
34
|
+
"success": true,
|
|
35
|
+
"action": "already-up-to-date",
|
|
36
|
+
"commits_behind": 0
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import subprocess
|
|
42
|
+
from dataclasses import asdict, dataclass
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Literal
|
|
45
|
+
|
|
46
|
+
import click
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class RebaseSuccess:
|
|
51
|
+
"""Success result for rebase operation."""
|
|
52
|
+
|
|
53
|
+
success: bool
|
|
54
|
+
action: Literal["rebased", "already-up-to-date"]
|
|
55
|
+
commits_behind: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class RebaseError:
|
|
60
|
+
"""Error result when rebase fails."""
|
|
61
|
+
|
|
62
|
+
success: bool
|
|
63
|
+
error: Literal["fetch-failed", "rebase-failed", "push-failed"]
|
|
64
|
+
message: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_commits_behind(trunk_branch: str) -> int | None:
|
|
68
|
+
"""Get number of commits behind trunk branch.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Number of commits behind, or None if command fails.
|
|
72
|
+
"""
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
["git", "rev-list", "--count", f"HEAD..origin/{trunk_branch}"],
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
check=False,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
return None
|
|
81
|
+
count_str = result.stdout.strip()
|
|
82
|
+
if not count_str.isdigit():
|
|
83
|
+
return None
|
|
84
|
+
return int(count_str)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_rebase_in_progress() -> bool:
|
|
88
|
+
"""Check if a rebase is currently in progress."""
|
|
89
|
+
git_dir = Path(".git")
|
|
90
|
+
return (git_dir / "rebase-merge").is_dir() or (git_dir / "rebase-apply").is_dir()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _invoke_claude_for_conflicts(model: str) -> bool:
|
|
94
|
+
"""Invoke Claude to fix merge conflicts.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if Claude invocation succeeded (exit code 0).
|
|
98
|
+
"""
|
|
99
|
+
prompt = (
|
|
100
|
+
"Fix all merge conflicts in this repository. "
|
|
101
|
+
"For each conflicted file, read it, resolve the conflict markers appropriately, "
|
|
102
|
+
"and save the file. After fixing all conflicts, stage the resolved files with "
|
|
103
|
+
"'git add' and then run 'git rebase --continue' to continue the rebase."
|
|
104
|
+
)
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
[
|
|
107
|
+
"claude",
|
|
108
|
+
"--print",
|
|
109
|
+
"--model",
|
|
110
|
+
model,
|
|
111
|
+
"--output-format",
|
|
112
|
+
"stream-json",
|
|
113
|
+
"--dangerously-skip-permissions",
|
|
114
|
+
"--verbose",
|
|
115
|
+
prompt,
|
|
116
|
+
],
|
|
117
|
+
capture_output=False, # Let output stream to stdout/stderr
|
|
118
|
+
check=False, # We check returncode explicitly below
|
|
119
|
+
)
|
|
120
|
+
return result.returncode == 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _rebase_with_conflict_resolution_impl(
|
|
124
|
+
trunk_branch: str,
|
|
125
|
+
branch_name: str,
|
|
126
|
+
model: str,
|
|
127
|
+
max_attempts: int,
|
|
128
|
+
) -> RebaseSuccess | RebaseError:
|
|
129
|
+
"""Rebase onto trunk and resolve conflicts with Claude.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
trunk_branch: Trunk branch to rebase onto (e.g., 'main', 'master')
|
|
133
|
+
branch_name: Current branch name for force push
|
|
134
|
+
model: Claude model to use for conflict resolution
|
|
135
|
+
max_attempts: Maximum number of conflict resolution attempts
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
RebaseSuccess on success, RebaseError on failure
|
|
139
|
+
"""
|
|
140
|
+
# Fetch trunk branch
|
|
141
|
+
fetch_result = subprocess.run(
|
|
142
|
+
["git", "fetch", "origin", trunk_branch],
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
check=False, # We check returncode explicitly below
|
|
146
|
+
)
|
|
147
|
+
if fetch_result.returncode != 0:
|
|
148
|
+
return RebaseError(
|
|
149
|
+
success=False,
|
|
150
|
+
error="fetch-failed",
|
|
151
|
+
message=f"Failed to fetch origin/{trunk_branch}: {fetch_result.stderr}",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Check if behind
|
|
155
|
+
commits_behind = _get_commits_behind(trunk_branch)
|
|
156
|
+
if commits_behind is None:
|
|
157
|
+
return RebaseError(
|
|
158
|
+
success=False,
|
|
159
|
+
error="fetch-failed",
|
|
160
|
+
message="Failed to determine commits behind trunk",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if commits_behind == 0:
|
|
164
|
+
return RebaseSuccess(
|
|
165
|
+
success=True,
|
|
166
|
+
action="already-up-to-date",
|
|
167
|
+
commits_behind=0,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Start rebase (may fail with conflicts, which is expected)
|
|
171
|
+
subprocess.run(
|
|
172
|
+
["git", "rebase", f"origin/{trunk_branch}"],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
check=False, # Conflicts expected - we check _is_rebase_in_progress()
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Loop while rebase has conflicts
|
|
179
|
+
attempt = 0
|
|
180
|
+
while _is_rebase_in_progress() and attempt < max_attempts:
|
|
181
|
+
attempt += 1
|
|
182
|
+
# Invoke Claude to fix conflicts
|
|
183
|
+
_invoke_claude_for_conflicts(model)
|
|
184
|
+
|
|
185
|
+
# Check if rebase completed
|
|
186
|
+
if _is_rebase_in_progress():
|
|
187
|
+
# Abort rebase and return error
|
|
188
|
+
subprocess.run(["git", "rebase", "--abort"], capture_output=True, check=False)
|
|
189
|
+
return RebaseError(
|
|
190
|
+
success=False,
|
|
191
|
+
error="rebase-failed",
|
|
192
|
+
message=f"Failed to resolve conflicts after {max_attempts} attempts",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Force push after successful rebase
|
|
196
|
+
push_result = subprocess.run(
|
|
197
|
+
["git", "push", "-f", "origin", branch_name],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
check=False, # We check returncode explicitly below
|
|
201
|
+
)
|
|
202
|
+
if push_result.returncode != 0:
|
|
203
|
+
return RebaseError(
|
|
204
|
+
success=False,
|
|
205
|
+
error="push-failed",
|
|
206
|
+
message=f"Failed to force push: {push_result.stderr}",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return RebaseSuccess(
|
|
210
|
+
success=True,
|
|
211
|
+
action="rebased",
|
|
212
|
+
commits_behind=commits_behind,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@click.command(name="rebase-with-conflict-resolution")
|
|
217
|
+
@click.option(
|
|
218
|
+
"--trunk-branch",
|
|
219
|
+
required=True,
|
|
220
|
+
help="Trunk branch to rebase onto (e.g., 'main', 'master')",
|
|
221
|
+
)
|
|
222
|
+
@click.option(
|
|
223
|
+
"--branch-name",
|
|
224
|
+
required=True,
|
|
225
|
+
help="Current branch name for force push",
|
|
226
|
+
)
|
|
227
|
+
@click.option(
|
|
228
|
+
"--model",
|
|
229
|
+
default="claude-sonnet-4-5",
|
|
230
|
+
help="Claude model to use for conflict resolution",
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"--max-attempts",
|
|
234
|
+
default=5,
|
|
235
|
+
type=int,
|
|
236
|
+
help="Maximum number of conflict resolution attempts",
|
|
237
|
+
)
|
|
238
|
+
def rebase_with_conflict_resolution(
|
|
239
|
+
trunk_branch: str,
|
|
240
|
+
branch_name: str,
|
|
241
|
+
model: str,
|
|
242
|
+
max_attempts: int,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Rebase onto trunk and resolve conflicts with Claude.
|
|
245
|
+
|
|
246
|
+
This command is designed for CI workflows where push may fail due to
|
|
247
|
+
branch divergence. It fetches the trunk branch, rebases onto it,
|
|
248
|
+
and uses Claude to resolve any merge conflicts.
|
|
249
|
+
"""
|
|
250
|
+
result = _rebase_with_conflict_resolution_impl(
|
|
251
|
+
trunk_branch=trunk_branch,
|
|
252
|
+
branch_name=branch_name,
|
|
253
|
+
model=model,
|
|
254
|
+
max_attempts=max_attempts,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
258
|
+
|
|
259
|
+
if isinstance(result, RebaseError):
|
|
260
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Reply to a PR discussion comment with a blockquote and action summary.
|
|
2
|
+
|
|
3
|
+
This exec command posts a reply to a discussion comment that:
|
|
4
|
+
1. Quotes the original comment with author attribution
|
|
5
|
+
2. Includes an action summary explaining what was done
|
|
6
|
+
3. Adds a reaction to the original comment
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
erk exec reply-to-discussion-comment --comment-id 12345 --reply "Action taken: ..."
|
|
10
|
+
erk exec reply-to-discussion-comment --comment-id 12345 --pr 789 --reply "..."
|
|
11
|
+
|
|
12
|
+
Output:
|
|
13
|
+
JSON with success status and reply details
|
|
14
|
+
|
|
15
|
+
Exit Codes:
|
|
16
|
+
0: Success
|
|
17
|
+
1: Error (comment not found, API failure, etc.)
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
$ erk exec reply-to-discussion-comment --comment-id 12345 --reply "Fixed typo in docs"
|
|
21
|
+
{"success": true, "comment_id": 12345, "reply_id": 67890}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from datetime import UTC, datetime
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from erk.cli.script_output import exit_with_error
|
|
30
|
+
from erk_shared.context.helpers import (
|
|
31
|
+
get_current_branch,
|
|
32
|
+
require_github,
|
|
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.checks import GitHubChecks
|
|
39
|
+
from erk_shared.non_ideal_state import (
|
|
40
|
+
BranchDetectionFailed,
|
|
41
|
+
GitHubAPIFailed,
|
|
42
|
+
NoPRForBranch,
|
|
43
|
+
PRNotFoundError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_reply(author: str, url: str, body: str, action_summary: str) -> str:
|
|
48
|
+
"""Format a reply with blockquote of original comment and action summary.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
author: Original comment author's GitHub login
|
|
52
|
+
url: URL to the original comment
|
|
53
|
+
body: Body of the original comment
|
|
54
|
+
action_summary: Description of action taken
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Formatted markdown reply
|
|
58
|
+
"""
|
|
59
|
+
# Quote the original comment (truncate if very long)
|
|
60
|
+
quoted_lines = body.strip().split("\n")
|
|
61
|
+
if len(quoted_lines) > 10:
|
|
62
|
+
# Truncate long comments
|
|
63
|
+
quoted_body = "\n".join(quoted_lines[:10]) + "\n> ..."
|
|
64
|
+
else:
|
|
65
|
+
quoted_body = body.strip()
|
|
66
|
+
|
|
67
|
+
# Add blockquote prefix to each line
|
|
68
|
+
quoted = "\n".join(f"> {line}" for line in quoted_body.split("\n"))
|
|
69
|
+
|
|
70
|
+
# Get current timestamp
|
|
71
|
+
now = datetime.now(UTC).strftime("%Y-%m-%d %I:%M %p UTC")
|
|
72
|
+
|
|
73
|
+
return f"""> **@{author}** [commented]({url}):
|
|
74
|
+
{quoted}
|
|
75
|
+
|
|
76
|
+
{action_summary}
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
<sub>Addressed via `/erk:pr-address` at {now}</sub>"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.command(name="reply-to-discussion-comment")
|
|
83
|
+
@click.option("--comment-id", required=True, type=int, help="Numeric comment ID to reply to")
|
|
84
|
+
@click.option("--pr", type=int, default=None, help="PR number (defaults to current branch's PR)")
|
|
85
|
+
@click.option("--reply", required=True, help="Action summary text (what was done)")
|
|
86
|
+
@click.pass_context
|
|
87
|
+
def reply_to_discussion_comment(
|
|
88
|
+
ctx: click.Context, comment_id: int, pr: int | None, reply: str
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Reply to a PR discussion comment with quote and action summary.
|
|
91
|
+
|
|
92
|
+
Fetches the original comment to get its author and body, then posts a
|
|
93
|
+
formatted reply quoting the original with your action summary. Also adds
|
|
94
|
+
a +1 reaction to the original comment.
|
|
95
|
+
"""
|
|
96
|
+
# Get dependencies from context
|
|
97
|
+
repo_root = require_repo_root(ctx)
|
|
98
|
+
github = require_github(ctx)
|
|
99
|
+
github_issues = require_github_issues(ctx)
|
|
100
|
+
|
|
101
|
+
# Get PR details - either from current branch or specified PR number
|
|
102
|
+
if pr is None:
|
|
103
|
+
branch_result = GitHubChecks.branch(get_current_branch(ctx))
|
|
104
|
+
if isinstance(branch_result, BranchDetectionFailed):
|
|
105
|
+
exit_with_error(branch_result.error_type, branch_result.message)
|
|
106
|
+
# Type narrowing: exit_with_error returns NoReturn, so branch_result is str
|
|
107
|
+
assert not isinstance(branch_result, BranchDetectionFailed)
|
|
108
|
+
branch = branch_result
|
|
109
|
+
|
|
110
|
+
pr_result = GitHubChecks.pr_for_branch(github, repo_root, branch)
|
|
111
|
+
if isinstance(pr_result, NoPRForBranch):
|
|
112
|
+
exit_with_error(pr_result.error_type, pr_result.message)
|
|
113
|
+
assert not isinstance(pr_result, NoPRForBranch)
|
|
114
|
+
pr_details = pr_result
|
|
115
|
+
else:
|
|
116
|
+
pr_result = GitHubChecks.pr_by_number(github, repo_root, pr)
|
|
117
|
+
if isinstance(pr_result, PRNotFoundError):
|
|
118
|
+
exit_with_error(pr_result.error_type, pr_result.message)
|
|
119
|
+
assert not isinstance(pr_result, PRNotFoundError)
|
|
120
|
+
pr_details = pr_result
|
|
121
|
+
|
|
122
|
+
# Fetch all discussion comments to find the one we're replying to
|
|
123
|
+
comments_result = GitHubChecks.issue_comments(github_issues, repo_root, pr_details.number)
|
|
124
|
+
if isinstance(comments_result, GitHubAPIFailed):
|
|
125
|
+
exit_with_error(comments_result.error_type, comments_result.message)
|
|
126
|
+
assert not isinstance(comments_result, GitHubAPIFailed)
|
|
127
|
+
|
|
128
|
+
# Find the comment by ID
|
|
129
|
+
target_comment = None
|
|
130
|
+
for comment in comments_result:
|
|
131
|
+
if comment.id == comment_id:
|
|
132
|
+
target_comment = comment
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
if target_comment is None:
|
|
136
|
+
exit_with_error(
|
|
137
|
+
"comment-not-found",
|
|
138
|
+
f"Comment ID {comment_id} not found in PR #{pr_details.number} discussion",
|
|
139
|
+
)
|
|
140
|
+
# Type narrowing: target_comment is not None after the check above
|
|
141
|
+
assert target_comment is not None
|
|
142
|
+
|
|
143
|
+
# Format the reply
|
|
144
|
+
reply_body = _format_reply(
|
|
145
|
+
author=target_comment.author,
|
|
146
|
+
url=target_comment.url,
|
|
147
|
+
body=target_comment.body,
|
|
148
|
+
action_summary=reply,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Post the reply as a new comment
|
|
152
|
+
try:
|
|
153
|
+
reply_comment_id = github_issues.add_comment(repo_root, pr_details.number, reply_body)
|
|
154
|
+
except RuntimeError as e:
|
|
155
|
+
exit_with_error("github-api-error", f"Failed to post reply: {e}")
|
|
156
|
+
|
|
157
|
+
# Add reaction to original comment
|
|
158
|
+
reaction_result = GitHubChecks.add_reaction(github_issues, repo_root, comment_id, "+1")
|
|
159
|
+
if isinstance(reaction_result, GitHubAPIFailed):
|
|
160
|
+
# Non-fatal: reply was posted, just log warning
|
|
161
|
+
click.echo(
|
|
162
|
+
f"Warning: Reply posted but failed to add reaction: {reaction_result.message}",
|
|
163
|
+
err=True,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
result = {
|
|
167
|
+
"success": True,
|
|
168
|
+
"comment_id": comment_id,
|
|
169
|
+
"reply_id": reply_comment_id,
|
|
170
|
+
"pr_number": pr_details.number,
|
|
171
|
+
}
|
|
172
|
+
click.echo(json.dumps(result, indent=2))
|
|
173
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Resolve a PR review thread via GraphQL mutation.
|
|
2
|
+
|
|
3
|
+
This exec command resolves a single PR review thread and outputs
|
|
4
|
+
JSON with the result. Optionally adds a reply comment before resolving.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
erk exec resolve-review-thread --thread-id "PRRT_xxxx"
|
|
8
|
+
erk exec resolve-review-thread --thread-id "PRRT_xxxx" --comment "Resolved via ..."
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
JSON with success status
|
|
12
|
+
|
|
13
|
+
Exit Codes:
|
|
14
|
+
0: Always (even on error, to support || true pattern)
|
|
15
|
+
1: Context not initialized
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
$ erk exec resolve-review-thread --thread-id "PRRT_abc123"
|
|
19
|
+
{"success": true, "thread_id": "PRRT_abc123"}
|
|
20
|
+
|
|
21
|
+
$ erk exec resolve-review-thread --thread-id "PRRT_abc123" --comment "Fixed"
|
|
22
|
+
{"success": true, "thread_id": "PRRT_abc123", "comment_added": true}
|
|
23
|
+
|
|
24
|
+
$ erk exec resolve-review-thread --thread-id "invalid"
|
|
25
|
+
{"success": false, "error_type": "resolution_failed", "message": "..."}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import asdict, dataclass
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
35
|
+
|
|
36
|
+
import click
|
|
37
|
+
|
|
38
|
+
from erk_shared.context.helpers import require_github, require_repo_root
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from erk_shared.github.abc import GitHub
|
|
42
|
+
|
|
43
|
+
T = TypeVar("T")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ResolveThreadSuccess:
|
|
48
|
+
"""Success response for thread resolution."""
|
|
49
|
+
|
|
50
|
+
success: bool
|
|
51
|
+
thread_id: str
|
|
52
|
+
comment_added: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ResolveThreadError:
|
|
57
|
+
"""Error response for thread resolution."""
|
|
58
|
+
|
|
59
|
+
success: bool
|
|
60
|
+
error_type: str
|
|
61
|
+
message: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_resolution_comment(comment: str) -> str:
|
|
65
|
+
"""Format a resolution comment with timestamp and source attribution.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
comment: The user-provided comment text
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Formatted comment with timestamp and /erk:pr-address attribution
|
|
72
|
+
"""
|
|
73
|
+
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M %Z")
|
|
74
|
+
return f"{comment}\n\n_Addressed via `/erk:pr-address` at {timestamp}_"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ensure_not_error(result: T | ResolveThreadError) -> T:
|
|
78
|
+
"""Ensure result is not an error, otherwise output JSON and exit.
|
|
79
|
+
|
|
80
|
+
Provides type narrowing: takes `T | ResolveThreadError` and returns `T`.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
result: Value that may be a ResolveThreadError
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The value unchanged if not an error (with narrowed type T)
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
SystemExit: If result is ResolveThreadError (with exit code 0)
|
|
90
|
+
"""
|
|
91
|
+
if isinstance(result, ResolveThreadError):
|
|
92
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
93
|
+
raise SystemExit(0)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _add_comment_if_provided(
|
|
98
|
+
github: GitHub,
|
|
99
|
+
repo_root: Path,
|
|
100
|
+
thread_id: str,
|
|
101
|
+
comment: str | None,
|
|
102
|
+
) -> bool | ResolveThreadError:
|
|
103
|
+
"""Add a comment to the thread if provided.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True/False for comment_added status, or ResolveThreadError on failure
|
|
107
|
+
"""
|
|
108
|
+
if comment is None:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
formatted_comment = _format_resolution_comment(comment)
|
|
112
|
+
try:
|
|
113
|
+
return github.add_review_thread_reply(repo_root, thread_id, formatted_comment)
|
|
114
|
+
except RuntimeError as e:
|
|
115
|
+
return ResolveThreadError(
|
|
116
|
+
success=False,
|
|
117
|
+
error_type="comment-failed",
|
|
118
|
+
message=f"Failed to add comment: {e}",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@click.command(name="resolve-review-thread")
|
|
123
|
+
@click.option("--thread-id", required=True, help="GraphQL node ID of the thread to resolve")
|
|
124
|
+
@click.option("--comment", default=None, help="Optional comment to add before resolving")
|
|
125
|
+
@click.pass_context
|
|
126
|
+
def resolve_review_thread(ctx: click.Context, thread_id: str, comment: str | None) -> None:
|
|
127
|
+
"""Resolve a PR review thread.
|
|
128
|
+
|
|
129
|
+
Takes a GraphQL node ID (from get-pr-review-comments output) and
|
|
130
|
+
marks the thread as resolved. Optionally adds a reply comment first.
|
|
131
|
+
|
|
132
|
+
THREAD_ID: GraphQL node ID of the review thread
|
|
133
|
+
"""
|
|
134
|
+
# Get dependencies from context
|
|
135
|
+
repo_root = require_repo_root(ctx)
|
|
136
|
+
github = require_github(ctx)
|
|
137
|
+
|
|
138
|
+
# Add comment first if provided
|
|
139
|
+
comment_added = _ensure_not_error(
|
|
140
|
+
_add_comment_if_provided(github, repo_root, thread_id, comment)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Attempt to resolve the thread
|
|
144
|
+
try:
|
|
145
|
+
resolved = github.resolve_review_thread(repo_root, thread_id)
|
|
146
|
+
except RuntimeError as e:
|
|
147
|
+
result = ResolveThreadError(
|
|
148
|
+
success=False,
|
|
149
|
+
error_type="github-api-failed",
|
|
150
|
+
message=str(e),
|
|
151
|
+
)
|
|
152
|
+
click.echo(json.dumps(asdict(result), indent=2))
|
|
153
|
+
raise SystemExit(0) from None
|
|
154
|
+
|
|
155
|
+
if resolved:
|
|
156
|
+
result_success = ResolveThreadSuccess(
|
|
157
|
+
success=True,
|
|
158
|
+
thread_id=thread_id,
|
|
159
|
+
comment_added=comment_added,
|
|
160
|
+
)
|
|
161
|
+
click.echo(json.dumps(asdict(result_success), indent=2))
|
|
162
|
+
else:
|
|
163
|
+
result_error = ResolveThreadError(
|
|
164
|
+
success=False,
|
|
165
|
+
error_type="resolution-failed",
|
|
166
|
+
message=f"Failed to resolve thread {thread_id}",
|
|
167
|
+
)
|
|
168
|
+
click.echo(json.dumps(asdict(result_error), indent=2))
|
|
169
|
+
|
|
170
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session ID Injector Hook
|
|
4
|
+
|
|
5
|
+
This command is invoked via erk exec session-id-injector-hook.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from erk.hooks.decorators import HookContext, hook_command
|
|
11
|
+
from erk_shared.gateway.erk_installation.real import RealErkInstallation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_github_planning_enabled() -> bool:
|
|
15
|
+
"""Check if github_planning is enabled in ~/.erk/config.toml.
|
|
16
|
+
|
|
17
|
+
Returns True (enabled) if config doesn't exist or flag is missing.
|
|
18
|
+
"""
|
|
19
|
+
# Use RealErkInstallation directly since hooks run outside normal CLI context
|
|
20
|
+
installation = RealErkInstallation()
|
|
21
|
+
if not installation.config_exists():
|
|
22
|
+
return True # Default enabled
|
|
23
|
+
|
|
24
|
+
config = installation.load_config()
|
|
25
|
+
return config.github_planning
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@hook_command(name="session-id-injector-hook")
|
|
29
|
+
def session_id_injector_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
30
|
+
"""Inject session ID into conversation context when relevant."""
|
|
31
|
+
# Scope check: only run in erk-managed projects
|
|
32
|
+
if not hook_ctx.is_erk_project:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Early exit if github_planning is disabled - output nothing
|
|
36
|
+
if not _is_github_planning_enabled():
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Output session ID if available
|
|
40
|
+
if hook_ctx.session_id is not None:
|
|
41
|
+
# Write to file for CLI tools to read (worktree-scoped persistence)
|
|
42
|
+
session_file = hook_ctx.repo_root / ".erk" / "scratch" / "current-session-id"
|
|
43
|
+
session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
session_file.write_text(hook_ctx.session_id, encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
# Still output reminder for LLM context
|
|
47
|
+
click.echo(f"📌 session: {hook_ctx.session_id}")
|
|
48
|
+
# If no session ID available, output nothing (hook doesn't fire unnecessarily)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
session_id_injector_hook()
|