claude-dev-env 2.11.0 → 2.13.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/_shared/advisor/reference/sol-rung.md +5 -2
- package/_shared/advisor/reference/warm-up.md +1 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/CLAUDE.md +1 -0
- package/commands/sr-loop.md +52 -0
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/skills/usage-pause/SKILL.md +1 -1
- package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""Contract: code-verifier cannot message a warm session-advisor."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
_PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
6
|
-
_CODE_VERIFIER_AGENT_PATH = _PACKAGE_ROOT / "agents" / "code-verifier.md"
|
|
7
|
-
_FRONTMATTER_OPEN = "---\n"
|
|
8
|
-
_FRONTMATTER_CLOSE = "\n---\n"
|
|
9
|
-
_TOOLS_PREFIX = "tools:"
|
|
10
|
-
_SEND_MESSAGE_TOOL_NAME = "SendMessage"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_code_verifier_tools_exclude_sendmessage() -> None:
|
|
14
|
-
"""tools frontmatter must not list SendMessage (warm-advisor path closed)."""
|
|
15
|
-
agent_text = _CODE_VERIFIER_AGENT_PATH.read_text(encoding="utf-8")
|
|
16
|
-
assert agent_text.startswith(_FRONTMATTER_OPEN), "missing frontmatter open"
|
|
17
|
-
frontmatter_end = agent_text.find(_FRONTMATTER_CLOSE, len(_FRONTMATTER_OPEN))
|
|
18
|
-
assert frontmatter_end > 0, "missing frontmatter close"
|
|
19
|
-
frontmatter_body = agent_text[len(_FRONTMATTER_OPEN) : frontmatter_end]
|
|
20
|
-
all_tools_lines = [
|
|
21
|
-
each_line
|
|
22
|
-
for each_line in frontmatter_body.splitlines()
|
|
23
|
-
if each_line.startswith(_TOOLS_PREFIX)
|
|
24
|
-
]
|
|
25
|
-
assert len(all_tools_lines) == 1, all_tools_lines
|
|
26
|
-
tools_value = all_tools_lines[0].removeprefix(_TOOLS_PREFIX).strip()
|
|
27
|
-
all_tool_names = {each_name.strip() for each_name in tools_value.split(",")}
|
|
28
|
-
assert _SEND_MESSAGE_TOOL_NAME not in all_tool_names, all_tool_names
|
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
"""Unit tests for convergence-gate-blocker PreToolUse hook."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import pathlib
|
|
7
|
-
import re
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
14
|
-
if str(_HOOK_DIR) not in sys.path:
|
|
15
|
-
sys.path.insert(0, str(_HOOK_DIR))
|
|
16
|
-
|
|
17
|
-
_GH_PR_READY_PATTERN = re.compile(r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)")
|
|
18
|
-
|
|
19
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
20
|
-
"convergence_gate_blocker",
|
|
21
|
-
_HOOK_DIR / "convergence_gate_blocker.py",
|
|
22
|
-
)
|
|
23
|
-
assert hook_spec is not None
|
|
24
|
-
assert hook_spec.loader is not None
|
|
25
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
26
|
-
hook_spec.loader.exec_module(hook_module)
|
|
27
|
-
_resolve_pr_number = hook_module._resolve_pr_number
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_matches_gh_pr_ready_with_number() -> None:
|
|
31
|
-
assert _GH_PR_READY_PATTERN.search("gh pr ready 418")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_matches_gh_pr_ready_without_number() -> None:
|
|
35
|
-
assert _GH_PR_READY_PATTERN.search("gh pr ready")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_matches_gh_pr_ready_with_flags() -> None:
|
|
39
|
-
assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_does_not_match_gh_pr_create() -> None:
|
|
43
|
-
assert not _GH_PR_READY_PATTERN.search("gh pr create --title T")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_does_not_match_gh_pr_view() -> None:
|
|
47
|
-
assert not _GH_PR_READY_PATTERN.search("gh pr view 418")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_does_not_match_gh_issue_close() -> None:
|
|
51
|
-
assert not _GH_PR_READY_PATTERN.search("gh issue close 42")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_extracts_pr_number_from_command() -> None:
|
|
55
|
-
assert _resolve_pr_number("gh pr ready 418", None, None, None) == 418
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def test_extracts_pr_number_when_repo_flag_precedes_number() -> None:
|
|
59
|
-
assert (
|
|
60
|
-
_resolve_pr_number(
|
|
61
|
-
"gh pr ready --repo sample-owner/target-repo 161",
|
|
62
|
-
None,
|
|
63
|
-
"sample-owner",
|
|
64
|
-
"target-repo",
|
|
65
|
-
)
|
|
66
|
-
== 161
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_extracts_pr_number_with_flags() -> None:
|
|
71
|
-
assert _resolve_pr_number("gh pr ready 99 --undo", None, None, None) == 99
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_returns_none_when_no_number_and_no_repo() -> None:
|
|
75
|
-
assert _resolve_pr_number("gh pr ready", "/nonexistent/path", None, None) is None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_number_resolution_binds_gh_view_to_named_repo(
|
|
79
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
80
|
-
) -> None:
|
|
81
|
-
captured_commands: list[list[str]] = []
|
|
82
|
-
|
|
83
|
-
def fake_run(all_arguments: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
84
|
-
captured_commands.append(all_arguments)
|
|
85
|
-
return subprocess.CompletedProcess(args=all_arguments, returncode=0, stdout="500\n", stderr="")
|
|
86
|
-
|
|
87
|
-
monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
|
|
88
|
-
assert _resolve_pr_number("gh pr ready", None, "flag-owner", "flag-repo") == 500
|
|
89
|
-
assert captured_commands
|
|
90
|
-
for each_command in captured_commands:
|
|
91
|
-
assert "--repo" in each_command
|
|
92
|
-
assert "flag-owner/flag-repo" in each_command
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def test_matches_gh_pr_ready_in_compound_command() -> None:
|
|
96
|
-
assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo && gh pr create")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_run_convergence_check_forwards_cwd_to_subprocess(
|
|
100
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
101
|
-
) -> None:
|
|
102
|
-
captured_cwd: list[object] = []
|
|
103
|
-
|
|
104
|
-
def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
|
|
105
|
-
captured_cwd.append(run_keywords.get("cwd"))
|
|
106
|
-
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
107
|
-
|
|
108
|
-
monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
|
|
109
|
-
hook_module._run_convergence_check(
|
|
110
|
-
"check_convergence.py", "owner", "repo", 783, "C:/worktrees/pr-783"
|
|
111
|
-
)
|
|
112
|
-
assert captured_cwd == ["C:/worktrees/pr-783"]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def test_run_convergence_check_forwards_none_cwd_as_none(
|
|
116
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
117
|
-
) -> None:
|
|
118
|
-
captured_cwd: list[object] = []
|
|
119
|
-
|
|
120
|
-
def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
|
|
121
|
-
captured_cwd.append(run_keywords.get("cwd"))
|
|
122
|
-
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
123
|
-
|
|
124
|
-
monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
|
|
125
|
-
hook_module._run_convergence_check("check_convergence.py", "owner", "repo", 783, None)
|
|
126
|
-
assert captured_cwd == [None]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def test_main_reads_cwd_from_top_level_payload(
|
|
130
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
131
|
-
) -> None:
|
|
132
|
-
convergence_script = (
|
|
133
|
-
tmp_path / ".claude" / "skills" / "pr-converge" / "scripts" / "check_convergence.py"
|
|
134
|
-
)
|
|
135
|
-
convergence_script.parent.mkdir(parents=True)
|
|
136
|
-
convergence_script.write_text("")
|
|
137
|
-
monkeypatch.setattr(hook_module.Path, "home", classmethod(lambda _cls: tmp_path))
|
|
138
|
-
|
|
139
|
-
worktree_path = str(tmp_path / "worktrees" / "pr-783")
|
|
140
|
-
monkeypatch.setattr(hook_module, "_resolve_owner_repo", lambda _cwd: ("jl-cmd", "repo"))
|
|
141
|
-
|
|
142
|
-
captured_cwd: list[object] = []
|
|
143
|
-
|
|
144
|
-
def fake_check(
|
|
145
|
-
_script: str, _owner: str, _repo: str, _pr_number: int, cwd: str | None
|
|
146
|
-
) -> subprocess.CompletedProcess[str]:
|
|
147
|
-
captured_cwd.append(cwd)
|
|
148
|
-
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
149
|
-
|
|
150
|
-
monkeypatch.setattr(hook_module, "_run_convergence_check", fake_check)
|
|
151
|
-
|
|
152
|
-
payload = {
|
|
153
|
-
"tool_name": "Bash",
|
|
154
|
-
"cwd": worktree_path,
|
|
155
|
-
"tool_input": {"command": "gh pr ready 783", "cwd": "C:/wrong/session/dir"},
|
|
156
|
-
}
|
|
157
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
158
|
-
|
|
159
|
-
with pytest.raises(SystemExit):
|
|
160
|
-
hook_module.main()
|
|
161
|
-
|
|
162
|
-
assert captured_cwd == [worktree_path]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _capture_convergence_identity(
|
|
166
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
167
|
-
tmp_path: pathlib.Path,
|
|
168
|
-
command: str,
|
|
169
|
-
cwd_owner_repo: tuple[str, str],
|
|
170
|
-
) -> tuple[str, str, int]:
|
|
171
|
-
convergence_script = (
|
|
172
|
-
tmp_path / ".claude" / "skills" / "pr-converge" / "scripts" / "check_convergence.py"
|
|
173
|
-
)
|
|
174
|
-
convergence_script.parent.mkdir(parents=True)
|
|
175
|
-
convergence_script.write_text("")
|
|
176
|
-
monkeypatch.setattr(hook_module.Path, "home", classmethod(lambda _cls: tmp_path))
|
|
177
|
-
monkeypatch.setattr(hook_module, "_resolve_owner_repo", lambda _cwd: cwd_owner_repo)
|
|
178
|
-
|
|
179
|
-
captured_identity: list[tuple[str, str, int]] = []
|
|
180
|
-
|
|
181
|
-
def fake_check(
|
|
182
|
-
_script: str, owner: str, repo: str, pr_number: int, _cwd: str | None
|
|
183
|
-
) -> subprocess.CompletedProcess[str]:
|
|
184
|
-
captured_identity.append((owner, repo, pr_number))
|
|
185
|
-
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
186
|
-
|
|
187
|
-
monkeypatch.setattr(hook_module, "_run_convergence_check", fake_check)
|
|
188
|
-
|
|
189
|
-
payload = {
|
|
190
|
-
"tool_name": "Bash",
|
|
191
|
-
"cwd": str(tmp_path / "worktree"),
|
|
192
|
-
"tool_input": {"command": command},
|
|
193
|
-
}
|
|
194
|
-
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
195
|
-
with pytest.raises(SystemExit):
|
|
196
|
-
hook_module.main()
|
|
197
|
-
return captured_identity[0]
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def test_repo_flag_overrides_cwd_repo(
|
|
201
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
202
|
-
) -> None:
|
|
203
|
-
identity = _capture_convergence_identity(
|
|
204
|
-
monkeypatch,
|
|
205
|
-
tmp_path,
|
|
206
|
-
"gh pr ready 161 --repo sample-owner/target-repo",
|
|
207
|
-
("cwd-owner", "cwd-repo"),
|
|
208
|
-
)
|
|
209
|
-
assert identity == ("sample-owner", "target-repo", 161)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def test_pr_url_yields_repo_and_number(
|
|
213
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
214
|
-
) -> None:
|
|
215
|
-
identity = _capture_convergence_identity(
|
|
216
|
-
monkeypatch,
|
|
217
|
-
tmp_path,
|
|
218
|
-
"gh pr ready https://github.com/sample-owner/target-repo/pull/161",
|
|
219
|
-
("cwd-owner", "cwd-repo"),
|
|
220
|
-
)
|
|
221
|
-
assert identity == ("sample-owner", "target-repo", 161)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def test_bare_pr_ready_resolves_repo_from_cwd(
|
|
225
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
226
|
-
) -> None:
|
|
227
|
-
identity = _capture_convergence_identity(
|
|
228
|
-
monkeypatch,
|
|
229
|
-
tmp_path,
|
|
230
|
-
"gh pr ready 418",
|
|
231
|
-
("cwd-owner", "cwd-repo"),
|
|
232
|
-
)
|
|
233
|
-
assert identity == ("cwd-owner", "cwd-repo", 418)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def test_parse_repo_flag_space_form() -> None:
|
|
237
|
-
assert hook_module._parse_repo_flag("gh pr ready 161 --repo sample-owner/target-repo") == (
|
|
238
|
-
"sample-owner",
|
|
239
|
-
"target-repo",
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def test_parse_repo_flag_equals_form() -> None:
|
|
244
|
-
assert hook_module._parse_repo_flag("gh pr ready 161 --repo=sample-owner/target-repo") == (
|
|
245
|
-
"sample-owner",
|
|
246
|
-
"target-repo",
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def test_parse_repo_flag_short_alias() -> None:
|
|
251
|
-
assert hook_module._parse_repo_flag("gh pr ready 161 -R sample-owner/target-repo") == (
|
|
252
|
-
"sample-owner",
|
|
253
|
-
"target-repo",
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def test_parse_repo_flag_short_alias_attached_value() -> None:
|
|
258
|
-
assert hook_module._parse_repo_flag("gh pr ready 161 -Rsample-owner/target-repo") == (
|
|
259
|
-
"sample-owner",
|
|
260
|
-
"target-repo",
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def test_parse_repo_flag_absent_returns_none() -> None:
|
|
265
|
-
assert hook_module._parse_repo_flag("gh pr ready 418") is None
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def test_parse_pr_url_yields_owner_repo_number() -> None:
|
|
269
|
-
assert hook_module._parse_pr_url(
|
|
270
|
-
"gh pr ready https://github.com/sample-owner/target-repo/pull/161"
|
|
271
|
-
) == ("sample-owner", "target-repo", 161)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def test_parse_pr_url_absent_returns_none() -> None:
|
|
275
|
-
assert hook_module._parse_pr_url("gh pr ready 418") is None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def test_ready_segment_clips_at_command_separator() -> None:
|
|
279
|
-
assert (
|
|
280
|
-
hook_module._ready_command_segment(
|
|
281
|
-
"gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo"
|
|
282
|
-
)
|
|
283
|
-
== "gh pr ready 161 "
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def test_ready_segment_keeps_repo_flag_on_a_continued_line() -> None:
|
|
288
|
-
segment = hook_module._ready_command_segment(
|
|
289
|
-
"gh pr ready 161 \\\n --repo target-owner/target-repo"
|
|
290
|
-
)
|
|
291
|
-
assert "--repo" in segment
|
|
292
|
-
assert "target-owner/target-repo" in segment
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def test_continued_repo_flag_binds_the_gate_to_the_named_repo(
|
|
296
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
297
|
-
) -> None:
|
|
298
|
-
identity = _capture_convergence_identity(
|
|
299
|
-
monkeypatch,
|
|
300
|
-
tmp_path,
|
|
301
|
-
"gh pr ready 161 \\\n --repo target-owner/target-repo",
|
|
302
|
-
("cwd-owner", "cwd-repo"),
|
|
303
|
-
)
|
|
304
|
-
assert identity == ("target-owner", "target-repo", 161)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def test_chained_repo_flag_does_not_misbind_the_gate(
|
|
308
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
309
|
-
) -> None:
|
|
310
|
-
identity = _capture_convergence_identity(
|
|
311
|
-
monkeypatch,
|
|
312
|
-
tmp_path,
|
|
313
|
-
"gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo --body-file note.md",
|
|
314
|
-
("cwd-owner", "cwd-repo"),
|
|
315
|
-
)
|
|
316
|
-
assert identity == ("cwd-owner", "cwd-repo", 161)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def test_chained_pr_url_does_not_misbind_the_gate(
|
|
320
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
321
|
-
) -> None:
|
|
322
|
-
identity = _capture_convergence_identity(
|
|
323
|
-
monkeypatch,
|
|
324
|
-
tmp_path,
|
|
325
|
-
"gh pr ready 161 && echo https://github.com/other-owner/other-repo/pull/5",
|
|
326
|
-
("cwd-owner", "cwd-repo"),
|
|
327
|
-
)
|
|
328
|
-
assert identity == ("cwd-owner", "cwd-repo", 161)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def test_ready_segment_skips_a_leading_undo_invocation() -> None:
|
|
332
|
-
assert (
|
|
333
|
-
hook_module._ready_command_segment("gh pr ready --undo && gh pr ready 161")
|
|
334
|
-
== "gh pr ready 161"
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def test_parse_repo_flag_full_url_form() -> None:
|
|
339
|
-
assert hook_module._parse_repo_flag(
|
|
340
|
-
"gh pr ready 161 --repo https://github.com/other-owner/other-repo"
|
|
341
|
-
) == ("other-owner", "other-repo")
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def test_parse_repo_flag_short_alias_full_url_form() -> None:
|
|
345
|
-
assert hook_module._parse_repo_flag(
|
|
346
|
-
"gh pr ready 161 -R https://github.com/other-owner/other-repo"
|
|
347
|
-
) == ("other-owner", "other-repo")
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def test_parse_repo_flag_ssh_form() -> None:
|
|
351
|
-
assert hook_module._parse_repo_flag(
|
|
352
|
-
"gh pr ready 161 --repo git@github.com:other-owner/other-repo"
|
|
353
|
-
) == ("other-owner", "other-repo")
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def test_parse_repo_flag_ssh_form_strips_git_suffix() -> None:
|
|
357
|
-
assert hook_module._parse_repo_flag(
|
|
358
|
-
"gh pr ready 161 --repo git@github.com:other-owner/other-repo.git"
|
|
359
|
-
) == ("other-owner", "other-repo")
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def test_parse_repo_flag_url_form_strips_git_suffix() -> None:
|
|
363
|
-
assert hook_module._parse_repo_flag(
|
|
364
|
-
"gh pr ready 161 --repo https://github.com/other-owner/other-repo.git"
|
|
365
|
-
) == ("other-owner", "other-repo")
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def test_parse_pr_url_accepts_enterprise_host() -> None:
|
|
369
|
-
assert hook_module._parse_pr_url(
|
|
370
|
-
"gh pr ready https://github.example.com/other-owner/other-repo/pull/161"
|
|
371
|
-
) == ("other-owner", "other-repo", 161)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def test_background_operator_clips_chained_repo_flag(
|
|
375
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
376
|
-
) -> None:
|
|
377
|
-
identity = _capture_convergence_identity(
|
|
378
|
-
monkeypatch,
|
|
379
|
-
tmp_path,
|
|
380
|
-
"gh pr ready 161 & gh pr comment 999 --repo other-owner/other-repo",
|
|
381
|
-
("cwd-owner", "cwd-repo"),
|
|
382
|
-
)
|
|
383
|
-
assert identity == ("cwd-owner", "cwd-repo", 161)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def test_trailing_redirect_keeps_earlier_repo_flag(
|
|
387
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
388
|
-
) -> None:
|
|
389
|
-
identity = _capture_convergence_identity(
|
|
390
|
-
monkeypatch,
|
|
391
|
-
tmp_path,
|
|
392
|
-
"gh pr ready 161 --repo other-owner/other-repo 2>&1",
|
|
393
|
-
("cwd-owner", "cwd-repo"),
|
|
394
|
-
)
|
|
395
|
-
assert identity == ("other-owner", "other-repo", 161)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def test_stderr_redirect_dup_keeps_repo_flag() -> None:
|
|
399
|
-
segment = hook_module._ready_command_segment(
|
|
400
|
-
"gh pr ready 161 >&2 --repo other-owner/other-repo"
|
|
401
|
-
)
|
|
402
|
-
assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def test_stderr_to_stdout_redirect_keeps_repo_flag() -> None:
|
|
406
|
-
segment = hook_module._ready_command_segment(
|
|
407
|
-
"gh pr ready 2>&1 --repo other-owner/other-repo"
|
|
408
|
-
)
|
|
409
|
-
assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def test_append_all_output_redirect_keeps_repo_flag() -> None:
|
|
413
|
-
segment = hook_module._ready_command_segment(
|
|
414
|
-
"gh pr ready 161 &> log --repo other-owner/other-repo"
|
|
415
|
-
)
|
|
416
|
-
assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def test_stderr_redirect_binds_repo_end_to_end(
|
|
420
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
421
|
-
) -> None:
|
|
422
|
-
identity = _capture_convergence_identity(
|
|
423
|
-
monkeypatch,
|
|
424
|
-
tmp_path,
|
|
425
|
-
"gh pr ready 161 >&2 --repo other-owner/other-repo",
|
|
426
|
-
("cwd-owner", "cwd-repo"),
|
|
427
|
-
)
|
|
428
|
-
assert identity == ("other-owner", "other-repo", 161)
|