claude-dev-env 2.3.0 → 2.5.0
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.
- package/CLAUDE.md +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +3 -4
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +2 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -146,6 +146,21 @@ def build_repo_with_origin(workspace_path: Path) -> Path:
|
|
|
146
146
|
return clone_path
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def read_branch_config(
|
|
150
|
+
repository_path: Path,
|
|
151
|
+
branch_name: str,
|
|
152
|
+
config_leaf: str,
|
|
153
|
+
) -> str:
|
|
154
|
+
completed = subprocess.run(
|
|
155
|
+
["git", "config", "--get", f"branch.{branch_name}.{config_leaf}"],
|
|
156
|
+
cwd=str(repository_path),
|
|
157
|
+
check=False,
|
|
158
|
+
capture_output=True,
|
|
159
|
+
text=True,
|
|
160
|
+
)
|
|
161
|
+
return completed.stdout.strip()
|
|
162
|
+
|
|
163
|
+
|
|
149
164
|
def read_head_branch(repository_path: Path) -> str:
|
|
150
165
|
completed = run_git(["rev-parse", "--abbrev-ref", "HEAD"], repository_path)
|
|
151
166
|
return completed.stdout.strip()
|
|
@@ -156,6 +171,14 @@ def read_head_commit(repository_path: Path) -> str:
|
|
|
156
171
|
return completed.stdout.strip()
|
|
157
172
|
|
|
158
173
|
|
|
174
|
+
def read_remote_main_commit(repository_path: Path) -> str:
|
|
175
|
+
completed = run_git(
|
|
176
|
+
["ls-remote", REMOTE_NAME, f"refs/heads/{MAIN_BRANCH_NAME}"],
|
|
177
|
+
repository_path,
|
|
178
|
+
)
|
|
179
|
+
return completed.stdout.split()[0]
|
|
180
|
+
|
|
181
|
+
|
|
159
182
|
class TestResolveAgentSlug:
|
|
160
183
|
def should_prefer_flag_over_environment(
|
|
161
184
|
self,
|
|
@@ -512,6 +535,81 @@ class TestCreateFreshBranchIntegration:
|
|
|
512
535
|
assert read_head_commit(worktree_path) != local_main_commit
|
|
513
536
|
|
|
514
537
|
|
|
538
|
+
class TestWorktreeBranchTracking:
|
|
539
|
+
def should_leave_new_branch_without_an_upstream(
|
|
540
|
+
self,
|
|
541
|
+
tmp_path: Path,
|
|
542
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
543
|
+
) -> None:
|
|
544
|
+
module = load_create_fresh_branch_module()
|
|
545
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
546
|
+
agent_scratch_parent = tmp_path / "agent-scratch"
|
|
547
|
+
monkeypatch.setattr(module.sys, "platform", "linux")
|
|
548
|
+
monkeypatch.setattr(
|
|
549
|
+
module.tempfile,
|
|
550
|
+
"gettempdir",
|
|
551
|
+
lambda: str(agent_scratch_parent),
|
|
552
|
+
)
|
|
553
|
+
success_payload = module.create_fresh_branch(
|
|
554
|
+
branch_name="fix/no-upstream",
|
|
555
|
+
repo_path=repository_path,
|
|
556
|
+
agent_slug="claude",
|
|
557
|
+
base_ref=DEFAULT_BASE_REF,
|
|
558
|
+
)
|
|
559
|
+
worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
|
|
560
|
+
assert read_head_branch(worktree_path) == "fix/no-upstream"
|
|
561
|
+
assert read_branch_config(repository_path, "fix/no-upstream", "merge") == ""
|
|
562
|
+
assert read_branch_config(repository_path, "fix/no-upstream", "remote") == ""
|
|
563
|
+
|
|
564
|
+
def should_not_aim_a_bare_push_at_main_under_push_default_upstream(
|
|
565
|
+
self,
|
|
566
|
+
tmp_path: Path,
|
|
567
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
568
|
+
) -> None:
|
|
569
|
+
module = load_create_fresh_branch_module()
|
|
570
|
+
repository_path = build_repo_with_origin(tmp_path / "repo")
|
|
571
|
+
empty_hooks_path = tmp_path / "repo" / "empty-hooks"
|
|
572
|
+
run_git(
|
|
573
|
+
["config", "push.default", "upstream"],
|
|
574
|
+
repository_path,
|
|
575
|
+
empty_hooks_path=empty_hooks_path,
|
|
576
|
+
)
|
|
577
|
+
agent_scratch_parent = tmp_path / "agent-scratch"
|
|
578
|
+
monkeypatch.setattr(module.sys, "platform", "linux")
|
|
579
|
+
monkeypatch.setattr(
|
|
580
|
+
module.tempfile,
|
|
581
|
+
"gettempdir",
|
|
582
|
+
lambda: str(agent_scratch_parent),
|
|
583
|
+
)
|
|
584
|
+
success_payload = module.create_fresh_branch(
|
|
585
|
+
branch_name="feature/silent-push",
|
|
586
|
+
repo_path=repository_path,
|
|
587
|
+
agent_slug="claude",
|
|
588
|
+
base_ref=DEFAULT_BASE_REF,
|
|
589
|
+
)
|
|
590
|
+
worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
|
|
591
|
+
(worktree_path / SEED_FILE_NAME).write_text("drive-by\n", encoding="utf-8")
|
|
592
|
+
run_git(["add", SEED_FILE_NAME], worktree_path, empty_hooks_path=empty_hooks_path)
|
|
593
|
+
run_git(
|
|
594
|
+
["commit", "-m", "work on the feature branch"],
|
|
595
|
+
worktree_path,
|
|
596
|
+
empty_hooks_path=empty_hooks_path,
|
|
597
|
+
)
|
|
598
|
+
origin_main_before = read_remote_main_commit(repository_path)
|
|
599
|
+
push_attempt = subprocess.run(
|
|
600
|
+
["git", "-c", f"core.hooksPath={empty_hooks_path}", "push"],
|
|
601
|
+
cwd=str(worktree_path),
|
|
602
|
+
check=False,
|
|
603
|
+
capture_output=True,
|
|
604
|
+
text=True,
|
|
605
|
+
)
|
|
606
|
+
assert push_attempt.returncode != 0, (
|
|
607
|
+
"bare push should be refused, but it succeeded:\n"
|
|
608
|
+
f"{push_attempt.stdout}{push_attempt.stderr}"
|
|
609
|
+
)
|
|
610
|
+
assert read_remote_main_commit(repository_path) == origin_main_before
|
|
611
|
+
|
|
612
|
+
|
|
515
613
|
class TestMainCli:
|
|
516
614
|
def should_print_success_json_and_exit_zero(
|
|
517
615
|
self,
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Behavioral tests for fresh_branch_git_commands using real temporary git repos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
12
|
+
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
from fresh_branch_git_commands import (
|
|
16
|
+
assert_git_accepts_branch_name,
|
|
17
|
+
build_worktree_add_arguments,
|
|
18
|
+
create_worktree_branch,
|
|
19
|
+
fetch_base_ref,
|
|
20
|
+
is_ref_present,
|
|
21
|
+
read_failure_text,
|
|
22
|
+
resolve_base_commit,
|
|
23
|
+
resolve_repo_root,
|
|
24
|
+
run_git,
|
|
25
|
+
split_remote_ref,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
GIT_USER_NAME = "fresh-branch-test"
|
|
29
|
+
GIT_USER_EMAIL = "fresh-branch-test@example.com"
|
|
30
|
+
MAIN_BRANCH_NAME = "main"
|
|
31
|
+
SEED_FILE_NAME = "README.md"
|
|
32
|
+
SEED_FILE_CONTENTS = "seed\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_repo(workspace_path: Path) -> Path:
|
|
36
|
+
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
empty_hooks_path = workspace_path / "empty-hooks"
|
|
38
|
+
empty_hooks_path.mkdir()
|
|
39
|
+
hooks_argument = ["-c", f"core.hooksPath={empty_hooks_path}"]
|
|
40
|
+
subprocess.run(
|
|
41
|
+
["git", *hooks_argument, "init", "-b", MAIN_BRANCH_NAME, str(workspace_path)],
|
|
42
|
+
check=True,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
)
|
|
46
|
+
for each_pair in (
|
|
47
|
+
("user.name", GIT_USER_NAME),
|
|
48
|
+
("user.email", GIT_USER_EMAIL),
|
|
49
|
+
("commit.gpgsign", "false"),
|
|
50
|
+
):
|
|
51
|
+
subprocess.run(
|
|
52
|
+
["git", *hooks_argument, "config", *each_pair],
|
|
53
|
+
cwd=str(workspace_path),
|
|
54
|
+
check=True,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
)
|
|
58
|
+
(workspace_path / SEED_FILE_NAME).write_text(SEED_FILE_CONTENTS, encoding="utf-8")
|
|
59
|
+
subprocess.run(
|
|
60
|
+
["git", *hooks_argument, "add", SEED_FILE_NAME],
|
|
61
|
+
cwd=str(workspace_path),
|
|
62
|
+
check=True,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
)
|
|
66
|
+
subprocess.run(
|
|
67
|
+
["git", *hooks_argument, "commit", "-m", "initial commit"],
|
|
68
|
+
cwd=str(workspace_path),
|
|
69
|
+
check=True,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
)
|
|
73
|
+
return workspace_path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_clone_with_origin(workspace_path: Path) -> tuple[Path, Path]:
|
|
77
|
+
seed_path = build_repo(workspace_path / "seed")
|
|
78
|
+
empty_hooks_path = seed_path / "empty-hooks"
|
|
79
|
+
hooks_argument = ["-c", f"core.hooksPath={empty_hooks_path}"]
|
|
80
|
+
bare_origin = workspace_path / "origin.git"
|
|
81
|
+
subprocess.run(
|
|
82
|
+
["git", *hooks_argument, "clone", "--bare", str(seed_path), str(bare_origin)],
|
|
83
|
+
check=True,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
)
|
|
87
|
+
clone_path = workspace_path / "clone"
|
|
88
|
+
subprocess.run(
|
|
89
|
+
["git", *hooks_argument, "clone", str(bare_origin), str(clone_path)],
|
|
90
|
+
check=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
)
|
|
94
|
+
for each_pair in (
|
|
95
|
+
("user.name", GIT_USER_NAME),
|
|
96
|
+
("user.email", GIT_USER_EMAIL),
|
|
97
|
+
("commit.gpgsign", "false"),
|
|
98
|
+
):
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["git", *hooks_argument, "config", *each_pair],
|
|
101
|
+
cwd=str(clone_path),
|
|
102
|
+
check=True,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
)
|
|
106
|
+
return clone_path, bare_origin
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def advance_origin_main(workspace_path: Path, bare_origin: Path) -> str:
|
|
110
|
+
pusher_path = workspace_path / "pusher"
|
|
111
|
+
empty_hooks_path = workspace_path / "seed" / "empty-hooks"
|
|
112
|
+
hooks_argument = ["-c", f"core.hooksPath={empty_hooks_path}"]
|
|
113
|
+
subprocess.run(
|
|
114
|
+
["git", *hooks_argument, "clone", str(bare_origin), str(pusher_path)],
|
|
115
|
+
check=True,
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
for each_pair in (
|
|
120
|
+
("user.name", GIT_USER_NAME),
|
|
121
|
+
("user.email", GIT_USER_EMAIL),
|
|
122
|
+
("commit.gpgsign", "false"),
|
|
123
|
+
):
|
|
124
|
+
subprocess.run(
|
|
125
|
+
["git", *hooks_argument, "config", *each_pair],
|
|
126
|
+
cwd=str(pusher_path),
|
|
127
|
+
check=True,
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
)
|
|
131
|
+
(pusher_path / SEED_FILE_NAME).write_text("advanced\n", encoding="utf-8")
|
|
132
|
+
subprocess.run(
|
|
133
|
+
["git", *hooks_argument, "commit", "-am", "advance main"],
|
|
134
|
+
cwd=str(pusher_path),
|
|
135
|
+
check=True,
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
)
|
|
139
|
+
subprocess.run(
|
|
140
|
+
["git", *hooks_argument, "push", "origin", MAIN_BRANCH_NAME],
|
|
141
|
+
cwd=str(pusher_path),
|
|
142
|
+
check=True,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
)
|
|
146
|
+
completed = subprocess.run(
|
|
147
|
+
["git", "rev-parse", "HEAD"],
|
|
148
|
+
cwd=str(pusher_path),
|
|
149
|
+
check=True,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
)
|
|
153
|
+
return completed.stdout.strip()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_branch_config(
|
|
157
|
+
repository_path: Path,
|
|
158
|
+
branch_name: str,
|
|
159
|
+
config_leaf: str,
|
|
160
|
+
) -> str:
|
|
161
|
+
completed = subprocess.run(
|
|
162
|
+
["git", "config", "--get", f"branch.{branch_name}.{config_leaf}"],
|
|
163
|
+
cwd=str(repository_path),
|
|
164
|
+
check=False,
|
|
165
|
+
capture_output=True,
|
|
166
|
+
text=True,
|
|
167
|
+
)
|
|
168
|
+
return completed.stdout.strip()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestBuildWorktreeAddArguments:
|
|
172
|
+
def test_pass_no_track_so_the_new_branch_gets_no_upstream(self) -> None:
|
|
173
|
+
all_arguments = build_worktree_add_arguments(
|
|
174
|
+
"fix/x",
|
|
175
|
+
Path("/tmp/agent/fix/x"),
|
|
176
|
+
"origin/main",
|
|
177
|
+
)
|
|
178
|
+
assert "--no-track" in all_arguments
|
|
179
|
+
|
|
180
|
+
def test_order_arguments_the_way_git_worktree_add_expects(self) -> None:
|
|
181
|
+
all_arguments = build_worktree_add_arguments(
|
|
182
|
+
"fix/x",
|
|
183
|
+
Path("/tmp/agent/fix/x"),
|
|
184
|
+
"origin/main",
|
|
185
|
+
)
|
|
186
|
+
assert all_arguments[:4] == ["worktree", "add", "-b", "fix/x"]
|
|
187
|
+
assert all_arguments[-1] == "origin/main"
|
|
188
|
+
assert all_arguments[-2] == str(Path("/tmp/agent/fix/x"))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestSplitRemoteRef:
|
|
192
|
+
def test_split_an_origin_ref(self) -> None:
|
|
193
|
+
assert split_remote_ref("origin/main") == ("origin", "main")
|
|
194
|
+
|
|
195
|
+
def test_split_a_non_origin_remote_ref(self) -> None:
|
|
196
|
+
assert split_remote_ref("upstream/dev") == ("upstream", "dev")
|
|
197
|
+
|
|
198
|
+
def test_default_a_bare_name_to_origin(self) -> None:
|
|
199
|
+
assert split_remote_ref("main") == ("origin", "main")
|
|
200
|
+
|
|
201
|
+
def test_keep_slashes_inside_the_branch_name(self) -> None:
|
|
202
|
+
assert split_remote_ref("origin/fix/nested") == ("origin", "fix/nested")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestAssertGitAcceptsBranchName:
|
|
206
|
+
def test_accept_a_conventional_branch_name(self) -> None:
|
|
207
|
+
assert assert_git_accepts_branch_name("fix/example-one") is None
|
|
208
|
+
|
|
209
|
+
def test_reject_a_name_git_refuses(self) -> None:
|
|
210
|
+
with pytest.raises(ValueError, match="relative path"):
|
|
211
|
+
assert_git_accepts_branch_name("fix..example")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TestResolveRepoRoot:
|
|
215
|
+
def test_return_the_toplevel_for_a_nested_path(self, tmp_path: Path) -> None:
|
|
216
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
217
|
+
nested_path = repository_path / "nested"
|
|
218
|
+
nested_path.mkdir()
|
|
219
|
+
assert resolve_repo_root(nested_path).resolve() == repository_path.resolve()
|
|
220
|
+
|
|
221
|
+
def test_raise_outside_a_repository(self, tmp_path: Path) -> None:
|
|
222
|
+
plain_directory = tmp_path / "plain"
|
|
223
|
+
plain_directory.mkdir()
|
|
224
|
+
with pytest.raises(RuntimeError, match="git repository"):
|
|
225
|
+
resolve_repo_root(plain_directory)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class TestIsRefPresent:
|
|
229
|
+
def test_find_a_local_branch(self, tmp_path: Path) -> None:
|
|
230
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
231
|
+
assert is_ref_present(repository_path, MAIN_BRANCH_NAME) is True
|
|
232
|
+
|
|
233
|
+
def test_not_find_an_absent_ref(self, tmp_path: Path) -> None:
|
|
234
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
235
|
+
assert is_ref_present(repository_path, "origin/nowhere") is False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class TestReadFailureText:
|
|
239
|
+
def test_prefer_stderr(self, tmp_path: Path) -> None:
|
|
240
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
241
|
+
completed = run_git(["rev-parse", "does-not-exist"], repository_path)
|
|
242
|
+
assert completed.returncode != 0
|
|
243
|
+
assert "does-not-exist" in read_failure_text(completed)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class TestFetchBaseRef:
|
|
247
|
+
def test_bring_the_remote_tip_into_the_clone(self, tmp_path: Path) -> None:
|
|
248
|
+
clone_path, bare_origin = build_clone_with_origin(tmp_path / "workspace")
|
|
249
|
+
advanced_commit = advance_origin_main(tmp_path / "workspace", bare_origin)
|
|
250
|
+
fetch_base_ref(clone_path, "origin/main")
|
|
251
|
+
assert is_ref_present(clone_path, "origin/main") is True
|
|
252
|
+
assert resolve_base_commit(clone_path, "origin/main") == advanced_commit
|
|
253
|
+
|
|
254
|
+
def test_raise_for_a_remote_branch_that_does_not_exist(
|
|
255
|
+
self,
|
|
256
|
+
tmp_path: Path,
|
|
257
|
+
) -> None:
|
|
258
|
+
clone_path, _bare_origin = build_clone_with_origin(tmp_path / "workspace")
|
|
259
|
+
with pytest.raises(RuntimeError, match="git fetch failed"):
|
|
260
|
+
fetch_base_ref(clone_path, "origin/nowhere")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestResolveBaseCommit:
|
|
264
|
+
def test_return_the_sha_at_the_ref(self, tmp_path: Path) -> None:
|
|
265
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
266
|
+
head_commit = run_git(["rev-parse", "HEAD"], repository_path).stdout.strip()
|
|
267
|
+
assert resolve_base_commit(repository_path, MAIN_BRANCH_NAME) == head_commit
|
|
268
|
+
|
|
269
|
+
def test_raise_for_an_unresolvable_ref(self, tmp_path: Path) -> None:
|
|
270
|
+
repository_path = build_repo(tmp_path / "repo")
|
|
271
|
+
with pytest.raises(RuntimeError, match="could not resolve base commit"):
|
|
272
|
+
resolve_base_commit(repository_path, "refs/heads/nowhere")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TestCreateWorktreeBranch:
|
|
276
|
+
def test_create_the_branch_at_the_base_ref_with_no_upstream(
|
|
277
|
+
self,
|
|
278
|
+
tmp_path: Path,
|
|
279
|
+
) -> None:
|
|
280
|
+
clone_path, _bare_origin = build_clone_with_origin(tmp_path / "workspace")
|
|
281
|
+
origin_tip = resolve_base_commit(clone_path, "origin/main")
|
|
282
|
+
worktree_path = tmp_path / "worktrees" / "fix" / "created"
|
|
283
|
+
create_worktree_branch(
|
|
284
|
+
clone_path,
|
|
285
|
+
branch_name="fix/created",
|
|
286
|
+
worktree_path=worktree_path,
|
|
287
|
+
base_ref="origin/main",
|
|
288
|
+
)
|
|
289
|
+
assert worktree_path.is_dir()
|
|
290
|
+
assert resolve_base_commit(worktree_path, "HEAD") == origin_tip
|
|
291
|
+
assert read_branch_config(clone_path, "fix/created", "merge") == ""
|
|
292
|
+
assert read_branch_config(clone_path, "fix/created", "remote") == ""
|
|
293
|
+
|
|
294
|
+
def test_raise_when_the_branch_already_exists(self, tmp_path: Path) -> None:
|
|
295
|
+
clone_path, _bare_origin = build_clone_with_origin(tmp_path / "workspace")
|
|
296
|
+
first_path = tmp_path / "worktrees" / "dup-one"
|
|
297
|
+
second_path = tmp_path / "worktrees" / "dup-two"
|
|
298
|
+
create_worktree_branch(
|
|
299
|
+
clone_path,
|
|
300
|
+
branch_name="fix/dup",
|
|
301
|
+
worktree_path=first_path,
|
|
302
|
+
base_ref="origin/main",
|
|
303
|
+
)
|
|
304
|
+
with pytest.raises(RuntimeError, match="worktree add failed"):
|
|
305
|
+
create_worktree_branch(
|
|
306
|
+
clone_path,
|
|
307
|
+
branch_name="fix/dup",
|
|
308
|
+
worktree_path=second_path,
|
|
309
|
+
base_ref="origin/main",
|
|
310
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: issue-tracker
|
|
3
|
+
description: >-
|
|
4
|
+
File, update, and close GitHub work as one epic with native sub-issues.
|
|
5
|
+
Dedup open and closed issues first. Edit status only inside marker sections.
|
|
6
|
+
Refresh the epic checklist so it matches its children. Triggers: issue tracker,
|
|
7
|
+
file an issue, track this issue, open an epic, update the epic, close the issue,
|
|
8
|
+
refresh the epic checklist, attach a sub-issue.
|
|
9
|
+
argument-hint: "[issue action - file | update | close | refresh-epic | full handoff]"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Issue tracker
|
|
13
|
+
|
|
14
|
+
**User wants an issue action done. Here is how those actions work.**
|
|
15
|
+
|
|
16
|
+
**dedup -> do the work -> markers only -> numbers + URLs**
|
|
17
|
+
|
|
18
|
+
Run every step the ask needs in one go (file, label, attach, refresh, and so on). The `issue-tracker` agent is the primary handler for one action per turn; this skill is the session path when the agent is unavailable or the ask spans several steps.
|
|
19
|
+
|
|
20
|
+
Prefer the **same warm** `issue-tracker` agent for follow-ups on the same issue or a related issue on the same epic. Spawn a new agent only for an unrelated work-stream or when the warm agent is gone.
|
|
21
|
+
|
|
22
|
+
## Voice
|
|
23
|
+
|
|
24
|
+
Use the `plain-brief` output style (`output-styles/plain-brief.md`). End with the issue number(s) and URL(s) the caller must keep.
|
|
25
|
+
|
|
26
|
+
## Gotchas
|
|
27
|
+
|
|
28
|
+
Append a bullet when an action fails in a new way.
|
|
29
|
+
|
|
30
|
+
- Use REST `.id`, not display `#N`, to attach a sub-issue - [operation-matrix](reference/operation-matrix.md).
|
|
31
|
+
- `gh` attach: `-F sub_issue_id=<n>` (typed int), never `-f`.
|
|
32
|
+
- Status goes in markers, not comments - comments are cross-links only.
|
|
33
|
+
- Dedup open **and** closed; closed twin -> reopen vs file new, never silent refile.
|
|
34
|
+
- Edit only between markers - free-form epic edits break the children checklist.
|
|
35
|
+
- Auto-close is PR-body-first: a related-only UI link does not close the issue on merge.
|
|
36
|
+
|
|
37
|
+
## The model
|
|
38
|
+
|
|
39
|
+
One **epic** issue owns a work-stream. Each unit of work is a native GitHub **sub-issue** under it. The epic checklist and every status block live inside marker pairs. Skeletons and refresh steps: [reference/epic-and-sub-issue-model.md](reference/epic-and-sub-issue-model.md).
|
|
40
|
+
|
|
41
|
+
## Labels
|
|
42
|
+
|
|
43
|
+
- Parent: `epic`.
|
|
44
|
+
- Sub-issue: `type: roadblock`, `type: task`, `type: bug`, or `type: enhancement`.
|
|
45
|
+
|
|
46
|
+
Create a missing label, then apply it. Label create is in the operation matrix.
|
|
47
|
+
|
|
48
|
+
## Markers
|
|
49
|
+
|
|
50
|
+
- Status (any issue): `<!-- issue-tracker:status -->` … `<!-- /issue-tracker:status -->`
|
|
51
|
+
- Children checklist (epic only): `<!-- issue-tracker:children -->` … `<!-- /issue-tracker:children -->`
|
|
52
|
+
|
|
53
|
+
Update path: read body -> replace only the text between the target pair -> write the full body back.
|
|
54
|
+
|
|
55
|
+
## Dedup
|
|
56
|
+
|
|
57
|
+
Search open and closed issues on the target repo before create. Open twin -> update in place. Closed twin -> ask reopen vs file new.
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
Prefer GitHub MCP. Fall back to `gh` on the same REST endpoints. Full map and `.id` rule: [reference/operation-matrix.md](reference/operation-matrix.md).
|
|
62
|
+
|
|
63
|
+
## Handoff input
|
|
64
|
+
|
|
65
|
+
Optional issue-candidate JSON from orchestrator or closeout. Full schema and consume path: [reference/handoff-schema.md](reference/handoff-schema.md).
|
|
66
|
+
|
|
67
|
+
## Close the sub-issue through the PR (required when a fix ships)
|
|
68
|
+
|
|
69
|
+
GitHub closes the sub-issue when someone **merges** a pull request into the **default branch** and that PR carries a closing keyword for the issue.
|
|
70
|
+
|
|
71
|
+
**Required on the PR that merges into the default branch:** put `Closes #N` in the PR body for each finished sub-issue. One keyword per issue: write `Closes #12` and `Closes #13`, not `Closes #12, #13` (only the first number closes in the comma form).
|
|
72
|
+
|
|
73
|
+
**Preferred on commits:** also put `Closes #N` in the first commit message when the fix starts. Commit alone is backup; the PR body is the contract.
|
|
74
|
+
|
|
75
|
+
**Stacked / intermediate PRs** that do not merge into the default branch: use a plain `#N` reference in the body so the issue links without a false close promise. Put `Closes #N` only on the PR that actually lands on the default branch.
|
|
76
|
+
|
|
77
|
+
**Not enough:** a Development "related" link with no closing keyword. **Not enough:** closing the PR without merging.
|
|
78
|
+
|
|
79
|
+
**Who owns `#N`:** the agent or session writing the PR must use the correct sub-issue number. Nothing in this package verifies the number matches the fix.
|
|
80
|
+
|
|
81
|
+
## Return shape
|
|
82
|
+
|
|
83
|
+
Reply with every affected issue number and URL - no narration after that payload.
|
|
84
|
+
|
|
85
|
+
## File index
|
|
86
|
+
|
|
87
|
+
| File | Purpose |
|
|
88
|
+
|------|---------|
|
|
89
|
+
| `SKILL.md` | Hub: how issue actions work, gotchas, model, markers, dedup, PR auto-close, return shape |
|
|
90
|
+
| `reference/operation-matrix.md` | MCP + `gh` per action; sub-issue `.id` rule |
|
|
91
|
+
| `reference/epic-and-sub-issue-model.md` | Epic model, markers, body skeleton, checklist refresh |
|
|
92
|
+
| `reference/handoff-schema.md` | Issue-candidate JSON and consume flow |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Epic and sub-issue model
|
|
2
|
+
|
|
3
|
+
One **epic** issue holds a whole work-stream. Each unit of work under it is a native GitHub **sub-issue**. The epic carries the `epic` label; each sub-issue carries a `type:` label. The epic body mirrors its children as a checklist, and every issue — epic and sub-issue alike — carries a status section. Both sections live between marker comments so a body edit can replace one section and leave the rest untouched.
|
|
4
|
+
|
|
5
|
+
## The two marker pairs
|
|
6
|
+
|
|
7
|
+
- `<!-- issue-tracker:status -->` … `<!-- /issue-tracker:status -->` — on every issue. Holds the current status line.
|
|
8
|
+
- `<!-- issue-tracker:children -->` … `<!-- /issue-tracker:children -->` — on the epic only. Holds the checklist of sub-issues.
|
|
9
|
+
|
|
10
|
+
An edit reads the whole body, swaps only the text between one marker pair, and writes the whole body back. Text outside the markers stays byte-for-byte the same.
|
|
11
|
+
|
|
12
|
+
## Epic body skeleton
|
|
13
|
+
|
|
14
|
+
```markdown
|
|
15
|
+
## Epic — <work-stream label>
|
|
16
|
+
|
|
17
|
+
<One sentence naming the work-stream and its goal.>
|
|
18
|
+
|
|
19
|
+
<!-- issue-tracker:status -->
|
|
20
|
+
Status: in progress — 1 of 3 children closed.
|
|
21
|
+
<!-- /issue-tracker:status -->
|
|
22
|
+
|
|
23
|
+
<!-- issue-tracker:children -->
|
|
24
|
+
- [x] owner/repo#12 — parser rejects an empty path
|
|
25
|
+
- [ ] owner/repo#13 — attach reads the display number
|
|
26
|
+
- [ ] owner/repo#14 — label create skips an existing label
|
|
27
|
+
<!-- /issue-tracker:children -->
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Sub-issue body skeleton
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
## <sub-issue title>
|
|
34
|
+
|
|
35
|
+
<One sentence: what the work is.>
|
|
36
|
+
|
|
37
|
+
<!-- issue-tracker:status -->
|
|
38
|
+
Status: open.
|
|
39
|
+
<!-- /issue-tracker:status -->
|
|
40
|
+
|
|
41
|
+
## Detail
|
|
42
|
+
|
|
43
|
+
<The evidence, where, impact, and proposed fix — self-contained.>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Refresh the epic checklist
|
|
47
|
+
|
|
48
|
+
After you create a sub-issue or close one, refresh the epic's `children` section so it matches the children:
|
|
49
|
+
|
|
50
|
+
1. Read the epic body.
|
|
51
|
+
2. Add a `- [ ] owner/repo#<N> — <title>` line for a newly created sub-issue, or flip a line to `- [x]` for a closed one.
|
|
52
|
+
3. Update the epic's status line to name the closed count against the total.
|
|
53
|
+
4. Write the whole body back with the issue-update op, replacing only the two marker sections you touched.
|
|
54
|
+
|
|
55
|
+
A created or closed sub-issue always triggers this refresh, so the epic checklist stays in step with the sub-issues at every step.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Handoff schema
|
|
2
|
+
|
|
3
|
+
An orchestrator or the closeout skill hands the tracker one **issue-candidate** JSON record per obstacle. The tracker consumes each record through the full path and returns the issue number and URL.
|
|
4
|
+
|
|
5
|
+
## The record
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"kind": "roadblock | task | bug | enhancement",
|
|
10
|
+
"title": "<short imperative title>",
|
|
11
|
+
"epic": "<work-stream label the sub-issue belongs under>",
|
|
12
|
+
"summary": "<one sentence a reader with zero session context acts on>",
|
|
13
|
+
"evidence": "<verbatim line captured this session — error text, command, or log line>",
|
|
14
|
+
"where": "<file, hook, gate, or tool the evidence names — path relative to the repo root>",
|
|
15
|
+
"impact": "<what the obstacle cost: work blocked, count of times hit, workaround forced>",
|
|
16
|
+
"proposed_fix": "<the specific change — name the failure mode and the condition>",
|
|
17
|
+
"blocking": true
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Field meanings
|
|
22
|
+
|
|
23
|
+
| Field | Meaning |
|
|
24
|
+
|-------|---------|
|
|
25
|
+
| `kind` | The sub-issue's `type:` label stem. `roadblock`, `task`, `bug`, or `enhancement`. |
|
|
26
|
+
| `title` | The sub-issue title. Short and imperative. |
|
|
27
|
+
| `epic` | The work-stream the sub-issue belongs under. The tracker finds or creates the matching epic. |
|
|
28
|
+
| `summary` | The one-sentence body opener. Self-contained — a reader with no session context acts on it. |
|
|
29
|
+
| `evidence` | A verbatim line captured this session, placed in a fenced block in the body. |
|
|
30
|
+
| `where` | The file, hook, gate, or tool the evidence names. A repo-relative path, no volatile temp path. |
|
|
31
|
+
| `impact` | What the obstacle cost. |
|
|
32
|
+
| `proposed_fix` | The specific change. |
|
|
33
|
+
| `blocking` | `true` routes the sub-issue to `type: roadblock`, overriding `kind`. `false` keeps the `kind` label. |
|
|
34
|
+
|
|
35
|
+
`kind` maps to the `type:` label. `blocking: true` takes precedence and routes the sub-issue to `type: roadblock`.
|
|
36
|
+
|
|
37
|
+
## Filled example
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"kind": "bug",
|
|
42
|
+
"title": "Inline-collection gate fires in exempt test files",
|
|
43
|
+
"epic": "hook exemptions for test files",
|
|
44
|
+
"summary": "The code_rules_enforcer inline-collection check blocks a valid list literal in a test file, where test files are exempt from the magic-value gate.",
|
|
45
|
+
"evidence": "BLOCKED: [MAGIC_VALUE] Inline list literal [200, 404, 500] in a function body -- extract to a named constant in config/.",
|
|
46
|
+
"where": "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py",
|
|
47
|
+
"impact": "Hit 3 times in one session on three test files; each forced a workaround move to a module constant.",
|
|
48
|
+
"proposed_fix": "Extend the magic-value test-file exemption to cover the inline-collection check, so list and set literals in test bodies pass.",
|
|
49
|
+
"blocking": false
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Consume flow
|
|
54
|
+
|
|
55
|
+
For each record, in order:
|
|
56
|
+
|
|
57
|
+
1. **Dedup** — search open and closed issues on the target repository for a twin.
|
|
58
|
+
2. **Find or create the epic** — match the `epic` label; open a parent with the `epic` label when none matches.
|
|
59
|
+
3. **Create the sub-issue** — body from `summary`, `evidence`, `where`, `impact`, `proposed_fix`.
|
|
60
|
+
4. **Labels** — apply the `type:` label from `kind`, or `type: roadblock` when `blocking` is `true`. Create the label when the repository lacks it.
|
|
61
|
+
5. **Attach the native sub-issue** — read the child's REST database `.id` and attach it under the epic.
|
|
62
|
+
6. **Refresh the epic checklist** — add the child's `- [ ] owner/repo#<N>` line and update the epic status.
|
|
63
|
+
|
|
64
|
+
The return is the sub-issue number and URL (and the epic number and URL when the epic was created this call).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Operation matrix
|
|
2
|
+
|
|
3
|
+
Each issue op maps to a GitHub MCP tool and a `gh` fallback that reaches the same REST endpoint. Prefer the MCP tool; fall back to `gh` when the MCP server is unreachable. The REST endpoint column names the authoritative contract both surfaces call, so a session with neither surface can still drive the raw API.
|
|
4
|
+
|
|
5
|
+
The MCP tool names below match the connected GitHub MCP server's issue toolset. When a server exposes a tool under a different name, pick the tool by what the row describes, and the REST endpoint tells you what it must call.
|
|
6
|
+
|
|
7
|
+
| Operation | GitHub MCP tool | REST endpoint | `gh` fallback |
|
|
8
|
+
|-----------|-----------------|---------------|---------------|
|
|
9
|
+
| Search open + closed | `mcp__github__search_issues` | `GET /search/issues?q=<terms>+repo:{owner}/{repo}` | `gh issue list --search "<terms>" --state all` |
|
|
10
|
+
| Create epic / sub-issue | `mcp__github__issue_write` (create) | `POST /repos/{owner}/{repo}/issues` | `gh issue create --title "<t>" --body-file <path> --label <label>` |
|
|
11
|
+
| Read issue body | `mcp__github__issue_read` | `GET /repos/{owner}/{repo}/issues/{number}` | `gh issue view <number> --json body` |
|
|
12
|
+
| Update body / labels | `mcp__github__issue_write` (update) | `PATCH /repos/{owner}/{repo}/issues/{number}` | `gh issue edit <number> --body-file <path> --add-label <label>` |
|
|
13
|
+
| Attach native sub-issue | `mcp__github__sub_issue_write` | `POST /repos/{owner}/{repo}/issues/{number}/sub_issues` | `gh api repos/{owner}/{repo}/issues/{number}/sub_issues -F sub_issue_id=<int>` |
|
|
14
|
+
| Add cross-reference comment | `mcp__github__add_issue_comment` | `POST /repos/{owner}/{repo}/issues/{number}/comments` | `gh issue comment <number> --body-file <path>` |
|
|
15
|
+
| Create label | `mcp__github__` label-create tool | `POST /repos/{owner}/{repo}/labels` | `gh label create <name> --color <hex> --description "<text>"` |
|
|
16
|
+
|
|
17
|
+
## The sub-issue `.id` rule (read this before every attach)
|
|
18
|
+
|
|
19
|
+
The sub-issues endpoint identifies the child by its REST **database `.id`** — a large integer such as `2138472019` — not by its display number (`#42`). The two are different values. Passing the display number attaches nothing.
|
|
20
|
+
|
|
21
|
+
Read the child's `.id` from the create op's response, or fetch it:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
gh api repos/{owner}/{repo}/issues/{number} --jq .id
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`gh issue view <number> --json id` returns the GraphQL node id, a base64 string — the wrong value for this endpoint. Use `gh api ... --jq .id` for the REST database id.
|
|
28
|
+
|
|
29
|
+
The gh attach uses `-F` (typed field), which sends the id as a JSON integer. `-f` sends a string, and the endpoint rejects a string sub-issue id.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
gh api repos/{owner}/{repo}/issues/{parent}/sub_issues -F sub_issue_id=2138472019
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Body content through `--body-file`
|
|
36
|
+
|
|
37
|
+
Every `gh issue create`, `gh issue edit`, and `gh issue comment` passes body content with `--body-file <path>`, never `--body "<text>"`. A `--body` string mangles backticks on GitHub — they land as literal `\``. Write the body to a temp file and point `--body-file` at it.
|
|
38
|
+
|
|
39
|
+
## Paginated reads
|
|
40
|
+
|
|
41
|
+
`gh issue list` needs no pagination flags. For a `gh api` read of a paginated list endpoint (an issue's comments, a repository's issues), pass `--paginate --slurp` and pipe to external `jq` — `gh`'s built-in `--jq` runs per page and gives wrong cross-page results.
|