@homericintelligence/athena-opencode 0.5.0 → 0.5.2
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/README.md +30 -17
- package/package.json +2 -2
- package/plugin.js +20 -5
- package/skills/TECHNICAL_ENGLISH.md +78 -0
- package/skills/_cli.py +33 -22
- package/skills/_plugin.json +1 -0
- package/skills/_support/docs/dependency-resolution.md +203 -0
- package/skills/_support/docs/policies/development.md +131 -0
- package/skills/_support/docs/policies/evidence-integrity.md +40 -0
- package/skills/_support/docs/principles/README.md +565 -0
- package/skills/_support/docs/principles/details/p001-kiss.md +114 -0
- package/skills/_support/docs/principles/details/p002-yagni.md +109 -0
- package/skills/_support/docs/principles/details/p003-dry.md +126 -0
- package/skills/_support/docs/principles/details/p004-solid.md +165 -0
- package/skills/_support/docs/principles/details/p005-modularity.md +123 -0
- package/skills/_support/docs/principles/details/p006-principle-of-least-astonishment.md +115 -0
- package/skills/_support/docs/principles/details/p007-subtraction-over-addition.md +113 -0
- package/skills/_support/docs/principles/details/p008-understand-before-subtracting.md +115 -0
- package/skills/_support/docs/principles/details/p009-general-mechanisms-over-special-cases.md +108 -0
- package/skills/_support/docs/principles/details/p010-scope-fidelity.md +117 -0
- package/skills/_support/docs/principles/details/p011-minimal-coherent-change.md +128 -0
- package/skills/_support/docs/principles/details/p012-evidence-before-modification.md +115 -0
- package/skills/_support/docs/principles/details/p013-avoid-hasty-abstractions.md +112 -0
- package/skills/_support/docs/principles/details/p014-preserve-unrequested-behavior.md +110 -0
- package/skills/_support/docs/principles/details/p015-architecture-conformance.md +124 -0
- package/skills/_support/docs/principles/details/p016-separation-of-concerns.md +125 -0
- package/skills/_support/docs/principles/details/p017-high-cohesion-low-coupling.md +130 -0
- package/skills/_support/docs/principles/details/p018-information-hiding.md +128 -0
- package/skills/_support/docs/principles/details/p019-explicit-contracts.md +134 -0
- package/skills/_support/docs/principles/details/p020-executable-architecture.md +130 -0
- package/skills/_support/docs/principles/details/p021-evolutionary-and-reversible-design.md +126 -0
- package/skills/_support/docs/principles/details/p022-test-behavior-not-implementation.md +129 -0
- package/skills/_support/docs/principles/details/p023-parameterized-table-driven-testing.md +129 -0
- package/skills/_support/docs/principles/details/p024-boundary-value-testing.md +128 -0
- package/skills/_support/docs/principles/details/p025-property-based-testing-for-invariants.md +133 -0
- package/skills/_support/docs/principles/details/p026-regression-before-repair.md +134 -0
- package/skills/_support/docs/principles/details/p027-deterministic-and-hermetic-tests.md +128 -0
- package/skills/_support/docs/principles/details/p028-test-failure-paths.md +133 -0
- package/skills/_support/docs/principles/details/p029-generalize-error-policy-preserve-specific-cause.md +131 -0
- package/skills/_support/docs/principles/details/p030-nearest-responsible-error-boundary.md +133 -0
- package/skills/_support/docs/principles/details/p031-propagate-rather-than-swallow.md +131 -0
- package/skills/_support/docs/principles/details/p032-handle-once-preserve-causality.md +137 -0
- package/skills/_support/docs/principles/details/p033-state-safe-failure-semantics.md +140 -0
- package/skills/_support/docs/principles/details/p034-fail-fast.md +132 -0
- package/skills/_support/docs/principles/details/p035-fail-secure-fail-closed.md +137 -0
- package/skills/_support/docs/principles/details/p036-graceful-degradation.md +137 -0
- package/skills/_support/docs/principles/details/p037-idempotency-before-retry.md +139 -0
- package/skills/_support/docs/principles/details/p038-bounded-retry.md +145 -0
- package/skills/_support/docs/principles/details/p039-bounded-waiting.md +151 -0
- package/skills/_support/docs/principles/details/p040-bounded-resources.md +138 -0
- package/skills/_support/docs/principles/details/p041-backpressure-and-load-shedding.md +141 -0
- package/skills/_support/docs/principles/details/p042-fault-isolation-bulkheads.md +140 -0
- package/skills/_support/docs/principles/details/p043-circuit-breakers.md +148 -0
- package/skills/_support/docs/principles/details/p044-atomicity-where-possible.md +141 -0
- package/skills/_support/docs/principles/details/p045-compensation-where-atomicity-is-impossible.md +163 -0
- package/skills/_support/docs/principles/details/p046-resumability.md +138 -0
- package/skills/_support/docs/principles/details/p047-observability-is-part-of-correctness.md +134 -0
- package/skills/_support/docs/principles/details/p048-secure-by-design.md +132 -0
- package/skills/_support/docs/principles/details/p049-secure-by-default.md +126 -0
- package/skills/_support/docs/principles/details/p050-least-privilege.md +123 -0
- package/skills/_support/docs/principles/details/p051-complete-mediation.md +124 -0
- package/skills/_support/docs/principles/details/p052-separation-of-duties.md +129 -0
- package/skills/_support/docs/principles/details/p053-validate-at-trust-boundaries.md +126 -0
- package/skills/_support/docs/principles/details/p054-defense-in-depth.md +135 -0
- package/skills/_support/docs/principles/details/p055-minimize-attack-surface.md +133 -0
- package/skills/_support/docs/principles/details/p056-secrets-stay-out-of-code-and-context.md +137 -0
- package/skills/_support/docs/principles/details/p057-supply-chain-integrity.md +135 -0
- package/skills/_support/docs/principles/details/p058-bounded-agent-authority.md +137 -0
- package/skills/_support/docs/principles/details/p059-data-is-not-instruction.md +130 -0
- package/skills/_support/docs/principles/details/p060-constrain-sub-agents.md +136 -0
- package/skills/_support/docs/principles/details/p061-separate-decision-from-high-impact-execution.md +131 -0
- package/skills/_support/docs/principles/details/p062-human-approval-for-irreversible-or-high-risk-actions.md +129 -0
- package/skills/_support/docs/principles/details/p063-requirement-to-code-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p064-requirement-to-test-traceability.md +116 -0
- package/skills/_support/docs/principles/details/p065-verify-before-claiming-completion.md +131 -0
- package/skills/_support/docs/principles/details/p066-preserve-existing-work.md +114 -0
- package/skills/_support/docs/principles/details/p067-no-test-cheating.md +126 -0
- package/skills/_support/docs/principles/details/p068-no-validation-bypass.md +119 -0
- package/skills/_support/docs/principles/details/p069-independent-review-for-high-risk-changes.md +129 -0
- package/skills/_support/docs/principles/details/p070-code-health-must-not-regress.md +116 -0
- package/skills/_support/docs/principles/details/p071-consistency-over-personal-preference.md +117 -0
- package/skills/_support/docs/principles/details/p072-technical-evidence-over-preference.md +122 -0
- package/skills/_support/docs/principles/details/p073-optimize-only-with-evidence.md +123 -0
- package/skills/_support/docs/principles/details/p074-prefer-existing-mechanisms.md +121 -0
- package/skills/_support/docs/principles/details/p075-make-invalid-states-hard-to-represent.md +128 -0
- package/skills/_support/docs/principles/details/p076-parse-then-validate-then-operate.md +133 -0
- package/skills/_support/docs/principles/details/p077-separate-policy-from-mechanism.md +112 -0
- package/skills/_support/docs/principles/details/p078-single-source-of-truth.md +131 -0
- package/skills/_support/docs/principles/details/p079-explicit-ownership-and-lifetimes.md +124 -0
- package/skills/_support/docs/principles/details/p080-make-concurrency-deliberate.md +122 -0
- package/skills/_support/docs/principles/details/p081-forward-progress-with-safety.md +125 -0
- package/skills/_support/docs/principles/details/p082-design-for-cancellation.md +133 -0
- package/skills/_support/docs/principles/details/p083-irreversible-actions-last.md +124 -0
- package/skills/_support/docs/principles/details/p084-prefer-local-reasoning.md +131 -0
- package/skills/_support/docs/principles/details/p085-explicit-is-better-than-implicit.md +123 -0
- package/skills/_support/docs/principles/details/p086-readability-counts.md +113 -0
- package/skills/_support/docs/principles/details/p087-comments-explain-why-code-explains-what.md +120 -0
- package/skills/_support/docs/principles/details/p088-delete-dead-code.md +125 -0
- package/skills/_support/docs/principles/details/p089-delete-obsolete-configuration-and-dependencies.md +125 -0
- package/skills/_support/docs/principles/details/p090-prefer-negative-code.md +134 -0
- package/skills/_support/docs/principles/details/p091-test-driven-development.md +149 -0
- package/skills/_support/docs/review/README.md +63 -0
- package/skills/_support/docs/review/behavior-first-testing.md +78 -0
- package/skills/_support/docs/review/common.md +400 -0
- package/skills/_support/docs/review/design-docs.md +74 -0
- package/skills/_support/docs/review/issue-planning.md +247 -0
- package/skills/_support/docs/review/language-routing.md +141 -0
- package/skills/_support/docs/review/repository-scorecard.md +170 -0
- package/skills/advise/SKILL.md +108 -74
- package/skills/advise/scripts/list_retrievable_skills.py +20 -8
- package/skills/advise/scripts/resolve_knowledge_checkout.py +533 -0
- package/skills/brainstorm/SKILL.md +163 -127
- package/skills/change-review/SKILL.md +91 -71
- package/skills/change-review/references/scope-resolution.md +69 -46
- package/skills/change-review/scripts/resolve_scope.py +144 -84
- package/skills/finalize-plan/SKILL.md +205 -139
- package/skills/git-worktrees/SKILL.md +100 -80
- package/skills/git-worktrees/scripts/prepare_worktree.py +34 -15
- package/skills/issue-review/SKILL.md +136 -79
- package/skills/learn/SKILL.md +359 -192
- package/skills/myrmidon-swarm/SKILL.md +126 -86
- package/skills/plan-issue/SKILL.md +122 -81
- package/skills/pr-review/SKILL.md +184 -116
- package/skills/pr-review/references/criteria.md +61 -37
- package/skills/pr-review/references/delivery.md +277 -101
- package/skills/pr-review/references/evidence.md +346 -149
- package/skills/pr-review/references/prevalidated.md +93 -61
- package/skills/pr-review/scripts/collect_evidence.py +312 -162
- package/skills/pr-review/scripts/deliver_go.py +701 -0
- package/skills/pr-review/scripts/diff_context.py +34 -15
- package/skills/pr-review/scripts/materialize_snapshot.py +141 -130
- package/skills/pr-review/scripts/pr_identity.py +27 -15
- package/skills/pr-review/scripts/resolve_pr.py +77 -41
- package/skills/realign/SKILL.md +516 -0
- package/skills/realign/references/aislop-integration.md +215 -0
- package/skills/realign/references/architecture-and-structure.md +271 -0
- package/skills/realign/references/control-flow-and-errors.md +344 -0
- package/skills/realign/references/tests-dependencies-and-security.md +261 -0
- package/skills/realign/scripts/resolve_assessment.py +1525 -0
- package/skills/repo-review/SKILL.md +202 -131
- package/skills/simplify/SKILL.md +174 -0
- package/skills/systematic-debugging/SKILL.md +212 -192
- package/skills/systematic-debugging/scripts/repository_evidence.py +26 -8
- package/skills/test-driven-development/SKILL.md +126 -96
- package/skills/tidy/SKILL.md +104 -59
- package/skills/tidy/scripts/run_tidy.py +54 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Validate pull-request identifiers shared by
|
|
1
|
+
"""Validate pull-request identifiers shared by pull-request review helpers."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -12,45 +12,51 @@ GITHUB_HOST = "github.com"
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def validate_pr_identifier(value: str) -> None:
|
|
15
|
-
"""Require a positive
|
|
15
|
+
"""Require a positive pull-request number or a canonical GitHub pull-request URL."""
|
|
16
16
|
if (value.isascii() and value.isdigit() and int(value) > 0) or PR_URL.fullmatch(
|
|
17
17
|
value
|
|
18
18
|
):
|
|
19
19
|
return
|
|
20
|
-
raise RuntimeError(f"
|
|
20
|
+
raise RuntimeError(f"The pull-request identifier is not valid: {value!r}.")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def require_commit_oid(value: object, label: str) -> str:
|
|
24
|
-
"""Return one canonical immutable commit
|
|
24
|
+
"""Return one canonical immutable commit object identifier or fail closed."""
|
|
25
25
|
if not isinstance(value, str) or COMMIT_OID.fullmatch(value) is None:
|
|
26
|
-
raise RuntimeError(
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
f"The value for {label} must be a Git commit object identifier that "
|
|
28
|
+
"contains 40 lowercase hexadecimal characters."
|
|
29
|
+
)
|
|
27
30
|
return value
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
def require_github_repository(value: object, label: str) -> str:
|
|
31
|
-
"""Return a canonical
|
|
34
|
+
"""Return a canonical GitHub repository or reject unsafe input."""
|
|
32
35
|
if not isinstance(value, str) or GITHUB_REPOSITORY.fullmatch(value) is None:
|
|
33
|
-
raise RuntimeError(
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"The value for "
|
|
38
|
+
f"{label} must use the canonical GitHub OWNER/REPOSITORY format."
|
|
39
|
+
)
|
|
34
40
|
return value
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
def require_github_host(value: object, label: str) -> str:
|
|
38
44
|
"""Return the supported canonical GitHub hostname or fail closed."""
|
|
39
45
|
if value != GITHUB_HOST:
|
|
40
|
-
raise RuntimeError(f"{label} must be {GITHUB_HOST}")
|
|
46
|
+
raise RuntimeError(f"The value for {label} must be '{GITHUB_HOST}'.")
|
|
41
47
|
return GITHUB_HOST
|
|
42
48
|
|
|
43
49
|
|
|
44
50
|
def canonical_pull_request_url(repository: object, number: object) -> str:
|
|
45
|
-
"""Return one exact public
|
|
51
|
+
"""Return one exact public GitHub pull-request URL from a trusted identity."""
|
|
46
52
|
canonical_repository = require_github_repository(repository, "repository")
|
|
47
53
|
if isinstance(number, bool) or not isinstance(number, int) or number < 1:
|
|
48
|
-
raise RuntimeError("pull-request number must be a positive integer")
|
|
54
|
+
raise RuntimeError("The pull-request number must be a positive integer.")
|
|
49
55
|
return f"https://{GITHUB_HOST}/{canonical_repository}/pull/{number}"
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
def pull_request_number(value: str) -> int:
|
|
53
|
-
"""Return the positive number
|
|
59
|
+
"""Return the positive number in a validated pull-request identifier."""
|
|
54
60
|
validate_pr_identifier(value)
|
|
55
61
|
if value.isdigit():
|
|
56
62
|
return int(value)
|
|
@@ -64,17 +70,23 @@ def require_canonical_pull_request_url(
|
|
|
64
70
|
"""Require the exact canonical URL for a retained pull-request identity."""
|
|
65
71
|
canonical_url = canonical_pull_request_url(repository, number)
|
|
66
72
|
if value != canonical_url:
|
|
67
|
-
raise RuntimeError(
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
f"The value for {label} must be the canonical GitHub pull-request URL."
|
|
75
|
+
)
|
|
68
76
|
return canonical_url
|
|
69
77
|
|
|
70
78
|
|
|
71
79
|
def repository_from_pr_url(url: str, number: int) -> str:
|
|
72
|
-
"""Return
|
|
80
|
+
"""Return the repository from a canonical pull-request URL and number."""
|
|
73
81
|
parsed_url = urlparse(url)
|
|
74
82
|
path_parts = parsed_url.path.strip("/").split("/")
|
|
75
83
|
if len(path_parts) != 4 or path_parts[2] != "pull" or path_parts[3] != str(number):
|
|
76
|
-
raise RuntimeError(
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"GitHub returned a pull-request URL that is not valid: '{url}'."
|
|
86
|
+
)
|
|
77
87
|
repository = "/".join(path_parts[:2])
|
|
78
88
|
if url != canonical_pull_request_url(repository, number):
|
|
79
|
-
raise RuntimeError(
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"GitHub returned a pull-request URL that is not valid: '{url}'."
|
|
91
|
+
)
|
|
80
92
|
return repository
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Resolve an explicit
|
|
2
|
+
"""Resolve an explicit pull request or the only open pull request for the current branch."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import importlib.util
|
|
6
7
|
import json
|
|
7
8
|
import sys
|
|
8
9
|
from collections.abc import Sequence
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
if __package__ in {None, ""}:
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
15
13
|
|
|
16
14
|
from pr_identity import (
|
|
17
15
|
canonical_pull_request_url,
|
|
@@ -23,38 +21,59 @@ from pr_identity import (
|
|
|
23
21
|
validate_pr_identifier,
|
|
24
22
|
)
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
if TYPE_CHECKING or __package__ not in {None, ""}:
|
|
25
|
+
from skills._cli import (
|
|
26
|
+
argument_parser,
|
|
27
|
+
git_read_arguments,
|
|
28
|
+
git_read_environment,
|
|
29
|
+
run_command,
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
_cli_path = Path(__file__).resolve().parents[2] / "_cli.py"
|
|
33
|
+
_cli_spec = importlib.util.spec_from_file_location(
|
|
34
|
+
"athena_installed_cli", _cli_path
|
|
35
|
+
)
|
|
36
|
+
if _cli_spec is None or _cli_spec.loader is None:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
f"The installed Athena CLI helper is unavailable: '{_cli_path}'."
|
|
39
|
+
)
|
|
40
|
+
_cli = importlib.util.module_from_spec(_cli_spec)
|
|
41
|
+
_cli_spec.loader.exec_module(_cli)
|
|
42
|
+
argument_parser = _cli.argument_parser
|
|
43
|
+
git_read_arguments = _cli.git_read_arguments
|
|
44
|
+
git_read_environment = _cli.git_read_environment
|
|
45
|
+
run_command = _cli.run_command
|
|
32
46
|
|
|
33
47
|
FIELDS = "number,url,state,headRefName,baseRefName,headRefOid,baseRefOid"
|
|
34
48
|
|
|
35
49
|
|
|
36
50
|
@dataclass(frozen=True)
|
|
37
51
|
class RepositoryTarget:
|
|
38
|
-
"""
|
|
52
|
+
"""The workflow gets this forge target from explicit input.
|
|
53
|
+
|
|
54
|
+
It does not infer the target from the checkout.
|
|
55
|
+
"""
|
|
39
56
|
|
|
40
57
|
host: str
|
|
41
58
|
repository: str
|
|
42
59
|
|
|
43
60
|
def repository_argument(self) -> str:
|
|
44
|
-
"""Return the fully qualified repository target
|
|
61
|
+
"""Return the fully qualified repository target for the GitHub CLI."""
|
|
45
62
|
return f"{self.host}/{self.repository}"
|
|
46
63
|
|
|
47
64
|
|
|
48
65
|
def command(*arguments: str) -> str:
|
|
49
66
|
result = run_command(arguments, capture_output=True, text=True, check=False)
|
|
50
67
|
if result.returncode != 0:
|
|
51
|
-
message = result.stderr.strip() or
|
|
68
|
+
message = result.stderr.strip() or (
|
|
69
|
+
f"The command failed. Command: {' '.join(arguments)}"
|
|
70
|
+
)
|
|
52
71
|
raise RuntimeError(message)
|
|
53
72
|
return result.stdout
|
|
54
73
|
|
|
55
74
|
|
|
56
75
|
def current_branch() -> str:
|
|
57
|
-
"""Return the current branch through the
|
|
76
|
+
"""Return the current branch through the isolated Git read boundary."""
|
|
58
77
|
result = run_command(
|
|
59
78
|
["git", *git_read_arguments(), "branch", "--show-current"],
|
|
60
79
|
capture_output=True,
|
|
@@ -63,7 +82,9 @@ def current_branch() -> str:
|
|
|
63
82
|
check=False,
|
|
64
83
|
)
|
|
65
84
|
if result.returncode != 0:
|
|
66
|
-
message =
|
|
85
|
+
message = (
|
|
86
|
+
result.stderr.strip() or "The git branch --show-current command failed."
|
|
87
|
+
)
|
|
67
88
|
raise RuntimeError(message)
|
|
68
89
|
return result.stdout.strip()
|
|
69
90
|
|
|
@@ -71,7 +92,7 @@ def current_branch() -> str:
|
|
|
71
92
|
def load_object(output: str) -> dict[str, Any]:
|
|
72
93
|
value = json.loads(output)
|
|
73
94
|
if not isinstance(value, dict):
|
|
74
|
-
raise TypeError("GitHub returned
|
|
95
|
+
raise TypeError("GitHub returned a pull-request object that is not valid.")
|
|
75
96
|
return value
|
|
76
97
|
|
|
77
98
|
|
|
@@ -81,18 +102,18 @@ def target_from_arguments(
|
|
|
81
102
|
host: str | None,
|
|
82
103
|
repository: str | None,
|
|
83
104
|
) -> RepositoryTarget:
|
|
84
|
-
"""Resolve a trusted target from explicit
|
|
105
|
+
"""Resolve a trusted target from explicit options or one canonical pull-request URL."""
|
|
85
106
|
|
|
86
107
|
def identity_from_url(value: str) -> tuple[int, str]:
|
|
87
108
|
try:
|
|
88
109
|
number = pull_request_number(value)
|
|
89
110
|
return number, repository_from_pr_url(value, number)
|
|
90
111
|
except RuntimeError as error:
|
|
91
|
-
parser.error(f"
|
|
92
|
-
raise AssertionError("argument parser returned after a URL error")
|
|
112
|
+
parser.error(f"The pull-request URL is not valid: {error}")
|
|
113
|
+
raise AssertionError("The argument parser returned after a URL error.")
|
|
93
114
|
|
|
94
115
|
if (host is None) != (repository is None):
|
|
95
|
-
parser.error("--target-host and --target-repository
|
|
116
|
+
parser.error("Specify --target-host and --target-repository together.")
|
|
96
117
|
if host is not None and repository is not None:
|
|
97
118
|
try:
|
|
98
119
|
target = RepositoryTarget(
|
|
@@ -104,7 +125,9 @@ def target_from_arguments(
|
|
|
104
125
|
if identifier is not None and identifier.startswith("https://"):
|
|
105
126
|
_, supplied_repository = identity_from_url(identifier)
|
|
106
127
|
if supplied_repository.casefold() != target.repository.casefold():
|
|
107
|
-
parser.error(
|
|
128
|
+
parser.error(
|
|
129
|
+
"The pull-request URL does not match '--target-repository'."
|
|
130
|
+
)
|
|
108
131
|
return target
|
|
109
132
|
if identifier is not None and identifier.startswith("https://"):
|
|
110
133
|
_, repository = identity_from_url(identifier)
|
|
@@ -113,20 +136,22 @@ def target_from_arguments(
|
|
|
113
136
|
repository=repository,
|
|
114
137
|
)
|
|
115
138
|
parser.error(
|
|
116
|
-
"numeric pull
|
|
117
|
-
"--target-repository"
|
|
139
|
+
"For a numeric pull-request identifier or branch discovery, specify "
|
|
140
|
+
"'--target-host' and '--target-repository'."
|
|
118
141
|
)
|
|
119
|
-
raise AssertionError("argument parser returned after a target error")
|
|
142
|
+
raise AssertionError("The argument parser returned after a target error.")
|
|
120
143
|
|
|
121
144
|
|
|
122
145
|
def _resolve_open_pr(identifier: str, target: RepositoryTarget) -> dict[str, Any]:
|
|
123
|
-
"""Return
|
|
146
|
+
"""Return complete metadata for one explicitly identified pull request."""
|
|
124
147
|
validate_pr_identifier(identifier)
|
|
125
148
|
number = pull_request_number(identifier)
|
|
126
149
|
if identifier.startswith("https://"):
|
|
127
150
|
supplied_repository = repository_from_pr_url(identifier, number)
|
|
128
151
|
if supplied_repository.casefold() != target.repository.casefold():
|
|
129
|
-
raise RuntimeError(
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
"The pull-request URL does not match the retained target."
|
|
154
|
+
)
|
|
130
155
|
pull_request = load_object(
|
|
131
156
|
command(
|
|
132
157
|
"gh",
|
|
@@ -140,12 +165,14 @@ def _resolve_open_pr(identifier: str, target: RepositoryTarget) -> dict[str, Any
|
|
|
140
165
|
)
|
|
141
166
|
)
|
|
142
167
|
if pull_request.get("state") != "OPEN":
|
|
143
|
-
raise RuntimeError(f"pull request
|
|
168
|
+
raise RuntimeError(f"The pull request is not open: '{identifier}'.")
|
|
144
169
|
if pull_request.get("number") != number:
|
|
145
|
-
raise RuntimeError(
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
"GitHub returned a pull request that differs from the request."
|
|
172
|
+
)
|
|
146
173
|
for field in ("baseRefOid", "headRefOid"):
|
|
147
174
|
require_commit_oid(
|
|
148
|
-
pull_request.get(field), f"GitHub immutable
|
|
175
|
+
pull_request.get(field), f"GitHub immutable pull-request revision {field}"
|
|
149
176
|
)
|
|
150
177
|
return pull_request
|
|
151
178
|
|
|
@@ -153,18 +180,21 @@ def _resolve_open_pr(identifier: str, target: RepositoryTarget) -> dict[str, Any
|
|
|
153
180
|
def _validate_repository_identity(
|
|
154
181
|
pull_request: dict[str, Any], target: RepositoryTarget
|
|
155
182
|
) -> None:
|
|
156
|
-
"""Reject a
|
|
183
|
+
"""Reject a pull-request URL that differs from the retained forge target."""
|
|
157
184
|
number = pull_request.get("number")
|
|
158
185
|
url = pull_request.get("url")
|
|
159
186
|
if not isinstance(number, int) or not isinstance(url, str):
|
|
160
|
-
raise TypeError("GitHub returned incomplete pull-request identity")
|
|
187
|
+
raise TypeError("GitHub returned an incomplete pull-request identity.")
|
|
161
188
|
pull_repository = repository_from_pr_url(url, number)
|
|
162
189
|
if pull_repository.casefold() != target.repository.casefold():
|
|
163
190
|
raise RuntimeError(
|
|
164
|
-
f"pull request {url}
|
|
191
|
+
f"The pull request {url} is not in the target repository "
|
|
192
|
+
f"'{target.repository}'."
|
|
165
193
|
)
|
|
166
194
|
if url != canonical_pull_request_url(target.repository, number):
|
|
167
|
-
raise RuntimeError(
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f"GitHub returned a pull-request URL that is not valid: '{url}'."
|
|
197
|
+
)
|
|
168
198
|
pull_request["review_target"] = {
|
|
169
199
|
"host": target.host,
|
|
170
200
|
"kind": "github",
|
|
@@ -180,7 +210,9 @@ def resolve(explicit: str | None, target: RepositoryTarget) -> dict[str, Any]:
|
|
|
180
210
|
|
|
181
211
|
branch = current_branch()
|
|
182
212
|
if not branch:
|
|
183
|
-
raise RuntimeError(
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
"The current checkout is detached. Specify a pull-request number or URL."
|
|
215
|
+
)
|
|
184
216
|
raw_candidates = json.loads(
|
|
185
217
|
command(
|
|
186
218
|
"gh",
|
|
@@ -199,20 +231,24 @@ def resolve(explicit: str | None, target: RepositoryTarget) -> dict[str, Any]:
|
|
|
199
231
|
)
|
|
200
232
|
)
|
|
201
233
|
if not isinstance(raw_candidates, list):
|
|
202
|
-
raise TypeError("GitHub returned
|
|
234
|
+
raise TypeError("GitHub returned a pull-request list that is not valid.")
|
|
203
235
|
candidates = [item for item in raw_candidates if isinstance(item, dict)]
|
|
204
236
|
if len(candidates) == 1:
|
|
205
237
|
number = candidates[0].get("number")
|
|
206
238
|
if not isinstance(number, int) or number < 1:
|
|
207
|
-
raise RuntimeError(
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
"GitHub returned a pull-request candidate that is not valid."
|
|
241
|
+
)
|
|
208
242
|
return _resolve_open_pr(str(number), target)
|
|
209
243
|
if not candidates:
|
|
210
|
-
raise LookupError(f"no open pull request
|
|
244
|
+
raise LookupError(f"GitHub found no open pull request for branch {branch!r}.")
|
|
211
245
|
rendered = "\n".join(
|
|
212
246
|
f" #{candidate.get('number')}: {candidate.get('url')}"
|
|
213
247
|
for candidate in candidates
|
|
214
248
|
)
|
|
215
|
-
raise ValueError(
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"GitHub found multiple open pull requests for {branch!r}.\n{rendered}"
|
|
251
|
+
)
|
|
216
252
|
|
|
217
253
|
|
|
218
254
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
@@ -220,12 +256,12 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
220
256
|
parser.add_argument(
|
|
221
257
|
"--target-host",
|
|
222
258
|
metavar="HOST",
|
|
223
|
-
help="canonical GitHub host from the configured forge capability",
|
|
259
|
+
help="Use the canonical GitHub host from the configured forge capability.",
|
|
224
260
|
)
|
|
225
261
|
parser.add_argument(
|
|
226
262
|
"--target-repository",
|
|
227
263
|
metavar="OWNER/REPOSITORY",
|
|
228
|
-
help="canonical GitHub repository from the configured forge capability",
|
|
264
|
+
help="Use the canonical GitHub repository from the configured forge capability.",
|
|
229
265
|
)
|
|
230
266
|
parser.add_argument("pull_request", nargs="?", metavar="PR_NUMBER_OR_URL")
|
|
231
267
|
arguments = parser.parse_args(argv)
|