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,95 @@
|
|
|
1
|
+
"""Extract arbitrary metadata fields from a plan issue's plan-header block.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec get-plan-metadata <issue-number> <field-name>
|
|
5
|
+
|
|
6
|
+
Output:
|
|
7
|
+
JSON with success status and field value (or null if field doesn't exist)
|
|
8
|
+
|
|
9
|
+
Exit Codes:
|
|
10
|
+
0: Success (field found or null)
|
|
11
|
+
1: Error (issue not found)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from erk_shared.context.helpers import require_issues as require_github_issues
|
|
21
|
+
from erk_shared.context.helpers import require_repo_root
|
|
22
|
+
from erk_shared.github.metadata.core import find_metadata_block
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class MetadataSuccess:
|
|
27
|
+
"""Success response for metadata extraction."""
|
|
28
|
+
|
|
29
|
+
success: bool
|
|
30
|
+
value: Any
|
|
31
|
+
issue_number: int
|
|
32
|
+
field: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class MetadataError:
|
|
37
|
+
"""Error response for metadata extraction."""
|
|
38
|
+
|
|
39
|
+
success: bool
|
|
40
|
+
error: str
|
|
41
|
+
message: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.command(name="get-plan-metadata")
|
|
45
|
+
@click.argument("issue_number", type=int)
|
|
46
|
+
@click.argument("field_name")
|
|
47
|
+
@click.pass_context
|
|
48
|
+
def get_plan_metadata(
|
|
49
|
+
ctx: click.Context,
|
|
50
|
+
issue_number: int,
|
|
51
|
+
field_name: str,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Extract a metadata field from a plan issue's plan-header block.
|
|
54
|
+
|
|
55
|
+
Fetches the issue, extracts the plan-header block, and returns the
|
|
56
|
+
specified field value. Returns null if the field doesn't exist.
|
|
57
|
+
"""
|
|
58
|
+
github_issues = require_github_issues(ctx)
|
|
59
|
+
repo_root = require_repo_root(ctx)
|
|
60
|
+
|
|
61
|
+
# Fetch current issue
|
|
62
|
+
try:
|
|
63
|
+
issue = github_issues.get_issue(repo_root, issue_number)
|
|
64
|
+
except RuntimeError as e:
|
|
65
|
+
result = MetadataError(
|
|
66
|
+
success=False,
|
|
67
|
+
error="issue_not_found",
|
|
68
|
+
message=f"Issue #{issue_number} not found: {e}",
|
|
69
|
+
)
|
|
70
|
+
click.echo(json.dumps(asdict(result)), err=True)
|
|
71
|
+
raise SystemExit(1) from None
|
|
72
|
+
|
|
73
|
+
# Extract plan-header block
|
|
74
|
+
block = find_metadata_block(issue.body, "plan-header")
|
|
75
|
+
if block is None:
|
|
76
|
+
# No plan-header block - return null for the field
|
|
77
|
+
result_success = MetadataSuccess(
|
|
78
|
+
success=True,
|
|
79
|
+
value=None,
|
|
80
|
+
issue_number=issue_number,
|
|
81
|
+
field=field_name,
|
|
82
|
+
)
|
|
83
|
+
click.echo(json.dumps(asdict(result_success)))
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Get field value (None if field doesn't exist)
|
|
87
|
+
field_value = block.data.get(field_name)
|
|
88
|
+
|
|
89
|
+
result_success = MetadataSuccess(
|
|
90
|
+
success=True,
|
|
91
|
+
value=field_value,
|
|
92
|
+
issue_number=issue_number,
|
|
93
|
+
field=field_name,
|
|
94
|
+
)
|
|
95
|
+
click.echo(json.dumps(asdict(result_success)))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Generate PR body footer for remote implementation PRs.
|
|
2
|
+
|
|
3
|
+
This exec command generates a footer section for PR descriptions that includes
|
|
4
|
+
the `erk pr checkout` command. This is used by the GitHub Actions workflow when
|
|
5
|
+
creating PRs from remote implementations.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
erk exec get-pr-body-footer --pr-number 123
|
|
9
|
+
erk exec get-pr-body-footer --pr-number 123 --issue-number 456
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
Markdown footer with checkout command and optional issue closing reference
|
|
13
|
+
|
|
14
|
+
Exit Codes:
|
|
15
|
+
0: Success
|
|
16
|
+
1: Error (missing pr-number)
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
$ erk exec get-pr-body-footer --pr-number 1895
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
To checkout this PR in a fresh worktree and environment locally, run:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
erk pr checkout 1895 && erk pr sync --dangerous
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
$ erk exec get-pr-body-footer --pr-number 1895 --issue-number 123
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Closes #123
|
|
34
|
+
|
|
35
|
+
To checkout this PR in a fresh worktree and environment locally, run:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
erk pr checkout 1895 && erk pr sync --dangerous
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import click
|
|
43
|
+
|
|
44
|
+
from erk_shared.github.pr_footer import build_pr_body_footer
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.command(name="get-pr-body-footer")
|
|
48
|
+
@click.option("--pr-number", type=int, required=True, help="PR number for checkout command")
|
|
49
|
+
@click.option("--issue-number", type=int, required=False, help="Issue number to close")
|
|
50
|
+
@click.option(
|
|
51
|
+
"--plans-repo", type=str, required=False, help="Target repo in owner/repo format (cross-repo)"
|
|
52
|
+
)
|
|
53
|
+
def get_pr_body_footer(pr_number: int, issue_number: int | None, plans_repo: str | None) -> None:
|
|
54
|
+
"""Generate PR body footer with checkout command.
|
|
55
|
+
|
|
56
|
+
Outputs a markdown footer section that includes the `erk pr checkout` command,
|
|
57
|
+
allowing users to easily checkout the PR in a fresh worktree locally.
|
|
58
|
+
|
|
59
|
+
When issue_number is provided, includes "Closes #N" (or "Closes owner/repo#N"
|
|
60
|
+
for cross-repo plans) to auto-close the issue when the PR is merged.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
pr_number: The PR number to include in the checkout command
|
|
64
|
+
issue_number: Optional issue number to close when PR is merged
|
|
65
|
+
plans_repo: Optional target repo in "owner/repo" format for cross-repo
|
|
66
|
+
"""
|
|
67
|
+
output = build_pr_body_footer(
|
|
68
|
+
pr_number=pr_number, issue_number=issue_number, plans_repo=plans_repo
|
|
69
|
+
)
|
|
70
|
+
click.echo(output, nl=False)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Fetch PR discussion comments (main conversation thread) for agent context injection.
|
|
2
|
+
|
|
3
|
+
This exec command fetches discussion comments from the PR's main conversation
|
|
4
|
+
(not inline code review comments) and outputs them as JSON for agent processing.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
erk exec get-pr-discussion-comments
|
|
8
|
+
erk exec get-pr-discussion-comments --pr 123
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
JSON with success status, PR info, and discussion comments
|
|
12
|
+
|
|
13
|
+
Exit Codes:
|
|
14
|
+
0: Success (or graceful error with JSON output)
|
|
15
|
+
1: Context not initialized
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
$ erk exec get-pr-discussion-comments
|
|
19
|
+
{"success": true, "pr_number": 123, "comments": [...]}
|
|
20
|
+
|
|
21
|
+
$ erk exec get-pr-discussion-comments --pr 456
|
|
22
|
+
{"success": true, "pr_number": 456, "comments": [...]}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from typing import TypedDict
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from erk.cli.script_output import exit_with_error
|
|
31
|
+
from erk_shared.context.helpers import (
|
|
32
|
+
get_current_branch,
|
|
33
|
+
require_github,
|
|
34
|
+
require_repo_root,
|
|
35
|
+
)
|
|
36
|
+
from erk_shared.context.helpers import (
|
|
37
|
+
require_issues as require_github_issues,
|
|
38
|
+
)
|
|
39
|
+
from erk_shared.github.checks import GitHubChecks
|
|
40
|
+
from erk_shared.github.issues.types import IssueComment
|
|
41
|
+
from erk_shared.github.types import PRDetails
|
|
42
|
+
from erk_shared.non_ideal_state import (
|
|
43
|
+
BranchDetectionFailed,
|
|
44
|
+
GitHubAPIFailed,
|
|
45
|
+
NoPRForBranch,
|
|
46
|
+
PRNotFoundError,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ensure_branch(branch_result: str | BranchDetectionFailed) -> str:
|
|
51
|
+
"""Ensure branch was detected, exit with error if not."""
|
|
52
|
+
if isinstance(branch_result, BranchDetectionFailed):
|
|
53
|
+
exit_with_error(branch_result.error_type, branch_result.message)
|
|
54
|
+
assert not isinstance(branch_result, BranchDetectionFailed) # Type narrowing after NoReturn
|
|
55
|
+
return branch_result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ensure_pr_result_for_branch(
|
|
59
|
+
pr_result: PRDetails | NoPRForBranch,
|
|
60
|
+
) -> PRDetails:
|
|
61
|
+
"""Ensure PR lookup by branch succeeded, exit with appropriate error if not."""
|
|
62
|
+
if isinstance(pr_result, NoPRForBranch):
|
|
63
|
+
exit_with_error(pr_result.error_type, pr_result.message)
|
|
64
|
+
assert not isinstance(pr_result, NoPRForBranch) # Type narrowing after NoReturn
|
|
65
|
+
return pr_result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ensure_pr_result_by_number(
|
|
69
|
+
pr_result: PRDetails | PRNotFoundError,
|
|
70
|
+
) -> PRDetails:
|
|
71
|
+
"""Ensure PR lookup by number succeeded, exit with appropriate error if not."""
|
|
72
|
+
if isinstance(pr_result, PRNotFoundError):
|
|
73
|
+
exit_with_error(pr_result.error_type, pr_result.message)
|
|
74
|
+
assert not isinstance(pr_result, PRNotFoundError) # Type narrowing after NoReturn
|
|
75
|
+
return pr_result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ensure_comments(
|
|
79
|
+
comments_result: list[IssueComment] | GitHubAPIFailed,
|
|
80
|
+
) -> list[IssueComment]:
|
|
81
|
+
"""Ensure comments fetch succeeded, exit with error if not."""
|
|
82
|
+
if isinstance(comments_result, GitHubAPIFailed):
|
|
83
|
+
exit_with_error(comments_result.error_type, comments_result.message)
|
|
84
|
+
assert not isinstance(comments_result, GitHubAPIFailed) # Type narrowing after NoReturn
|
|
85
|
+
return comments_result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DiscussionCommentDict(TypedDict):
|
|
89
|
+
"""Typed dict for a single discussion comment in JSON output."""
|
|
90
|
+
|
|
91
|
+
id: int
|
|
92
|
+
author: str
|
|
93
|
+
body: str
|
|
94
|
+
url: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.command(name="get-pr-discussion-comments")
|
|
98
|
+
@click.option("--pr", type=int, default=None, help="PR number (defaults to current branch's PR)")
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def get_pr_discussion_comments(ctx: click.Context, pr: int | None) -> None:
|
|
101
|
+
"""Fetch PR discussion comments for agent context injection.
|
|
102
|
+
|
|
103
|
+
Queries GitHub for discussion comments on a pull request's main
|
|
104
|
+
conversation thread (not inline code review comments) and outputs
|
|
105
|
+
structured JSON for agent processing.
|
|
106
|
+
|
|
107
|
+
If --pr is not specified, finds the PR for the current branch.
|
|
108
|
+
"""
|
|
109
|
+
# Get dependencies from context
|
|
110
|
+
repo_root = require_repo_root(ctx)
|
|
111
|
+
github = require_github(ctx)
|
|
112
|
+
github_issues = require_github_issues(ctx)
|
|
113
|
+
|
|
114
|
+
# Get PR details - either from current branch or specified PR number
|
|
115
|
+
if pr is None:
|
|
116
|
+
branch = _ensure_branch(GitHubChecks.branch(get_current_branch(ctx)))
|
|
117
|
+
pr_details = _ensure_pr_result_for_branch(
|
|
118
|
+
GitHubChecks.pr_for_branch(github, repo_root, branch)
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
pr_details = _ensure_pr_result_by_number(GitHubChecks.pr_by_number(github, repo_root, pr))
|
|
122
|
+
|
|
123
|
+
# Fetch discussion comments (exits on failure)
|
|
124
|
+
comments = _ensure_comments(
|
|
125
|
+
GitHubChecks.issue_comments(github_issues, repo_root, pr_details.number)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Format comments for JSON output
|
|
129
|
+
formatted_comments: list[DiscussionCommentDict] = []
|
|
130
|
+
for comment in comments:
|
|
131
|
+
assert isinstance(comment, IssueComment) # Runtime verification for type safety
|
|
132
|
+
formatted_comments.append(
|
|
133
|
+
{
|
|
134
|
+
"id": comment.id,
|
|
135
|
+
"author": comment.author,
|
|
136
|
+
"body": comment.body,
|
|
137
|
+
"url": comment.url,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = {
|
|
142
|
+
"success": True,
|
|
143
|
+
"pr_number": pr_details.number,
|
|
144
|
+
"pr_url": pr_details.url,
|
|
145
|
+
"pr_title": pr_details.title,
|
|
146
|
+
"comments": formatted_comments,
|
|
147
|
+
}
|
|
148
|
+
click.echo(json.dumps(result, indent=2))
|
|
149
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Fetch PR review comments for agent context injection.
|
|
2
|
+
|
|
3
|
+
This exec command fetches unresolved (or all) PR review comments from GitHub
|
|
4
|
+
and outputs them as JSON for agent processing.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
erk exec get-pr-review-comments
|
|
8
|
+
erk exec get-pr-review-comments --pr 123
|
|
9
|
+
erk exec get-pr-review-comments --include-resolved
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
JSON with success status, PR info, and review threads
|
|
13
|
+
|
|
14
|
+
Exit Codes:
|
|
15
|
+
0: Success (or graceful error with JSON output)
|
|
16
|
+
1: Context not initialized
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
$ erk exec get-pr-review-comments
|
|
20
|
+
{"success": true, "pr_number": 123, "threads": [...]}
|
|
21
|
+
|
|
22
|
+
$ erk exec get-pr-review-comments --pr 456
|
|
23
|
+
{"success": true, "pr_number": 456, "threads": [...]}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
from dataclasses import asdict, dataclass
|
|
30
|
+
from typing import TypedDict
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
|
|
34
|
+
from erk.cli.script_output import exit_with_error
|
|
35
|
+
from erk_shared.context.helpers import get_current_branch, require_github, require_repo_root
|
|
36
|
+
from erk_shared.github.types import PRDetails, PRNotFound, PRReviewThread
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReviewCommentDict(TypedDict):
|
|
40
|
+
"""Typed dict for a single review comment in JSON output."""
|
|
41
|
+
|
|
42
|
+
author: str
|
|
43
|
+
body: str
|
|
44
|
+
created_at: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReviewThreadDict(TypedDict):
|
|
48
|
+
"""Typed dict for a review thread in JSON output."""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
path: str
|
|
52
|
+
line: int | None
|
|
53
|
+
is_outdated: bool
|
|
54
|
+
comments: list[ReviewCommentDict]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ReviewCommentSuccess:
|
|
59
|
+
"""Success response for PR review comments."""
|
|
60
|
+
|
|
61
|
+
success: bool
|
|
62
|
+
pr_number: int
|
|
63
|
+
pr_url: str
|
|
64
|
+
pr_title: str
|
|
65
|
+
threads: list[ReviewThreadDict]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ensure_branch(branch: str | None) -> str:
|
|
69
|
+
"""Ensure branch was detected, exit with error if not."""
|
|
70
|
+
if branch is None:
|
|
71
|
+
exit_with_error("branch-detection-failed", "Could not determine current branch")
|
|
72
|
+
assert branch is not None # Type narrowing after NoReturn
|
|
73
|
+
return branch
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_pr_result(
|
|
77
|
+
pr_result: PRDetails | PRNotFound,
|
|
78
|
+
*,
|
|
79
|
+
branch: str | None,
|
|
80
|
+
pr_number: int | None,
|
|
81
|
+
) -> PRDetails:
|
|
82
|
+
"""Ensure PR lookup succeeded, exit with appropriate error if not."""
|
|
83
|
+
if isinstance(pr_result, PRNotFound):
|
|
84
|
+
if branch is not None:
|
|
85
|
+
exit_with_error("no-pr-for-branch", f"No PR found for branch '{branch}'")
|
|
86
|
+
else:
|
|
87
|
+
exit_with_error("pr-not-found", f"PR #{pr_number} not found")
|
|
88
|
+
assert not isinstance(pr_result, PRNotFound) # Type narrowing after NoReturn
|
|
89
|
+
return pr_result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _format_thread_for_json(thread: PRReviewThread) -> ReviewThreadDict:
|
|
93
|
+
"""Format a PRReviewThread for JSON output."""
|
|
94
|
+
comments: list[ReviewCommentDict] = []
|
|
95
|
+
for comment in thread.comments:
|
|
96
|
+
comments.append(
|
|
97
|
+
{
|
|
98
|
+
"author": comment.author,
|
|
99
|
+
"body": comment.body,
|
|
100
|
+
"created_at": comment.created_at,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"id": thread.id,
|
|
106
|
+
"path": thread.path,
|
|
107
|
+
"line": thread.line,
|
|
108
|
+
"is_outdated": thread.is_outdated,
|
|
109
|
+
"comments": comments,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@click.command(name="get-pr-review-comments")
|
|
114
|
+
@click.option("--pr", type=int, default=None, help="PR number (defaults to current branch's PR)")
|
|
115
|
+
@click.option("--include-resolved", is_flag=True, help="Include resolved threads")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def get_pr_review_comments(ctx: click.Context, pr: int | None, include_resolved: bool) -> None:
|
|
118
|
+
"""Fetch PR review comments for agent context injection.
|
|
119
|
+
|
|
120
|
+
Queries GitHub for review threads on a pull request and outputs
|
|
121
|
+
structured JSON for agent processing. By default, excludes resolved
|
|
122
|
+
threads.
|
|
123
|
+
|
|
124
|
+
If --pr is not specified, finds the PR for the current branch.
|
|
125
|
+
"""
|
|
126
|
+
# Get dependencies from context
|
|
127
|
+
repo_root = require_repo_root(ctx)
|
|
128
|
+
github = require_github(ctx)
|
|
129
|
+
|
|
130
|
+
# Get PR details - either from current branch or specified PR number
|
|
131
|
+
if pr is None:
|
|
132
|
+
branch = _ensure_branch(get_current_branch(ctx))
|
|
133
|
+
pr_result = _ensure_pr_result(
|
|
134
|
+
github.get_pr_for_branch(repo_root, branch), branch=branch, pr_number=None
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
pr_result = _ensure_pr_result(github.get_pr(repo_root, pr), branch=None, pr_number=pr)
|
|
138
|
+
|
|
139
|
+
# Fetch review threads
|
|
140
|
+
try:
|
|
141
|
+
threads = github.get_pr_review_threads(
|
|
142
|
+
repo_root, pr_result.number, include_resolved=include_resolved
|
|
143
|
+
)
|
|
144
|
+
except RuntimeError as e:
|
|
145
|
+
exit_with_error("github-api-failed", str(e))
|
|
146
|
+
|
|
147
|
+
result_success = ReviewCommentSuccess(
|
|
148
|
+
success=True,
|
|
149
|
+
pr_number=pr_result.number,
|
|
150
|
+
pr_url=pr_result.url,
|
|
151
|
+
pr_title=pr_result.title,
|
|
152
|
+
threads=[_format_thread_for_json(t) for t in threads],
|
|
153
|
+
)
|
|
154
|
+
click.echo(json.dumps(asdict(result_success), indent=2))
|
|
155
|
+
raise SystemExit(0)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Initialize implementation by validating .impl/ folder.
|
|
2
|
+
|
|
3
|
+
This exec command validates .impl/ folder for /erk:plan-implement:
|
|
4
|
+
- Validates .impl/ folder structure (plan.md exists)
|
|
5
|
+
- Checks for GitHub issue tracking (issue.json)
|
|
6
|
+
- Parses "Related Documentation" section for skills and docs
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
erk exec impl-init --json
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
JSON with validation status and related docs
|
|
13
|
+
Always outputs JSON (for machine parsing by slash command)
|
|
14
|
+
|
|
15
|
+
Exit Codes:
|
|
16
|
+
0: Success
|
|
17
|
+
1: Validation error
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
$ erk exec impl-init --json
|
|
21
|
+
{"valid": true, "impl_type": "impl", "has_issue_tracking": true, ...}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import NoReturn
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
|
|
31
|
+
from erk_shared.impl_folder import read_issue_reference
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _error_json(error_type: str, message: str) -> NoReturn:
|
|
35
|
+
"""Output error as JSON and exit with code 1."""
|
|
36
|
+
result = {"valid": False, "error_type": error_type, "message": message}
|
|
37
|
+
click.echo(json.dumps(result))
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_impl_folder() -> tuple[Path, str]:
|
|
42
|
+
"""Validate .impl/ or .worker-impl/ folder exists and has required files.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tuple of (impl_dir Path, impl_type string)
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
SystemExit: If validation fails
|
|
49
|
+
"""
|
|
50
|
+
cwd = Path.cwd()
|
|
51
|
+
|
|
52
|
+
# Check .impl/ first, then .worker-impl/
|
|
53
|
+
impl_dir = cwd / ".impl"
|
|
54
|
+
impl_type = "impl"
|
|
55
|
+
|
|
56
|
+
if not impl_dir.exists():
|
|
57
|
+
impl_dir = cwd / ".worker-impl"
|
|
58
|
+
impl_type = "worker-impl"
|
|
59
|
+
|
|
60
|
+
if not impl_dir.exists():
|
|
61
|
+
_error_json(
|
|
62
|
+
"no_impl_folder",
|
|
63
|
+
"No .impl/ or .worker-impl/ folder found in current directory",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
plan_file = impl_dir / "plan.md"
|
|
67
|
+
if not plan_file.exists():
|
|
68
|
+
_error_json("no_plan_file", f"No plan.md found in {impl_dir.name}/ folder")
|
|
69
|
+
|
|
70
|
+
return impl_dir, impl_type
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_related_docs(plan_content: str) -> dict[str, list[str]]:
|
|
74
|
+
"""Extract Related Documentation section from plan content.
|
|
75
|
+
|
|
76
|
+
Parses markdown like:
|
|
77
|
+
## Related Documentation
|
|
78
|
+
|
|
79
|
+
**Skills:**
|
|
80
|
+
- `dignified-python-313`
|
|
81
|
+
|
|
82
|
+
**Docs:**
|
|
83
|
+
- [Kit CLI Testing](docs/agent/testing/kit-cli-testing.md)
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
plan_content: Full plan markdown content
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict with 'skills' and 'docs' lists
|
|
90
|
+
"""
|
|
91
|
+
result: dict[str, list[str]] = {"skills": [], "docs": []}
|
|
92
|
+
|
|
93
|
+
# Find Related Documentation section
|
|
94
|
+
related_docs_pattern = re.compile(
|
|
95
|
+
r"##\s+Related Documentation\s*\n(.*?)(?=\n##|\Z)",
|
|
96
|
+
re.DOTALL | re.IGNORECASE,
|
|
97
|
+
)
|
|
98
|
+
match = related_docs_pattern.search(plan_content)
|
|
99
|
+
|
|
100
|
+
if match is None:
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
section = match.group(1)
|
|
104
|
+
|
|
105
|
+
# Extract skills (backtick-enclosed names after bullet points)
|
|
106
|
+
skills_section = re.search(r"\*\*Skills:\*\*\s*\n(.*?)(?=\*\*|\Z)", section, re.DOTALL)
|
|
107
|
+
if skills_section:
|
|
108
|
+
skill_pattern = re.compile(r"-\s*`([^`]+)`")
|
|
109
|
+
result["skills"] = skill_pattern.findall(skills_section.group(1))
|
|
110
|
+
|
|
111
|
+
# Extract docs (markdown links or plain paths)
|
|
112
|
+
docs_section = re.search(r"\*\*Docs:\*\*\s*\n(.*?)(?=\*\*|\Z)", section, re.DOTALL)
|
|
113
|
+
if docs_section:
|
|
114
|
+
# Match markdown links [text](path) or backtick paths `path`
|
|
115
|
+
link_pattern = re.compile(r"-\s*(?:\[[^\]]*\]\(([^)]+)\)|`([^`]+)`)")
|
|
116
|
+
for m in link_pattern.finditer(docs_section.group(1)):
|
|
117
|
+
doc_path = m.group(1) or m.group(2)
|
|
118
|
+
if doc_path:
|
|
119
|
+
result["docs"].append(doc_path)
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@click.command(name="impl-init")
|
|
125
|
+
@click.option("--json", "json_output", is_flag=True, default=True, help="Output JSON (default)")
|
|
126
|
+
def impl_init(json_output: bool) -> None:
|
|
127
|
+
"""Initialize implementation by validating .impl/ folder.
|
|
128
|
+
|
|
129
|
+
Validates .impl/ folder for /erk:plan-implement.
|
|
130
|
+
Returns structured JSON with validation status and related documentation.
|
|
131
|
+
"""
|
|
132
|
+
# Validate folder structure
|
|
133
|
+
impl_dir, impl_type = _validate_impl_folder()
|
|
134
|
+
|
|
135
|
+
# Get issue reference info
|
|
136
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
137
|
+
has_issue_tracking = issue_ref is not None
|
|
138
|
+
issue_number = issue_ref.issue_number if issue_ref else None
|
|
139
|
+
|
|
140
|
+
# Read plan content
|
|
141
|
+
plan_file = impl_dir / "plan.md"
|
|
142
|
+
plan_content = plan_file.read_text(encoding="utf-8")
|
|
143
|
+
|
|
144
|
+
# Extract related documentation
|
|
145
|
+
related_docs = _extract_related_docs(plan_content)
|
|
146
|
+
|
|
147
|
+
# Build result
|
|
148
|
+
result: dict = {
|
|
149
|
+
"valid": True,
|
|
150
|
+
"impl_type": impl_type,
|
|
151
|
+
"has_issue_tracking": has_issue_tracking,
|
|
152
|
+
"related_docs": related_docs,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if issue_number is not None:
|
|
156
|
+
result["issue_number"] = issue_number
|
|
157
|
+
|
|
158
|
+
click.echo(json.dumps(result))
|