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,174 @@
|
|
|
1
|
+
"""Command to validate plan format against Schema v2 requirements."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
10
|
+
from erk.core.context import ErkContext
|
|
11
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
12
|
+
from erk_shared.github.issues.abc import GitHubIssues
|
|
13
|
+
from erk_shared.github.metadata.core import find_metadata_block
|
|
14
|
+
from erk_shared.github.metadata.plan_header import extract_plan_from_comment
|
|
15
|
+
from erk_shared.github.metadata.schemas import PlanHeaderSchema
|
|
16
|
+
from erk_shared.output.output import user_output
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class PlanValidationSuccess:
|
|
21
|
+
"""Validation completed (may have passed or failed checks).
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
passed: True if all validation checks passed
|
|
25
|
+
checks: List of (passed, description) tuples for each check
|
|
26
|
+
failed_count: Number of failed checks
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
passed: bool
|
|
30
|
+
checks: list[tuple[bool, str]]
|
|
31
|
+
failed_count: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class PlanValidationError:
|
|
36
|
+
"""Could not complete validation (API error, network issue, etc.)."""
|
|
37
|
+
|
|
38
|
+
error: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
PlanValidationResult = PlanValidationSuccess | PlanValidationError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_plan_format(
|
|
45
|
+
github_issues: GitHubIssues,
|
|
46
|
+
repo_root: Path,
|
|
47
|
+
issue_number: int,
|
|
48
|
+
) -> PlanValidationResult:
|
|
49
|
+
"""Validate plan format programmatically.
|
|
50
|
+
|
|
51
|
+
Validates that a plan stored in a GitHub issue conforms to Schema v2:
|
|
52
|
+
- Issue body has plan-header metadata block with required fields
|
|
53
|
+
- First comment has plan-body metadata block with extractable content
|
|
54
|
+
|
|
55
|
+
This function is designed to be called programmatically (e.g., from land_cmd).
|
|
56
|
+
It does not produce output or raise SystemExit. It never raises exceptions -
|
|
57
|
+
API failures are returned as PlanValidationError.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
github_issues: GitHub issues interface
|
|
61
|
+
repo_root: Repository root path
|
|
62
|
+
issue_number: GitHub issue number to validate
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
PlanValidationSuccess if validation completed (may have passed or failed checks)
|
|
66
|
+
PlanValidationError if unable to complete validation (API error, etc.)
|
|
67
|
+
"""
|
|
68
|
+
# Track validation results
|
|
69
|
+
checks: list[tuple[bool, str]] = []
|
|
70
|
+
|
|
71
|
+
# Fetch issue from GitHub
|
|
72
|
+
try:
|
|
73
|
+
issue = github_issues.get_issue(repo_root, issue_number)
|
|
74
|
+
except RuntimeError as e:
|
|
75
|
+
return PlanValidationError(error=str(e))
|
|
76
|
+
|
|
77
|
+
issue_body = issue.body if issue.body else ""
|
|
78
|
+
|
|
79
|
+
# Check 1: plan-header metadata block exists
|
|
80
|
+
plan_header_block = find_metadata_block(issue_body, "plan-header")
|
|
81
|
+
if plan_header_block is None:
|
|
82
|
+
checks.append((False, "plan-header metadata block present"))
|
|
83
|
+
else:
|
|
84
|
+
checks.append((True, "plan-header metadata block present"))
|
|
85
|
+
|
|
86
|
+
# Check 2: plan-header has required fields and is valid
|
|
87
|
+
try:
|
|
88
|
+
schema = PlanHeaderSchema()
|
|
89
|
+
schema.validate(plan_header_block.data)
|
|
90
|
+
checks.append((True, "plan-header has required fields"))
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
# Extract first error message for cleaner output
|
|
93
|
+
error_msg = str(e).split("\n")[0]
|
|
94
|
+
checks.append((False, f"plan-header validation failed: {error_msg}"))
|
|
95
|
+
|
|
96
|
+
# Check 3: First comment exists
|
|
97
|
+
try:
|
|
98
|
+
comments = github_issues.get_issue_comments(repo_root, issue_number)
|
|
99
|
+
except RuntimeError as e:
|
|
100
|
+
return PlanValidationError(error=str(e))
|
|
101
|
+
|
|
102
|
+
if not comments:
|
|
103
|
+
checks.append((False, "First comment exists"))
|
|
104
|
+
else:
|
|
105
|
+
checks.append((True, "First comment exists"))
|
|
106
|
+
|
|
107
|
+
# Check 4: plan-body content extractable
|
|
108
|
+
first_comment = comments[0]
|
|
109
|
+
plan_content = extract_plan_from_comment(first_comment)
|
|
110
|
+
if plan_content is None:
|
|
111
|
+
checks.append((False, "plan-body content extractable"))
|
|
112
|
+
else:
|
|
113
|
+
checks.append((True, "plan-body content extractable"))
|
|
114
|
+
|
|
115
|
+
# Determine overall result
|
|
116
|
+
failed_count = sum(1 for passed, _ in checks if not passed)
|
|
117
|
+
|
|
118
|
+
return PlanValidationSuccess(
|
|
119
|
+
passed=failed_count == 0,
|
|
120
|
+
checks=checks,
|
|
121
|
+
failed_count=failed_count,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@click.command("check")
|
|
126
|
+
@click.argument("identifier", type=str)
|
|
127
|
+
@click.pass_obj
|
|
128
|
+
def check_plan(ctx: ErkContext, identifier: str) -> None:
|
|
129
|
+
"""Validate a plan's format against Schema v2 requirements.
|
|
130
|
+
|
|
131
|
+
Validates that a plan stored in a GitHub issue conforms to Schema v2:
|
|
132
|
+
- Issue body has plan-header metadata block with required fields
|
|
133
|
+
- First comment has plan-body metadata block with extractable content
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
identifier: Plan identifier (e.g., "42" or GitHub URL)
|
|
137
|
+
"""
|
|
138
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
139
|
+
ensure_erk_metadata_dir(repo) # Ensure erk metadata directories exist
|
|
140
|
+
repo_root = repo.root # Use git repository root for GitHub operations
|
|
141
|
+
|
|
142
|
+
# Parse identifier - raises click.ClickException if invalid
|
|
143
|
+
issue_number = parse_issue_identifier(identifier)
|
|
144
|
+
|
|
145
|
+
user_output(f"Validating plan #{issue_number}...")
|
|
146
|
+
user_output("")
|
|
147
|
+
|
|
148
|
+
# Run validation
|
|
149
|
+
result = validate_plan_format(ctx.issues, repo_root, issue_number)
|
|
150
|
+
|
|
151
|
+
if isinstance(result, PlanValidationError):
|
|
152
|
+
user_output(click.style("Error: ", fg="red") + f"Failed to validate plan: {result.error}")
|
|
153
|
+
raise SystemExit(1)
|
|
154
|
+
|
|
155
|
+
# result is now PlanValidationSuccess
|
|
156
|
+
# Output results
|
|
157
|
+
for passed, description in result.checks:
|
|
158
|
+
status = click.style("[PASS]", fg="green") if passed else click.style("[FAIL]", fg="red")
|
|
159
|
+
user_output(f"{status} {description}")
|
|
160
|
+
|
|
161
|
+
user_output("")
|
|
162
|
+
|
|
163
|
+
# Determine overall result
|
|
164
|
+
if result.passed:
|
|
165
|
+
user_output(click.style("Plan validation passed", fg="green"))
|
|
166
|
+
raise SystemExit(0)
|
|
167
|
+
else:
|
|
168
|
+
check_word = "checks" if result.failed_count > 1 else "check"
|
|
169
|
+
user_output(
|
|
170
|
+
click.style(
|
|
171
|
+
f"Plan validation failed ({result.failed_count} {check_word} failed)", fg="red"
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Command to close a plan."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.core import discover_repo_context
|
|
8
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
11
|
+
from erk_shared.output.output import user_output
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _close_linked_prs(
|
|
15
|
+
ctx: ErkContext,
|
|
16
|
+
repo_root: Path,
|
|
17
|
+
issue_number: int,
|
|
18
|
+
) -> list[int]:
|
|
19
|
+
"""Close all OPEN PRs linked to an issue.
|
|
20
|
+
|
|
21
|
+
Returns list of PR numbers that were closed.
|
|
22
|
+
"""
|
|
23
|
+
linked_prs = ctx.issues.get_prs_referencing_issue(repo_root, issue_number)
|
|
24
|
+
|
|
25
|
+
closed_prs: list[int] = []
|
|
26
|
+
for pr in linked_prs:
|
|
27
|
+
# Close all OPEN PRs (both drafts and non-drafts per user requirement)
|
|
28
|
+
if pr.state == "OPEN":
|
|
29
|
+
ctx.github.close_pr(repo_root, pr.number)
|
|
30
|
+
closed_prs.append(pr.number)
|
|
31
|
+
|
|
32
|
+
return closed_prs
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command("close")
|
|
36
|
+
@click.argument("identifier", type=str)
|
|
37
|
+
@click.pass_obj
|
|
38
|
+
def close_plan(ctx: ErkContext, identifier: str) -> None:
|
|
39
|
+
"""Close a plan by issue number or GitHub URL.
|
|
40
|
+
|
|
41
|
+
Closes all OPEN PRs linked to the issue in addition to closing the issue itself.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
identifier: Plan identifier (e.g., "42" or GitHub URL)
|
|
45
|
+
"""
|
|
46
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
47
|
+
ensure_erk_metadata_dir(repo) # Ensure erk metadata directories exist
|
|
48
|
+
repo_root = repo.root # Use git repository root for GitHub operations
|
|
49
|
+
|
|
50
|
+
# Parse issue number - errors if invalid
|
|
51
|
+
number = parse_issue_identifier(identifier)
|
|
52
|
+
|
|
53
|
+
# Fetch plan - errors if not found
|
|
54
|
+
try:
|
|
55
|
+
_plan = ctx.plan_store.get_plan(repo_root, str(number))
|
|
56
|
+
except RuntimeError as e:
|
|
57
|
+
raise click.ClickException(str(e)) from e
|
|
58
|
+
|
|
59
|
+
# Close linked PRs before closing the plan
|
|
60
|
+
closed_prs = _close_linked_prs(ctx, repo_root, number)
|
|
61
|
+
|
|
62
|
+
# Close the plan (issue)
|
|
63
|
+
ctx.plan_store.close_plan(repo_root, identifier)
|
|
64
|
+
|
|
65
|
+
# Output
|
|
66
|
+
user_output(f"Closed plan #{number}")
|
|
67
|
+
if closed_prs:
|
|
68
|
+
pr_list = ", ".join(f"#{pr}" for pr in closed_prs)
|
|
69
|
+
user_output(f"Closed {len(closed_prs)} linked PR(s): {pr_list}")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Command to create a plan issue from markdown content."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.cli.ensure import Ensure
|
|
10
|
+
from erk.core.context import ErkContext
|
|
11
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
12
|
+
from erk_shared.github.plan_issues import create_plan_issue
|
|
13
|
+
from erk_shared.output.output import user_output
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("create")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--file",
|
|
19
|
+
"-f",
|
|
20
|
+
type=click.Path(exists=True, path_type=Path),
|
|
21
|
+
help="Plan file to read",
|
|
22
|
+
)
|
|
23
|
+
@click.option("--title", "-t", type=str, help="Issue title (default: extract from H1)")
|
|
24
|
+
@click.option("--label", "-l", multiple=True, help="Additional labels")
|
|
25
|
+
@click.pass_obj
|
|
26
|
+
def create_plan(
|
|
27
|
+
ctx: ErkContext,
|
|
28
|
+
file: Path | None,
|
|
29
|
+
title: str | None,
|
|
30
|
+
label: tuple[str, ...],
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Create a plan issue from markdown content.
|
|
33
|
+
|
|
34
|
+
Supports two input modes:
|
|
35
|
+
- File: --file PATH (recommended for automation)
|
|
36
|
+
- Stdin: pipe content via shell (for Unix composability)
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
erk create --file plan.md
|
|
40
|
+
cat plan.md | erk create
|
|
41
|
+
erk create --file plan.md --title "Custom Title"
|
|
42
|
+
erk create --file plan.md --label bug --label urgent
|
|
43
|
+
"""
|
|
44
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
45
|
+
ensure_erk_metadata_dir(repo)
|
|
46
|
+
repo_root = repo.root
|
|
47
|
+
|
|
48
|
+
# LBYL: Check input sources - exactly one required
|
|
49
|
+
# Priority: --file flag takes precedence over stdin
|
|
50
|
+
content = "" # Initialize to ensure type safety
|
|
51
|
+
if file is not None:
|
|
52
|
+
# Use file input
|
|
53
|
+
Ensure.path_exists(ctx, file, f"File not found: {file}")
|
|
54
|
+
try:
|
|
55
|
+
content = file.read_text(encoding="utf-8")
|
|
56
|
+
except OSError as e:
|
|
57
|
+
user_output(click.style("Error: ", fg="red") + f"Failed to read file: {e}")
|
|
58
|
+
raise SystemExit(1) from e
|
|
59
|
+
elif not sys.stdin.isatty():
|
|
60
|
+
# Use stdin input (piped data)
|
|
61
|
+
try:
|
|
62
|
+
content = sys.stdin.read()
|
|
63
|
+
except OSError as e:
|
|
64
|
+
user_output(click.style("Error: ", fg="red") + f"Failed to read stdin: {e}")
|
|
65
|
+
raise SystemExit(1) from e
|
|
66
|
+
else:
|
|
67
|
+
# No input provided
|
|
68
|
+
Ensure.invariant(False, "No input provided. Use --file or pipe content to stdin.")
|
|
69
|
+
|
|
70
|
+
# Validate content is not empty
|
|
71
|
+
Ensure.not_empty(content.strip(), "Plan content is empty. Provide a non-empty plan.")
|
|
72
|
+
|
|
73
|
+
# Convert extra labels tuple to list
|
|
74
|
+
extra_labels = list(label) if label else None
|
|
75
|
+
|
|
76
|
+
# Determine source_repo for cross-repo plans
|
|
77
|
+
# When plans_repo is configured, plans are stored in a separate repo
|
|
78
|
+
# and source_repo records where implementation will happen
|
|
79
|
+
source_repo: str | None = None
|
|
80
|
+
plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
|
|
81
|
+
if plans_repo is not None and repo.github is not None:
|
|
82
|
+
source_repo = f"{repo.github.owner}/{repo.github.repo}"
|
|
83
|
+
|
|
84
|
+
# Use consolidated create_plan_issue for the entire workflow
|
|
85
|
+
result = create_plan_issue(
|
|
86
|
+
github_issues=ctx.issues,
|
|
87
|
+
repo_root=repo_root,
|
|
88
|
+
plan_content=content,
|
|
89
|
+
title=title,
|
|
90
|
+
plan_type=None,
|
|
91
|
+
extra_labels=extra_labels,
|
|
92
|
+
title_suffix=None,
|
|
93
|
+
source_plan_issues=None,
|
|
94
|
+
extraction_session_ids=None,
|
|
95
|
+
source_repo=source_repo,
|
|
96
|
+
objective_issue=None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not result.success:
|
|
100
|
+
if result.issue_number is not None:
|
|
101
|
+
# Partial success - issue created but comment failed
|
|
102
|
+
user_output(
|
|
103
|
+
click.style("Warning: ", fg="yellow")
|
|
104
|
+
+ f"Issue created but failed to add plan comment: {result.error}"
|
|
105
|
+
)
|
|
106
|
+
user_output(f"Issue #{result.issue_number} created but incomplete.")
|
|
107
|
+
user_output(f"URL: {result.issue_url}")
|
|
108
|
+
else:
|
|
109
|
+
user_output(click.style("Error: ", fg="red") + str(result.error))
|
|
110
|
+
raise SystemExit(1)
|
|
111
|
+
|
|
112
|
+
# Display success message with next steps
|
|
113
|
+
user_output(f"Created plan #{result.issue_number}")
|
|
114
|
+
user_output("")
|
|
115
|
+
user_output(f"Issue: {result.issue_url}")
|
|
116
|
+
user_output("")
|
|
117
|
+
user_output("Next steps:")
|
|
118
|
+
user_output(f" View: erk get {result.issue_number}")
|
|
119
|
+
user_output(f" Implement: erk implement {result.issue_number}")
|
|
120
|
+
user_output(f" Submit: erk submit {result.issue_number}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Docs subcommand group for plan documentation extraction tracking."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.plan.docs.extract_cmd import extract_docs
|
|
6
|
+
from erk.cli.commands.plan.docs.unextract_cmd import unextract_docs
|
|
7
|
+
from erk.cli.commands.plan.docs.unextracted_cmd import list_unextracted
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group("docs")
|
|
11
|
+
def docs_group() -> None:
|
|
12
|
+
"""Track documentation extraction from plan sessions."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
docs_group.add_command(list_unextracted, name="unextracted")
|
|
17
|
+
docs_group.add_command(extract_docs, name="extract")
|
|
18
|
+
docs_group.add_command(unextract_docs, name="unextract")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Command to mark a plan as having been analyzed for documentation."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.constants import (
|
|
6
|
+
DOCS_EXTRACTED_LABEL,
|
|
7
|
+
DOCS_EXTRACTED_LABEL_COLOR,
|
|
8
|
+
DOCS_EXTRACTED_LABEL_DESCRIPTION,
|
|
9
|
+
)
|
|
10
|
+
from erk.cli.core import discover_repo_context
|
|
11
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
12
|
+
from erk.core.context import ErkContext
|
|
13
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
14
|
+
from erk_shared.output.output import user_output
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("extract")
|
|
18
|
+
@click.argument("identifier", type=str)
|
|
19
|
+
@click.pass_obj
|
|
20
|
+
def extract_docs(ctx: ErkContext, identifier: str) -> None:
|
|
21
|
+
"""Mark a plan as having been analyzed for documentation.
|
|
22
|
+
|
|
23
|
+
Adds the docs-extracted label to the specified issue, indicating that
|
|
24
|
+
its session logs have been reviewed for documentation improvements.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
identifier: Plan identifier (e.g., "42" or GitHub URL)
|
|
28
|
+
"""
|
|
29
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
30
|
+
ensure_erk_metadata_dir(repo)
|
|
31
|
+
repo_root = repo.root
|
|
32
|
+
|
|
33
|
+
# Parse issue number
|
|
34
|
+
issue_number = parse_issue_identifier(identifier)
|
|
35
|
+
|
|
36
|
+
# Ensure label exists in repo (create if needed)
|
|
37
|
+
try:
|
|
38
|
+
ctx.issues.ensure_label_exists(
|
|
39
|
+
repo_root,
|
|
40
|
+
DOCS_EXTRACTED_LABEL,
|
|
41
|
+
DOCS_EXTRACTED_LABEL_DESCRIPTION,
|
|
42
|
+
DOCS_EXTRACTED_LABEL_COLOR,
|
|
43
|
+
)
|
|
44
|
+
except RuntimeError as e:
|
|
45
|
+
raise click.ClickException(f"Failed to ensure label exists: {e}") from e
|
|
46
|
+
|
|
47
|
+
# Add label to issue (idempotent)
|
|
48
|
+
try:
|
|
49
|
+
ctx.issues.ensure_label_on_issue(repo_root, issue_number, DOCS_EXTRACTED_LABEL)
|
|
50
|
+
except RuntimeError as e:
|
|
51
|
+
raise click.ClickException(f"Failed to add label to issue #{issue_number}: {e}") from e
|
|
52
|
+
|
|
53
|
+
user_output(f"Marked plan #{issue_number} as docs-extracted")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Command to remove the docs-extracted label from a plan (for re-analysis)."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.constants import DOCS_EXTRACTED_LABEL
|
|
6
|
+
from erk.cli.core import discover_repo_context
|
|
7
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
8
|
+
from erk.core.context import ErkContext
|
|
9
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
10
|
+
from erk_shared.output.output import user_output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("unextract")
|
|
14
|
+
@click.argument("identifier", type=str)
|
|
15
|
+
@click.pass_obj
|
|
16
|
+
def unextract_docs(ctx: ErkContext, identifier: str) -> None:
|
|
17
|
+
"""Remove the docs-extracted label from a plan (for re-analysis).
|
|
18
|
+
|
|
19
|
+
Removes the docs-extracted label from the specified issue, allowing it
|
|
20
|
+
to appear again in the unextracted list for future documentation review.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
identifier: Plan identifier (e.g., "42" or GitHub URL)
|
|
24
|
+
"""
|
|
25
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
26
|
+
ensure_erk_metadata_dir(repo)
|
|
27
|
+
repo_root = repo.root
|
|
28
|
+
|
|
29
|
+
# Parse issue number
|
|
30
|
+
issue_number = parse_issue_identifier(identifier)
|
|
31
|
+
|
|
32
|
+
# Remove label from issue
|
|
33
|
+
try:
|
|
34
|
+
ctx.issues.remove_label_from_issue(repo_root, issue_number, DOCS_EXTRACTED_LABEL)
|
|
35
|
+
except RuntimeError as e:
|
|
36
|
+
raise click.ClickException(f"Failed to remove label from issue #{issue_number}: {e}") from e
|
|
37
|
+
|
|
38
|
+
user_output(f"Removed docs-extracted label from plan #{issue_number}")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Command to list closed plans that haven't been analyzed for documentation."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from erk.cli.constants import DOCS_EXTRACTED_LABEL, ERK_PLAN_LABEL
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk.core.display_utils import format_relative_time
|
|
11
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
12
|
+
from erk_shared.output.output import user_output
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("unextracted")
|
|
16
|
+
@click.pass_obj
|
|
17
|
+
def list_unextracted(ctx: ErkContext) -> None:
|
|
18
|
+
"""List closed plans that haven't been analyzed for documentation.
|
|
19
|
+
|
|
20
|
+
Shows all closed erk-plan issues that don't have the docs-extracted label.
|
|
21
|
+
Use 'erk plan docs extract <number>' to mark a plan as extracted.
|
|
22
|
+
"""
|
|
23
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
24
|
+
ensure_erk_metadata_dir(repo)
|
|
25
|
+
repo_root = repo.root
|
|
26
|
+
|
|
27
|
+
# Query all closed issues with erk-plan label
|
|
28
|
+
try:
|
|
29
|
+
issues = ctx.issues.list_issues(
|
|
30
|
+
repo_root,
|
|
31
|
+
labels=[ERK_PLAN_LABEL],
|
|
32
|
+
state="closed",
|
|
33
|
+
)
|
|
34
|
+
except RuntimeError as e:
|
|
35
|
+
raise click.ClickException(f"Failed to list issues: {e}") from e
|
|
36
|
+
|
|
37
|
+
# Filter out issues that already have docs-extracted label
|
|
38
|
+
unextracted = [issue for issue in issues if DOCS_EXTRACTED_LABEL not in issue.labels]
|
|
39
|
+
|
|
40
|
+
if not unextracted:
|
|
41
|
+
user_output("No unextracted plans found. All closed plans have been analyzed.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Build table
|
|
45
|
+
table = Table(show_header=True, header_style="bold")
|
|
46
|
+
table.add_column("plan", style="cyan", no_wrap=True)
|
|
47
|
+
table.add_column("title", no_wrap=True)
|
|
48
|
+
table.add_column("closed", no_wrap=True)
|
|
49
|
+
|
|
50
|
+
for issue in unextracted:
|
|
51
|
+
# Format issue number with link
|
|
52
|
+
id_text = f"#{issue.number}"
|
|
53
|
+
if issue.url:
|
|
54
|
+
issue_id = f"[link={issue.url}][cyan]{id_text}[/cyan][/link]"
|
|
55
|
+
else:
|
|
56
|
+
issue_id = f"[cyan]{id_text}[/cyan]"
|
|
57
|
+
|
|
58
|
+
# Truncate title
|
|
59
|
+
title = issue.title
|
|
60
|
+
if len(title) > 50:
|
|
61
|
+
title = title[:47] + "..."
|
|
62
|
+
|
|
63
|
+
# Format closed time
|
|
64
|
+
closed_at = format_relative_time(issue.updated_at.isoformat()) if issue.updated_at else "-"
|
|
65
|
+
|
|
66
|
+
table.add_row(issue_id, title, closed_at)
|
|
67
|
+
|
|
68
|
+
user_output(f"\nFound {len(unextracted)} unextracted plan(s):\n")
|
|
69
|
+
|
|
70
|
+
console = Console(stderr=True, width=200, force_terminal=True)
|
|
71
|
+
console.print(table)
|
|
72
|
+
console.print()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Extraction subcommand group for plan documentation extraction workflow."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.plan.extraction.complete_cmd import complete_extraction
|
|
6
|
+
from erk.cli.commands.plan.extraction.create_raw_cmd import create_raw
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group("extraction")
|
|
10
|
+
def extraction_group() -> None:
|
|
11
|
+
"""Manage documentation extraction plans."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
extraction_group.add_command(complete_extraction, name="complete")
|
|
16
|
+
extraction_group.add_command(create_raw, name="raw")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Command to complete an extraction plan and mark source plans as extracted."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.constants import (
|
|
6
|
+
DOCS_EXTRACTED_LABEL,
|
|
7
|
+
DOCS_EXTRACTED_LABEL_COLOR,
|
|
8
|
+
DOCS_EXTRACTED_LABEL_DESCRIPTION,
|
|
9
|
+
)
|
|
10
|
+
from erk.cli.core import discover_repo_context
|
|
11
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
12
|
+
from erk.core.context import ErkContext
|
|
13
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
14
|
+
from erk_shared.github.metadata.core import find_metadata_block
|
|
15
|
+
from erk_shared.output.output import user_output
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("complete")
|
|
19
|
+
@click.argument("identifier", type=str)
|
|
20
|
+
@click.pass_obj
|
|
21
|
+
def complete_extraction(ctx: ErkContext, identifier: str) -> None:
|
|
22
|
+
"""Complete an extraction plan by marking source plans as docs-extracted.
|
|
23
|
+
|
|
24
|
+
Reads the extraction plan's metadata to find source_plan_issues,
|
|
25
|
+
then adds the docs-extracted label to each source plan.
|
|
26
|
+
|
|
27
|
+
This command is idempotent - safe to run multiple times.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
identifier: Extraction plan identifier (e.g., "42" or GitHub URL)
|
|
31
|
+
"""
|
|
32
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
33
|
+
ensure_erk_metadata_dir(repo)
|
|
34
|
+
repo_root = repo.root
|
|
35
|
+
|
|
36
|
+
# Parse extraction plan issue number
|
|
37
|
+
issue_number = parse_issue_identifier(identifier)
|
|
38
|
+
|
|
39
|
+
# Fetch the extraction plan issue to read its metadata
|
|
40
|
+
try:
|
|
41
|
+
issue_info = ctx.issues.get_issue(repo_root, issue_number)
|
|
42
|
+
except RuntimeError as e:
|
|
43
|
+
raise click.ClickException(f"Failed to fetch issue #{issue_number}: {e}") from e
|
|
44
|
+
|
|
45
|
+
# Extract plan-header metadata block
|
|
46
|
+
plan_header = find_metadata_block(issue_info.body, "plan-header")
|
|
47
|
+
if plan_header is None:
|
|
48
|
+
raise click.ClickException(
|
|
49
|
+
f"Issue #{issue_number} does not have a plan-header metadata block. "
|
|
50
|
+
"Is this an erk plan issue?"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Check plan_type
|
|
54
|
+
plan_type = plan_header.data.get("plan_type")
|
|
55
|
+
if plan_type != "extraction":
|
|
56
|
+
raise click.ClickException(
|
|
57
|
+
f"Issue #{issue_number} is not an extraction plan (plan_type: {plan_type}). "
|
|
58
|
+
"This command only works on extraction plans."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Get source_plan_issues
|
|
62
|
+
source_plan_issues = plan_header.data.get("source_plan_issues")
|
|
63
|
+
if not source_plan_issues:
|
|
64
|
+
raise click.ClickException(
|
|
65
|
+
f"Issue #{issue_number} has no source_plan_issues in its metadata. "
|
|
66
|
+
"Cannot determine which plans to mark as extracted."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Ensure docs-extracted label exists
|
|
70
|
+
try:
|
|
71
|
+
ctx.issues.ensure_label_exists(
|
|
72
|
+
repo_root,
|
|
73
|
+
DOCS_EXTRACTED_LABEL,
|
|
74
|
+
DOCS_EXTRACTED_LABEL_DESCRIPTION,
|
|
75
|
+
DOCS_EXTRACTED_LABEL_COLOR,
|
|
76
|
+
)
|
|
77
|
+
except RuntimeError as e:
|
|
78
|
+
raise click.ClickException(f"Failed to ensure label exists: {e}") from e
|
|
79
|
+
|
|
80
|
+
# Mark each source plan as docs-extracted
|
|
81
|
+
marked_count = 0
|
|
82
|
+
for source_issue_number in source_plan_issues:
|
|
83
|
+
try:
|
|
84
|
+
ctx.issues.ensure_label_on_issue(repo_root, source_issue_number, DOCS_EXTRACTED_LABEL)
|
|
85
|
+
user_output(f" Marked plan #{source_issue_number} as docs-extracted")
|
|
86
|
+
marked_count += 1
|
|
87
|
+
except RuntimeError as e:
|
|
88
|
+
# Log the error but continue with other issues
|
|
89
|
+
click.echo(f" Warning: Failed to mark plan #{source_issue_number}: {e}", err=True)
|
|
90
|
+
|
|
91
|
+
# Summary
|
|
92
|
+
if marked_count == len(source_plan_issues):
|
|
93
|
+
user_output(
|
|
94
|
+
f"\nExtraction plan #{issue_number} completed: "
|
|
95
|
+
f"marked {marked_count} source plan(s) as docs-extracted"
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
user_output(
|
|
99
|
+
f"\nExtraction plan #{issue_number} partially completed: "
|
|
100
|
+
f"marked {marked_count}/{len(source_plan_issues)} source plan(s) as docs-extracted"
|
|
101
|
+
)
|