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,159 @@
|
|
|
1
|
+
"""Set up .impl/ folder from GitHub issue in current worktree.
|
|
2
|
+
|
|
3
|
+
This exec command fetches a plan from a GitHub issue, creates a feature branch,
|
|
4
|
+
checks it out, and creates the .impl/ folder structure for implementation.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
erk exec setup-impl-from-issue <issue-number> [--session-id <id>]
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
Structured JSON output with success status and folder details
|
|
11
|
+
|
|
12
|
+
Exit Codes:
|
|
13
|
+
0: Success (.impl/ folder created, branch checked out)
|
|
14
|
+
1: Error (issue not found, plan fetch failed, git operations failed)
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
$ erk exec setup-impl-from-issue 1028
|
|
18
|
+
{"success": true, "impl_path": "/path/to/.impl", "issue_number": 1028, "branch": "P1028-..."}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from erk_shared.context.helpers import require_cwd, require_git, require_repo_root
|
|
28
|
+
from erk_shared.gateway.time.real import RealTime
|
|
29
|
+
from erk_shared.git.abc import Git
|
|
30
|
+
from erk_shared.github.issues import RealGitHubIssues
|
|
31
|
+
from erk_shared.impl_folder import create_impl_folder, save_issue_reference
|
|
32
|
+
from erk_shared.naming import generate_issue_branch_name
|
|
33
|
+
from erk_shared.plan_store.github import GitHubPlanStore
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_current_branch(git: Git, cwd: Path) -> str:
|
|
37
|
+
"""Get current branch via gateway, raising if detached HEAD."""
|
|
38
|
+
branch = git.get_current_branch(cwd)
|
|
39
|
+
if branch is None:
|
|
40
|
+
msg = "Cannot set up implementation from detached HEAD state"
|
|
41
|
+
raise click.ClickException(msg)
|
|
42
|
+
return branch
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_trunk_branch(branch: str) -> bool:
|
|
46
|
+
"""Check if branch is a trunk branch (main/master)."""
|
|
47
|
+
return branch in ("main", "master")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.command(name="setup-impl-from-issue")
|
|
51
|
+
@click.argument("issue_number", type=int)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--session-id",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Claude session ID for marker creation",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--no-impl",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
help="Skip .impl/ folder creation (for local execution without file overhead)",
|
|
61
|
+
)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def setup_impl_from_issue(
|
|
64
|
+
ctx: click.Context,
|
|
65
|
+
issue_number: int,
|
|
66
|
+
session_id: str | None,
|
|
67
|
+
no_impl: bool,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Set up .impl/ folder from GitHub issue in current worktree.
|
|
70
|
+
|
|
71
|
+
Fetches plan content from GitHub issue, creates/checks out a feature branch,
|
|
72
|
+
and creates .impl/ folder structure with plan.md, progress.md, and issue.json.
|
|
73
|
+
|
|
74
|
+
ISSUE_NUMBER: GitHub issue number containing the plan
|
|
75
|
+
|
|
76
|
+
The command:
|
|
77
|
+
1. Fetches the plan from the GitHub issue
|
|
78
|
+
2. Creates a feature branch from current branch (stacked) or trunk
|
|
79
|
+
3. Checks out the new branch in the current worktree
|
|
80
|
+
4. Creates .impl/ folder with plan content
|
|
81
|
+
5. Saves issue reference for PR linking
|
|
82
|
+
"""
|
|
83
|
+
cwd = require_cwd(ctx)
|
|
84
|
+
repo_root = require_repo_root(ctx)
|
|
85
|
+
git = require_git(ctx)
|
|
86
|
+
|
|
87
|
+
# Direct instantiation of required dependencies
|
|
88
|
+
time = RealTime()
|
|
89
|
+
github_issues = RealGitHubIssues(target_repo=None)
|
|
90
|
+
plan_store = GitHubPlanStore(github_issues, time)
|
|
91
|
+
|
|
92
|
+
# Step 1: Fetch plan from GitHub
|
|
93
|
+
try:
|
|
94
|
+
plan = plan_store.get_plan(repo_root, str(issue_number))
|
|
95
|
+
except RuntimeError as e:
|
|
96
|
+
error_output = {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": "plan_not_found",
|
|
99
|
+
"message": f"Could not fetch plan for issue #{issue_number}: {e}. "
|
|
100
|
+
f"Ensure issue has erk-plan label and plan content.",
|
|
101
|
+
}
|
|
102
|
+
click.echo(json.dumps(error_output), err=True)
|
|
103
|
+
raise SystemExit(1) from e
|
|
104
|
+
|
|
105
|
+
# Step 2: Determine base branch and create feature branch
|
|
106
|
+
current_branch = _get_current_branch(git, cwd)
|
|
107
|
+
|
|
108
|
+
# Generate branch name from issue
|
|
109
|
+
timestamp = datetime.now(UTC)
|
|
110
|
+
branch_name = generate_issue_branch_name(issue_number, plan.title, timestamp)
|
|
111
|
+
|
|
112
|
+
# Check if branch already exists
|
|
113
|
+
local_branches = git.list_local_branches(repo_root)
|
|
114
|
+
|
|
115
|
+
if branch_name in local_branches:
|
|
116
|
+
# Branch exists - just check it out
|
|
117
|
+
click.echo(f"Branch '{branch_name}' already exists, checking out...", err=True)
|
|
118
|
+
git.checkout_branch(cwd, branch_name)
|
|
119
|
+
else:
|
|
120
|
+
# Determine base branch: stack on feature branch, or use trunk
|
|
121
|
+
if _is_trunk_branch(current_branch):
|
|
122
|
+
base_branch = current_branch
|
|
123
|
+
else:
|
|
124
|
+
# Stack on current feature branch
|
|
125
|
+
base_branch = current_branch
|
|
126
|
+
|
|
127
|
+
# Create and checkout branch
|
|
128
|
+
git.create_branch(repo_root, branch_name, base_branch)
|
|
129
|
+
git.checkout_branch(cwd, branch_name)
|
|
130
|
+
click.echo(f"Created branch '{branch_name}' from '{base_branch}'", err=True)
|
|
131
|
+
|
|
132
|
+
# Step 3: Create .impl/ folder with plan content (unless --no-impl)
|
|
133
|
+
impl_path_str: str | None = None
|
|
134
|
+
|
|
135
|
+
if not no_impl:
|
|
136
|
+
impl_path = cwd / ".impl"
|
|
137
|
+
impl_path_str = str(impl_path)
|
|
138
|
+
|
|
139
|
+
# Use overwrite=True since we may be re-running after a failed attempt
|
|
140
|
+
create_impl_folder(
|
|
141
|
+
worktree_path=cwd,
|
|
142
|
+
plan_content=plan.body,
|
|
143
|
+
overwrite=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Step 4: Save issue reference for PR linking
|
|
147
|
+
save_issue_reference(impl_path, issue_number, plan.url, plan.title)
|
|
148
|
+
|
|
149
|
+
# Output structured success result
|
|
150
|
+
output: dict[str, str | int | bool | None] = {
|
|
151
|
+
"success": True,
|
|
152
|
+
"impl_path": impl_path_str,
|
|
153
|
+
"issue_number": issue_number,
|
|
154
|
+
"issue_url": plan.url,
|
|
155
|
+
"branch": branch_name,
|
|
156
|
+
"plan_title": plan.title,
|
|
157
|
+
"no_impl": no_impl,
|
|
158
|
+
}
|
|
159
|
+
click.echo(json.dumps(output))
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Get the last objective issue for the current slot.
|
|
2
|
+
|
|
3
|
+
This exec command looks up the current worktree's slot and returns
|
|
4
|
+
the last_objective_issue from pool.json.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
erk exec slot-objective
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
JSON with objective_issue (null if not found or not in a slot)
|
|
11
|
+
|
|
12
|
+
Exit Codes:
|
|
13
|
+
0: Success (even if no objective found)
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
$ erk exec slot-objective
|
|
17
|
+
{"objective_issue": 123, "slot_name": "erk-managed-wt-01"}
|
|
18
|
+
|
|
19
|
+
$ erk exec slot-objective # Not in a slot worktree
|
|
20
|
+
{"objective_issue": null, "slot_name": null}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
|
|
28
|
+
from erk.core.worktree_pool import load_pool_state
|
|
29
|
+
from erk_shared.context.helpers import require_cwd
|
|
30
|
+
from erk_shared.context.types import NoRepoSentinel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _null_result() -> None:
|
|
34
|
+
"""Output null result and return."""
|
|
35
|
+
click.echo(json.dumps({"objective_issue": None, "slot_name": None}))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command(name="slot-objective")
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def slot_objective(ctx: click.Context) -> None:
|
|
41
|
+
"""Get the last objective issue for the current slot.
|
|
42
|
+
|
|
43
|
+
Looks up the current worktree in pool.json and returns
|
|
44
|
+
the slot's last_objective_issue if set.
|
|
45
|
+
"""
|
|
46
|
+
if ctx.obj is None:
|
|
47
|
+
_null_result()
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
cwd = require_cwd(ctx)
|
|
51
|
+
repo = ctx.obj.repo
|
|
52
|
+
|
|
53
|
+
# Check if we're in a repo
|
|
54
|
+
if isinstance(repo, NoRepoSentinel):
|
|
55
|
+
_null_result()
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Load pool state
|
|
59
|
+
state = load_pool_state(repo.pool_json_path)
|
|
60
|
+
if state is None:
|
|
61
|
+
_null_result()
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Find assignment for current worktree
|
|
65
|
+
cwd_resolved = cwd.resolve()
|
|
66
|
+
slot_name: str | None = None
|
|
67
|
+
|
|
68
|
+
for assignment in state.assignments:
|
|
69
|
+
if not assignment.worktree_path.exists():
|
|
70
|
+
continue
|
|
71
|
+
if assignment.worktree_path.resolve() == cwd_resolved:
|
|
72
|
+
slot_name = assignment.slot_name
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if slot_name is None:
|
|
76
|
+
# Check if cwd is within an assignment's worktree
|
|
77
|
+
for assignment in state.assignments:
|
|
78
|
+
if _is_path_within(cwd_resolved, assignment.worktree_path):
|
|
79
|
+
slot_name = assignment.slot_name
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if slot_name is None:
|
|
83
|
+
_null_result()
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Find slot info to get last_objective_issue
|
|
87
|
+
objective_issue: int | None = None
|
|
88
|
+
for slot in state.slots:
|
|
89
|
+
if slot.name == slot_name:
|
|
90
|
+
objective_issue = slot.last_objective_issue
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
result = {"objective_issue": objective_issue, "slot_name": slot_name}
|
|
94
|
+
click.echo(json.dumps(result))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_path_within(child: Path, parent: Path) -> bool:
|
|
98
|
+
"""Check if child path is within parent path."""
|
|
99
|
+
if not parent.exists():
|
|
100
|
+
return False
|
|
101
|
+
parent_resolved = parent.resolve()
|
|
102
|
+
return child == parent_resolved or parent_resolved in child.parents
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tripwires Reminder Hook."""
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from erk.hooks.decorators import HookContext, hook_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@hook_command()
|
|
10
|
+
def tripwires_reminder_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
11
|
+
"""Output tripwires reminder for UserPromptSubmit hook."""
|
|
12
|
+
# Scope check: only run in erk-managed projects
|
|
13
|
+
if not hook_ctx.is_erk_project:
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
click.echo("🚧 Ensure docs/learned/tripwires.md is loaded and follow its directives.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
tripwires_reminder_hook()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Update dispatch info in GitHub issue plan-header metadata.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
erk exec update-dispatch-info <issue-number> <run-id> <node-id> <dispatched-at>
|
|
5
|
+
|
|
6
|
+
Output:
|
|
7
|
+
JSON with success status and issue_number
|
|
8
|
+
|
|
9
|
+
Exit Codes:
|
|
10
|
+
0: Success
|
|
11
|
+
1: Error (issue not found, invalid inputs, no plan-header block)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from erk_shared.context.helpers import require_issues as require_github_issues
|
|
20
|
+
from erk_shared.context.helpers import require_repo_root
|
|
21
|
+
from erk_shared.github.metadata.plan_header import update_plan_header_dispatch
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class UpdateSuccess:
|
|
26
|
+
"""Success response for dispatch info update."""
|
|
27
|
+
|
|
28
|
+
success: bool
|
|
29
|
+
issue_number: int
|
|
30
|
+
run_id: str
|
|
31
|
+
node_id: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class UpdateError:
|
|
36
|
+
"""Error response for dispatch info update."""
|
|
37
|
+
|
|
38
|
+
success: bool
|
|
39
|
+
error: str
|
|
40
|
+
message: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.command(name="update-dispatch-info")
|
|
44
|
+
@click.argument("issue_number", type=int)
|
|
45
|
+
@click.argument("run_id")
|
|
46
|
+
@click.argument("node_id")
|
|
47
|
+
@click.argument("dispatched_at")
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def update_dispatch_info(
|
|
50
|
+
ctx: click.Context,
|
|
51
|
+
issue_number: int,
|
|
52
|
+
run_id: str,
|
|
53
|
+
node_id: str,
|
|
54
|
+
dispatched_at: str,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Update dispatch info in GitHub issue plan-header metadata.
|
|
57
|
+
|
|
58
|
+
Fetches the issue, updates the plan-header block with last_dispatched_run_id,
|
|
59
|
+
last_dispatched_node_id, and last_dispatched_at, and posts the updated body
|
|
60
|
+
back to GitHub.
|
|
61
|
+
|
|
62
|
+
If issue uses old format (no plan-header block), exits with error code 1.
|
|
63
|
+
"""
|
|
64
|
+
# Get dependencies from context
|
|
65
|
+
github_issues = require_github_issues(ctx)
|
|
66
|
+
repo_root = require_repo_root(ctx)
|
|
67
|
+
|
|
68
|
+
# Fetch current issue
|
|
69
|
+
try:
|
|
70
|
+
issue = github_issues.get_issue(repo_root, issue_number)
|
|
71
|
+
except RuntimeError as e:
|
|
72
|
+
result = UpdateError(
|
|
73
|
+
success=False,
|
|
74
|
+
error="issue-not-found",
|
|
75
|
+
message=f"Issue #{issue_number} not found: {e}",
|
|
76
|
+
)
|
|
77
|
+
click.echo(json.dumps(asdict(result)), err=True)
|
|
78
|
+
raise SystemExit(1) from None
|
|
79
|
+
|
|
80
|
+
# Update dispatch info
|
|
81
|
+
try:
|
|
82
|
+
updated_body = update_plan_header_dispatch(
|
|
83
|
+
issue_body=issue.body,
|
|
84
|
+
run_id=run_id,
|
|
85
|
+
node_id=node_id,
|
|
86
|
+
dispatched_at=dispatched_at,
|
|
87
|
+
)
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
# plan-header block not found (old format issue)
|
|
90
|
+
result = UpdateError(
|
|
91
|
+
success=False,
|
|
92
|
+
error="no-plan-header-block",
|
|
93
|
+
message=str(e),
|
|
94
|
+
)
|
|
95
|
+
click.echo(json.dumps(asdict(result)), err=True)
|
|
96
|
+
raise SystemExit(1) from None
|
|
97
|
+
|
|
98
|
+
# Update issue body
|
|
99
|
+
try:
|
|
100
|
+
github_issues.update_issue_body(repo_root, issue_number, updated_body)
|
|
101
|
+
except RuntimeError as e:
|
|
102
|
+
result = UpdateError(
|
|
103
|
+
success=False,
|
|
104
|
+
error="github-api-failed",
|
|
105
|
+
message=f"Failed to update issue body: {e}",
|
|
106
|
+
)
|
|
107
|
+
click.echo(json.dumps(asdict(result)), err=True)
|
|
108
|
+
raise SystemExit(1) from None
|
|
109
|
+
|
|
110
|
+
result_success = UpdateSuccess(
|
|
111
|
+
success=True,
|
|
112
|
+
issue_number=issue_number,
|
|
113
|
+
run_id=run_id,
|
|
114
|
+
node_id=node_id,
|
|
115
|
+
)
|
|
116
|
+
click.echo(json.dumps(asdict(result_success)))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit hook for erk.
|
|
3
|
+
|
|
4
|
+
Consolidates multiple hooks into a single script:
|
|
5
|
+
1. Session ID injection + file persistence
|
|
6
|
+
2. Coding standards reminders
|
|
7
|
+
3. Tripwires reminder
|
|
8
|
+
|
|
9
|
+
Exit codes:
|
|
10
|
+
0: All checks pass, stdout goes to Claude's context
|
|
11
|
+
|
|
12
|
+
This command is invoked via:
|
|
13
|
+
ERK_HOOK_ID=user-prompt-hook erk exec user-prompt-hook
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from erk.hooks.decorators import HookContext, hook_command
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Pure Functions for Output Building
|
|
24
|
+
# ============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_session_context(session_id: str | None) -> str:
|
|
28
|
+
"""Build the session ID context string.
|
|
29
|
+
|
|
30
|
+
Pure function - string building only.
|
|
31
|
+
"""
|
|
32
|
+
if session_id is None:
|
|
33
|
+
return ""
|
|
34
|
+
return f"session: {session_id}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_coding_standards_reminder() -> str:
|
|
38
|
+
"""Return coding standards context.
|
|
39
|
+
|
|
40
|
+
Pure function - returns static string.
|
|
41
|
+
"""
|
|
42
|
+
return """No direct Bash for: pytest/ty/ruff/prettier/make/gt
|
|
43
|
+
Use Task(subagent_type='devrun') instead.
|
|
44
|
+
dignified-python: CRITICAL RULES (examples - full skill has more):
|
|
45
|
+
NO try/except for control flow (use LBYL - check conditions first)
|
|
46
|
+
NO default parameter values (no `foo: bool = False`)
|
|
47
|
+
NO mutable/non-frozen dataclasses (always `@dataclass(frozen=True)`)
|
|
48
|
+
MANDATORY: Load and READ the full dignified-python skill documents.
|
|
49
|
+
These are examples only. You MUST strictly abide by ALL rules in the skill.
|
|
50
|
+
AFTER completing Python changes: Verify sufficient test coverage.
|
|
51
|
+
Behavior changes ALWAYS need tests."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_tripwires_reminder() -> str:
|
|
55
|
+
"""Return tripwires context.
|
|
56
|
+
|
|
57
|
+
Pure function - returns static string.
|
|
58
|
+
"""
|
|
59
|
+
return "Ensure docs/learned/tripwires.md is loaded and follow its directives."
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# I/O Helper Functions
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _persist_session_id(repo_root: Path, session_id: str | None) -> None:
|
|
68
|
+
"""Write session ID to file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
repo_root: Path to the git repository root.
|
|
72
|
+
session_id: The current session ID, or None if not available.
|
|
73
|
+
"""
|
|
74
|
+
if session_id is None:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
session_file = repo_root / ".erk" / "scratch" / "current-session-id"
|
|
78
|
+
session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
session_file.write_text(session_id, encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# Main Hook Entry Point
|
|
84
|
+
# ============================================================================
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@hook_command(name="user-prompt-hook")
|
|
88
|
+
def user_prompt_hook(ctx: click.Context, *, hook_ctx: HookContext) -> None:
|
|
89
|
+
"""UserPromptSubmit hook for session persistence and coding reminders.
|
|
90
|
+
|
|
91
|
+
This hook runs on every user prompt submission in erk-managed projects.
|
|
92
|
+
|
|
93
|
+
Exit codes:
|
|
94
|
+
0: Success - context emitted to stdout
|
|
95
|
+
"""
|
|
96
|
+
# Scope check: only run in erk-managed projects
|
|
97
|
+
if not hook_ctx.is_erk_project:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Persist session ID
|
|
101
|
+
_persist_session_id(hook_ctx.repo_root, hook_ctx.session_id)
|
|
102
|
+
|
|
103
|
+
# Build and emit context
|
|
104
|
+
context_parts = [
|
|
105
|
+
build_session_context(hook_ctx.session_id),
|
|
106
|
+
build_coding_standards_reminder(),
|
|
107
|
+
build_tripwires_reminder(),
|
|
108
|
+
]
|
|
109
|
+
click.echo("\n".join(p for p in context_parts if p))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
user_prompt_hook()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Validate plan content structure and quality.
|
|
2
|
+
|
|
3
|
+
This exec command validates that plan content meets minimum requirements
|
|
4
|
+
for structure and length. It accepts plan content via stdin.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
echo "$plan" | erk exec validate-plan-content
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
JSON with validation status and details
|
|
11
|
+
|
|
12
|
+
Exit Codes:
|
|
13
|
+
0: Success (always - check JSON for validation result)
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
$ echo "# My Plan\n\n- Step 1\n- Step 2" | erk exec validate-plan-content
|
|
17
|
+
{"valid": true, "error": null, "details": {"length": 29, "has_headers": true,
|
|
18
|
+
"has_lists": true}}
|
|
19
|
+
|
|
20
|
+
$ echo "too short" | erk exec validate-plan-content
|
|
21
|
+
{"valid": false, "error": "Plan too short (9 characters, minimum 100)",
|
|
22
|
+
"details": {"length": 9, "has_headers": false, "has_lists": false}}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_plan_content(content: str) -> tuple[bool, str | None, dict[str, bool | int]]:
|
|
32
|
+
"""Validate plan content meets minimum requirements.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
content: Plan content as string
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (valid, error_message, details_dict)
|
|
39
|
+
- valid: True if plan passes all checks
|
|
40
|
+
- error_message: None if valid, descriptive error if invalid
|
|
41
|
+
- details_dict: Dict with length, has_headers, has_lists
|
|
42
|
+
"""
|
|
43
|
+
# Strip whitespace for validation
|
|
44
|
+
content_stripped = content.strip()
|
|
45
|
+
length = len(content_stripped)
|
|
46
|
+
|
|
47
|
+
# Check for structural elements
|
|
48
|
+
has_headers = any(line.startswith("#") for line in content_stripped.split("\n"))
|
|
49
|
+
has_lists = any(
|
|
50
|
+
line.strip().startswith(("-", "*", "+"))
|
|
51
|
+
or (line.strip() and line.strip()[0].isdigit() and ". " in line)
|
|
52
|
+
for line in content_stripped.split("\n")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
details = {
|
|
56
|
+
"length": length,
|
|
57
|
+
"has_headers": has_headers,
|
|
58
|
+
"has_lists": has_lists,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Validation checks
|
|
62
|
+
if not content_stripped:
|
|
63
|
+
return False, "Plan is empty or contains only whitespace", details
|
|
64
|
+
|
|
65
|
+
if length < 100:
|
|
66
|
+
return False, f"Plan too short ({length} characters, minimum 100)", details
|
|
67
|
+
|
|
68
|
+
if not has_headers and not has_lists:
|
|
69
|
+
return (
|
|
70
|
+
False,
|
|
71
|
+
"Plan lacks structure (no headers or lists found)",
|
|
72
|
+
details,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return True, None, details
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.command(name="validate-plan-content")
|
|
79
|
+
def validate_plan_content() -> None:
|
|
80
|
+
"""Validate plan content from stdin.
|
|
81
|
+
|
|
82
|
+
Reads plan content from stdin and validates:
|
|
83
|
+
- Minimum 100 characters
|
|
84
|
+
- Contains structural elements (headers OR lists)
|
|
85
|
+
- Not empty/whitespace only
|
|
86
|
+
|
|
87
|
+
Outputs JSON with validation result and details.
|
|
88
|
+
"""
|
|
89
|
+
content = sys.stdin.read()
|
|
90
|
+
valid, error, details = _validate_plan_content(content)
|
|
91
|
+
|
|
92
|
+
result = {
|
|
93
|
+
"valid": valid,
|
|
94
|
+
"error": error,
|
|
95
|
+
"details": details,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
click.echo(json.dumps(result))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Wrap a plan in a collapsible GitHub metadata block."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command(name="wrap-plan-in-metadata-block")
|
|
9
|
+
def wrap_plan_in_metadata_block() -> None:
|
|
10
|
+
"""Return plan content for issue body.
|
|
11
|
+
|
|
12
|
+
Reads plan content from stdin and returns it as-is (stripped).
|
|
13
|
+
Formatting and workflow instructions will be added via a separate comment.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
echo "$plan" | erk exec wrap-plan-in-metadata-block
|
|
17
|
+
|
|
18
|
+
Exit Codes:
|
|
19
|
+
0: Success
|
|
20
|
+
1: Error (empty input)
|
|
21
|
+
"""
|
|
22
|
+
# Read plan content from stdin
|
|
23
|
+
plan_content = sys.stdin.read()
|
|
24
|
+
|
|
25
|
+
# Validate input is not empty
|
|
26
|
+
if not plan_content or not plan_content.strip():
|
|
27
|
+
click.echo("Error: Empty plan content received", err=True)
|
|
28
|
+
raise SystemExit(1)
|
|
29
|
+
|
|
30
|
+
# Return plan content as-is (metadata wrapping delegated to separate comments)
|
|
31
|
+
result = plan_content.strip()
|
|
32
|
+
|
|
33
|
+
# Output the result
|
|
34
|
+
click.echo(result)
|