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,251 @@
|
|
|
1
|
+
name: erk-impl
|
|
2
|
+
run-name: "${{ inputs.issue_number }}:${{ inputs.distinct_id }}"
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
issue_number:
|
|
8
|
+
description: "GitHub issue number to implement"
|
|
9
|
+
required: true
|
|
10
|
+
type: string
|
|
11
|
+
submitted_by:
|
|
12
|
+
description: "GitHub username of the person who submitted the issue"
|
|
13
|
+
required: true
|
|
14
|
+
type: string
|
|
15
|
+
distinct_id:
|
|
16
|
+
description: "Unique identifier for run discovery (base36)"
|
|
17
|
+
required: true
|
|
18
|
+
type: string
|
|
19
|
+
issue_title:
|
|
20
|
+
description: "Issue title for workflow run display"
|
|
21
|
+
required: true
|
|
22
|
+
type: string
|
|
23
|
+
branch_name:
|
|
24
|
+
description: "Branch name for implementation (created by submit command)"
|
|
25
|
+
required: true
|
|
26
|
+
type: string
|
|
27
|
+
pr_number:
|
|
28
|
+
description: "PR number for implementation (created by submit command)"
|
|
29
|
+
required: true
|
|
30
|
+
type: string
|
|
31
|
+
model_name:
|
|
32
|
+
description: "Claude model to use for implementation"
|
|
33
|
+
required: false
|
|
34
|
+
type: string
|
|
35
|
+
default: "claude-sonnet-4-5"
|
|
36
|
+
|
|
37
|
+
concurrency:
|
|
38
|
+
group: implement-issue-${{ github.event.inputs.issue_number }}
|
|
39
|
+
cancel-in-progress: true
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
implement:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
timeout-minutes: 180
|
|
45
|
+
permissions:
|
|
46
|
+
contents: write
|
|
47
|
+
pull-requests: write
|
|
48
|
+
issues: write
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
with:
|
|
52
|
+
token: ${{ secrets.ERK_QUEUE_GH_PAT }}
|
|
53
|
+
fetch-depth: 0
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v5
|
|
57
|
+
with:
|
|
58
|
+
python-version: "3.14"
|
|
59
|
+
|
|
60
|
+
- name: Install Claude Code
|
|
61
|
+
uses: ./.github/actions/setup-claude-code
|
|
62
|
+
|
|
63
|
+
- name: Setup remaining tools
|
|
64
|
+
run: |
|
|
65
|
+
npm install -g prettier
|
|
66
|
+
cd $GITHUB_WORKSPACE
|
|
67
|
+
uv tool install -e . --with-editable ./packages/erk-shared
|
|
68
|
+
|
|
69
|
+
- name: Configure git
|
|
70
|
+
env:
|
|
71
|
+
SUBMITTED_BY: ${{ inputs.submitted_by }}
|
|
72
|
+
run: |
|
|
73
|
+
git config user.name "$SUBMITTED_BY"
|
|
74
|
+
git config user.email "${SUBMITTED_BY}@users.noreply.github.com"
|
|
75
|
+
|
|
76
|
+
- name: Detect trunk branch
|
|
77
|
+
id: trunk
|
|
78
|
+
run: |
|
|
79
|
+
result=$(erk exec detect-trunk-branch)
|
|
80
|
+
echo "trunk_branch=$(echo "$result" | jq -r '.trunk_branch')" >> $GITHUB_OUTPUT
|
|
81
|
+
|
|
82
|
+
- name: Set branch and PR from inputs
|
|
83
|
+
id: find_pr
|
|
84
|
+
run: |
|
|
85
|
+
echo "branch_name=${{ inputs.branch_name }}" >> $GITHUB_OUTPUT
|
|
86
|
+
echo "pr_number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
|
87
|
+
echo "pr_exists=true" >> $GITHUB_OUTPUT
|
|
88
|
+
echo "Using branch: ${{ inputs.branch_name }}"
|
|
89
|
+
echo "Using PR: #${{ inputs.pr_number }}"
|
|
90
|
+
|
|
91
|
+
- name: Checkout implementation branch
|
|
92
|
+
env:
|
|
93
|
+
BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
|
|
94
|
+
ISSUE_NUMBER: ${{ inputs.issue_number }}
|
|
95
|
+
SUBMITTED_BY: ${{ inputs.submitted_by }}
|
|
96
|
+
GH_TOKEN: ${{ github.token }}
|
|
97
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
98
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
99
|
+
run: |
|
|
100
|
+
git fetch origin "$BRANCH_NAME"
|
|
101
|
+
git checkout "$BRANCH_NAME"
|
|
102
|
+
rm -rf .worker-impl
|
|
103
|
+
erk exec create-worker-impl-from-issue "$ISSUE_NUMBER"
|
|
104
|
+
if [ $? -ne 0 ]; then
|
|
105
|
+
gh issue comment "$ISSUE_NUMBER" --body "Could not fetch plan content for issue #$ISSUE_NUMBER."
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
git config user.name "$SUBMITTED_BY"
|
|
109
|
+
git config user.email "$SUBMITTED_BY@users.noreply.github.com"
|
|
110
|
+
git add .worker-impl
|
|
111
|
+
if [[ -n $(git status --porcelain) ]]; then
|
|
112
|
+
git commit -m "Update plan for issue #$ISSUE_NUMBER (rerun)"
|
|
113
|
+
git push origin "$BRANCH_NAME"
|
|
114
|
+
fi
|
|
115
|
+
echo "Checked out branch: $BRANCH_NAME"
|
|
116
|
+
|
|
117
|
+
- name: Post workflow started comment
|
|
118
|
+
env:
|
|
119
|
+
ISSUE_NUMBER: ${{ inputs.issue_number }}
|
|
120
|
+
BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
|
|
121
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
122
|
+
GH_TOKEN: ${{ github.token }}
|
|
123
|
+
run: |
|
|
124
|
+
erk exec post-workflow-started-comment \
|
|
125
|
+
--issue-number "$ISSUE_NUMBER" \
|
|
126
|
+
--branch-name "$BRANCH_NAME" \
|
|
127
|
+
--pr-number "$PR_NUMBER" \
|
|
128
|
+
--run-id "${{ github.run_id }}" \
|
|
129
|
+
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
130
|
+
--repository "${{ github.repository }}"
|
|
131
|
+
|
|
132
|
+
- name: Add remote execution note to PR
|
|
133
|
+
env:
|
|
134
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
135
|
+
GH_TOKEN: ${{ github.token }}
|
|
136
|
+
WORKFLOW_RUN_ID: ${{ github.run_id }}
|
|
137
|
+
WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
138
|
+
run: |
|
|
139
|
+
erk exec add-remote-execution-note \
|
|
140
|
+
--pr-number "$PR_NUMBER" \
|
|
141
|
+
--run-id "$WORKFLOW_RUN_ID" \
|
|
142
|
+
--run-url "$WORKFLOW_RUN_URL"
|
|
143
|
+
|
|
144
|
+
- name: Set up implementation folder
|
|
145
|
+
run: |
|
|
146
|
+
cp -r .worker-impl .impl
|
|
147
|
+
echo "Copied .worker-impl/ to .impl/"
|
|
148
|
+
cat > .impl/run-info.json <<EOF
|
|
149
|
+
{
|
|
150
|
+
"run_id": "${{ github.run_id }}",
|
|
151
|
+
"run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
152
|
+
}
|
|
153
|
+
EOF
|
|
154
|
+
echo "Created .impl/run-info.json"
|
|
155
|
+
|
|
156
|
+
- name: Run implementation
|
|
157
|
+
id: implement
|
|
158
|
+
run: |
|
|
159
|
+
set +e
|
|
160
|
+
claude --print \
|
|
161
|
+
--model ${{ inputs.model_name }} \
|
|
162
|
+
--output-format stream-json \
|
|
163
|
+
--dangerously-skip-permissions \
|
|
164
|
+
--verbose \
|
|
165
|
+
"/erk:plan-implement"
|
|
166
|
+
EXIT_CODE=$?
|
|
167
|
+
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
|
|
168
|
+
if [ $EXIT_CODE -eq 0 ]; then
|
|
169
|
+
echo "implementation_success=true" >> $GITHUB_OUTPUT
|
|
170
|
+
else
|
|
171
|
+
echo "implementation_success=false" >> $GITHUB_OUTPUT
|
|
172
|
+
fi
|
|
173
|
+
if [ -d .worker-impl/ ]; then
|
|
174
|
+
rm -rf .worker-impl/
|
|
175
|
+
echo "Removed .worker-impl/ before submission"
|
|
176
|
+
fi
|
|
177
|
+
exit 0
|
|
178
|
+
env:
|
|
179
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
180
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
181
|
+
GH_TOKEN: ${{ github.token }}
|
|
182
|
+
PR_NUMBER: ${{ inputs.pr_number }}
|
|
183
|
+
|
|
184
|
+
- name: Submit branch with proper commit message
|
|
185
|
+
id: submit
|
|
186
|
+
continue-on-error: true
|
|
187
|
+
if: steps.implement.outputs.implementation_success == 'true'
|
|
188
|
+
env:
|
|
189
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
190
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
191
|
+
GH_TOKEN: ${{ github.token }}
|
|
192
|
+
ISSUE_NUMBER: ${{ inputs.issue_number }}
|
|
193
|
+
BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
|
|
194
|
+
SUBMITTED_BY: ${{ inputs.submitted_by }}
|
|
195
|
+
run: |
|
|
196
|
+
git config user.name "$SUBMITTED_BY"
|
|
197
|
+
git config user.email "$SUBMITTED_BY@users.noreply.github.com"
|
|
198
|
+
claude --print \
|
|
199
|
+
--model ${{ inputs.model_name }} \
|
|
200
|
+
--output-format stream-json \
|
|
201
|
+
--dangerously-skip-permissions \
|
|
202
|
+
--verbose \
|
|
203
|
+
"/erk:git-pr-push Implement issue #$ISSUE_NUMBER"
|
|
204
|
+
echo "impl_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
|
205
|
+
|
|
206
|
+
- name: Handle merge conflicts if push failed
|
|
207
|
+
id: handle_conflicts
|
|
208
|
+
if: steps.submit.outcome == 'failure'
|
|
209
|
+
env:
|
|
210
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
211
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
212
|
+
GH_TOKEN: ${{ github.token }}
|
|
213
|
+
run: |
|
|
214
|
+
erk exec rebase-with-conflict-resolution \
|
|
215
|
+
--trunk-branch "${{ steps.trunk.outputs.trunk_branch }}" \
|
|
216
|
+
--branch-name "${{ steps.find_pr.outputs.branch_name }}" \
|
|
217
|
+
--model "${{ inputs.model_name }}"
|
|
218
|
+
|
|
219
|
+
- name: Mark PR ready for review
|
|
220
|
+
if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
|
|
221
|
+
env:
|
|
222
|
+
GH_TOKEN: ${{ github.token }}
|
|
223
|
+
BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
|
|
224
|
+
run: |
|
|
225
|
+
gh pr ready "$BRANCH_NAME"
|
|
226
|
+
echo "PR marked ready for review"
|
|
227
|
+
|
|
228
|
+
- name: Update PR body with implementation summary
|
|
229
|
+
continue-on-error: true
|
|
230
|
+
if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
|
|
231
|
+
env:
|
|
232
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
233
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
234
|
+
GH_TOKEN: ${{ github.token }}
|
|
235
|
+
run: |
|
|
236
|
+
erk exec ci-update-pr-body \
|
|
237
|
+
--issue-number "${{ inputs.issue_number }}" \
|
|
238
|
+
--run-id "${{ github.run_id }}" \
|
|
239
|
+
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
240
|
+
|
|
241
|
+
- name: Trigger CI workflows
|
|
242
|
+
if: steps.implement.outputs.implementation_success == 'true' && (steps.submit.outcome == 'success' || steps.handle_conflicts.outcome == 'success')
|
|
243
|
+
env:
|
|
244
|
+
BRANCH_NAME: ${{ steps.find_pr.outputs.branch_name }}
|
|
245
|
+
SUBMITTED_BY: ${{ inputs.submitted_by }}
|
|
246
|
+
run: |
|
|
247
|
+
git config user.name "$SUBMITTED_BY"
|
|
248
|
+
git config user.email "$SUBMITTED_BY@users.noreply.github.com"
|
|
249
|
+
git commit --allow-empty -m "Trigger CI workflows"
|
|
250
|
+
git push origin "$BRANCH_NAME"
|
|
251
|
+
echo "CI workflows triggered via push event"
|
erk/hooks/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Hook utilities for erk CLI."""
|
erk/hooks/decorators.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Decorators for hook commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, TypeVar, cast
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from erk_shared.context.types import NoRepoSentinel
|
|
23
|
+
from erk_shared.hooks.logging import (
|
|
24
|
+
MAX_STDERR_BYTES,
|
|
25
|
+
MAX_STDIN_BYTES,
|
|
26
|
+
MAX_STDOUT_BYTES,
|
|
27
|
+
truncate_string,
|
|
28
|
+
write_hook_log,
|
|
29
|
+
)
|
|
30
|
+
from erk_shared.hooks.types import HookExecutionLog, HookExitStatus, classify_exit_code
|
|
31
|
+
from erk_shared.scratch.scratch import get_scratch_dir
|
|
32
|
+
|
|
33
|
+
F = TypeVar("F", bound=Callable[..., None])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class HookContext:
|
|
38
|
+
"""Context injected into hooks by the @logged_hook decorator.
|
|
39
|
+
|
|
40
|
+
This dataclass consolidates common derived values that hooks need,
|
|
41
|
+
eliminating duplicated code across hook implementations.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
session_id: Claude session ID from stdin JSON, or None if not available.
|
|
45
|
+
repo_root: Path to the git repository root.
|
|
46
|
+
scratch_dir: Session-scoped scratch directory, or None if no session_id.
|
|
47
|
+
is_erk_project: True if repo_root/.erk directory exists.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
session_id: str | None
|
|
51
|
+
repo_root: Path
|
|
52
|
+
scratch_dir: Path | None
|
|
53
|
+
is_erk_project: bool
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_stdin_once() -> str:
|
|
57
|
+
"""Read stdin if available, returning empty string if not.
|
|
58
|
+
|
|
59
|
+
This is a one-time read - stdin cannot be read again after this.
|
|
60
|
+
"""
|
|
61
|
+
if sys.stdin.isatty():
|
|
62
|
+
return ""
|
|
63
|
+
return sys.stdin.read()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_session_id(stdin_data: str) -> str | None:
|
|
67
|
+
"""Extract session_id from stdin JSON if present.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
stdin_data: Raw stdin content
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
session_id if found in JSON, None otherwise
|
|
74
|
+
"""
|
|
75
|
+
if not stdin_data.strip():
|
|
76
|
+
return None
|
|
77
|
+
data = json.loads(stdin_data)
|
|
78
|
+
return data.get("session_id")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_hook_context(
|
|
82
|
+
session_id: str | None,
|
|
83
|
+
repo_root: Path,
|
|
84
|
+
) -> HookContext:
|
|
85
|
+
"""Build HookContext with derived values.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
session_id: Claude session ID from stdin JSON, or None.
|
|
89
|
+
repo_root: Path to the git repository root.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
HookContext with all derived values computed.
|
|
93
|
+
"""
|
|
94
|
+
is_erk_project = (repo_root / ".erk").is_dir()
|
|
95
|
+
|
|
96
|
+
scratch_dir: Path | None = None
|
|
97
|
+
if session_id is not None:
|
|
98
|
+
scratch_dir = get_scratch_dir(session_id, repo_root=repo_root)
|
|
99
|
+
|
|
100
|
+
return HookContext(
|
|
101
|
+
session_id=session_id,
|
|
102
|
+
repo_root=repo_root,
|
|
103
|
+
scratch_dir=scratch_dir,
|
|
104
|
+
is_erk_project=is_erk_project,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _extract_repo_root_from_click_context(args: tuple) -> Path | None:
|
|
109
|
+
"""Extract repo_root from Click context if available.
|
|
110
|
+
|
|
111
|
+
Looks for ctx.obj with a repo attribute that has a root path.
|
|
112
|
+
Handles NoRepoSentinel by returning None.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
args: Positional arguments passed to the wrapped function.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Path to repo root if found, None otherwise.
|
|
119
|
+
"""
|
|
120
|
+
# First arg should be Click context if @click.pass_context was used
|
|
121
|
+
if not args:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
ctx = args[0]
|
|
125
|
+
|
|
126
|
+
# Check if it's a Click context with our ErkContext in obj
|
|
127
|
+
if not hasattr(ctx, "obj"):
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
obj = ctx.obj
|
|
131
|
+
if obj is None:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
if not hasattr(obj, "repo"):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
repo = obj.repo
|
|
138
|
+
if isinstance(repo, NoRepoSentinel):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
if not hasattr(repo, "root"):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
return repo.root
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _function_accepts_hook_ctx(func: Callable) -> bool:
|
|
148
|
+
"""Check if a function signature accepts hook_ctx parameter.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
func: The function to inspect.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if function has a hook_ctx parameter.
|
|
155
|
+
"""
|
|
156
|
+
sig = inspect.signature(func)
|
|
157
|
+
return "hook_ctx" in sig.parameters
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def logged_hook(func: F) -> F:
|
|
161
|
+
"""Decorator that logs hook execution for health monitoring.
|
|
162
|
+
|
|
163
|
+
This decorator MUST be applied BEFORE @project_scoped so that logging
|
|
164
|
+
happens even when the hook exits early due to project scope.
|
|
165
|
+
|
|
166
|
+
The decorator:
|
|
167
|
+
1. Reads ERK_HOOK_ID from environment
|
|
168
|
+
2. Captures stdin (contains session_id in JSON from Claude Code)
|
|
169
|
+
3. Redirects stdout/stderr to capture output
|
|
170
|
+
4. Records timing and exit status
|
|
171
|
+
5. Writes log on exit (success or failure)
|
|
172
|
+
6. Re-emits captured output to real stdout/stderr
|
|
173
|
+
7. Injects HookContext if function signature accepts hook_ctx parameter
|
|
174
|
+
|
|
175
|
+
Environment variables:
|
|
176
|
+
ERK_HOOK_ID: Hook identifier (e.g., "session-id-injector-hook")
|
|
177
|
+
|
|
178
|
+
Usage:
|
|
179
|
+
@click.command()
|
|
180
|
+
@click.pass_context
|
|
181
|
+
@logged_hook
|
|
182
|
+
def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
183
|
+
if not hook_ctx.is_erk_project:
|
|
184
|
+
return
|
|
185
|
+
click.echo(f"Session: {hook_ctx.session_id}")
|
|
186
|
+
"""
|
|
187
|
+
# Check once at decoration time whether function accepts hook_ctx
|
|
188
|
+
accepts_hook_ctx = _function_accepts_hook_ctx(func)
|
|
189
|
+
|
|
190
|
+
@functools.wraps(func)
|
|
191
|
+
def wrapper(*args, **kwargs):
|
|
192
|
+
# Read environment variables
|
|
193
|
+
hook_id = os.environ.get("ERK_HOOK_ID", "unknown")
|
|
194
|
+
|
|
195
|
+
# Capture stdin before hook reads it
|
|
196
|
+
stdin_data = _read_stdin_once()
|
|
197
|
+
session_id: str | None = None
|
|
198
|
+
try:
|
|
199
|
+
session_id = _extract_session_id(stdin_data)
|
|
200
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
# Replace stdin with a StringIO containing the captured data
|
|
204
|
+
# so the hook can still read it
|
|
205
|
+
original_stdin = sys.stdin
|
|
206
|
+
sys.stdin = io.StringIO(stdin_data)
|
|
207
|
+
|
|
208
|
+
# Build HookContext if function accepts it and we can extract repo_root
|
|
209
|
+
if accepts_hook_ctx:
|
|
210
|
+
repo_root = _extract_repo_root_from_click_context(args)
|
|
211
|
+
if repo_root is not None:
|
|
212
|
+
hook_ctx = _build_hook_context(session_id, repo_root)
|
|
213
|
+
kwargs["hook_ctx"] = hook_ctx
|
|
214
|
+
|
|
215
|
+
# Capture stdout/stderr
|
|
216
|
+
stdout_buffer = io.StringIO()
|
|
217
|
+
stderr_buffer = io.StringIO()
|
|
218
|
+
|
|
219
|
+
# Record start time
|
|
220
|
+
started_at = datetime.now(UTC)
|
|
221
|
+
exit_code = 0
|
|
222
|
+
exit_status = HookExitStatus.SUCCESS
|
|
223
|
+
error_message: str | None = None
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
227
|
+
func(*args, **kwargs)
|
|
228
|
+
except SystemExit as e:
|
|
229
|
+
# Click raises SystemExit on exit
|
|
230
|
+
exit_code = e.code if isinstance(e.code, int) else 1
|
|
231
|
+
exit_status = classify_exit_code(exit_code)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
# Uncaught exception
|
|
234
|
+
exit_code = 1
|
|
235
|
+
exit_status = HookExitStatus.EXCEPTION
|
|
236
|
+
error_message = f"{type(e).__name__}: {e}"
|
|
237
|
+
# Write traceback to stderr buffer
|
|
238
|
+
stderr_buffer.write(traceback.format_exc())
|
|
239
|
+
finally:
|
|
240
|
+
# Restore stdin
|
|
241
|
+
sys.stdin = original_stdin
|
|
242
|
+
|
|
243
|
+
# Record end time
|
|
244
|
+
ended_at = datetime.now(UTC)
|
|
245
|
+
duration_ms = int((ended_at - started_at).total_seconds() * 1000)
|
|
246
|
+
|
|
247
|
+
# Get captured output
|
|
248
|
+
stdout_content = stdout_buffer.getvalue()
|
|
249
|
+
stderr_content = stderr_buffer.getvalue()
|
|
250
|
+
|
|
251
|
+
# Create log entry
|
|
252
|
+
log = HookExecutionLog(
|
|
253
|
+
kit_id="erk", # All hooks are now in erk
|
|
254
|
+
hook_id=hook_id,
|
|
255
|
+
session_id=session_id,
|
|
256
|
+
started_at=started_at.isoformat(),
|
|
257
|
+
ended_at=ended_at.isoformat(),
|
|
258
|
+
duration_ms=duration_ms,
|
|
259
|
+
exit_code=exit_code,
|
|
260
|
+
exit_status=exit_status,
|
|
261
|
+
stdout=truncate_string(stdout_content, MAX_STDOUT_BYTES),
|
|
262
|
+
stderr=truncate_string(stderr_content, MAX_STDERR_BYTES),
|
|
263
|
+
stdin_context=truncate_string(stdin_data, MAX_STDIN_BYTES),
|
|
264
|
+
error_message=error_message,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Write log (only if we have a session_id)
|
|
268
|
+
write_hook_log(log)
|
|
269
|
+
|
|
270
|
+
# Re-emit captured output
|
|
271
|
+
sys.stdout.write(stdout_content)
|
|
272
|
+
sys.stderr.write(stderr_content)
|
|
273
|
+
|
|
274
|
+
# Re-raise SystemExit if hook exited with non-zero
|
|
275
|
+
if exit_code != 0:
|
|
276
|
+
raise SystemExit(exit_code)
|
|
277
|
+
|
|
278
|
+
# Cast wrapper to F - functools.wraps preserves the signature semantics
|
|
279
|
+
return cast(F, wrapper)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def hook_command(name: str | None = None) -> Callable[[Callable[..., None]], click.Command]:
|
|
283
|
+
"""Combined decorator for hook commands.
|
|
284
|
+
|
|
285
|
+
This decorator combines @click.command, @click.pass_context, and @logged_hook
|
|
286
|
+
into a single decorator, reducing boilerplate in hook implementations.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
name: Optional command name. If not provided, Click will infer from function name.
|
|
290
|
+
|
|
291
|
+
Usage:
|
|
292
|
+
@hook_command(name="my-hook")
|
|
293
|
+
def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
294
|
+
if not hook_ctx.is_erk_project:
|
|
295
|
+
return
|
|
296
|
+
click.echo(f"Session: {hook_ctx.session_id}")
|
|
297
|
+
|
|
298
|
+
Equivalent to:
|
|
299
|
+
@click.command(name="my-hook")
|
|
300
|
+
@click.pass_context
|
|
301
|
+
@logged_hook
|
|
302
|
+
def my_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
303
|
+
...
|
|
304
|
+
"""
|
|
305
|
+
# Inline import to avoid circular dependency at module load time
|
|
306
|
+
import click
|
|
307
|
+
|
|
308
|
+
def decorator(func: Callable[..., None]) -> click.Command:
|
|
309
|
+
# Apply decorators in reverse order (innermost first)
|
|
310
|
+
# 1. @logged_hook (innermost - applied first)
|
|
311
|
+
wrapped = logged_hook(func)
|
|
312
|
+
# 2. @click.pass_context
|
|
313
|
+
wrapped = click.pass_context(wrapped)
|
|
314
|
+
# 3. @click.command (outermost - applied last)
|
|
315
|
+
if name is not None:
|
|
316
|
+
return click.command(name=name)(wrapped)
|
|
317
|
+
return click.command()(wrapped)
|
|
318
|
+
|
|
319
|
+
return decorator
|
erk/status/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Base class for status information collectors."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StatusCollector(ABC):
|
|
11
|
+
"""Base class for status information collectors.
|
|
12
|
+
|
|
13
|
+
Each collector is responsible for gathering a specific type of status
|
|
14
|
+
information (git, PR, dependencies, etc.) and returning it in a structured
|
|
15
|
+
format.
|
|
16
|
+
|
|
17
|
+
Collectors should handle their own errors gracefully and return None
|
|
18
|
+
if information cannot be collected.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
"""Name identifier for this collector."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def is_available(self, ctx: ErkContext, worktree_path: Path) -> bool:
|
|
29
|
+
"""Check if this collector can run in the given worktree.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ctx: Erk context with operations
|
|
33
|
+
worktree_path: Path to the worktree
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if collector can gather information, False otherwise
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def collect(self, ctx: ErkContext, worktree_path: Path, repo_root: Path) -> Any:
|
|
42
|
+
"""Collect status information from worktree.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
ctx: Erk context with operations
|
|
46
|
+
worktree_path: Path to the worktree
|
|
47
|
+
repo_root: Path to repository root
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Collected status data or None if collection fails
|
|
51
|
+
"""
|
|
52
|
+
...
|