Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/maint-76-claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ jobs:
- name: Run Claude Code Review
id: claude
continue-on-error: true
uses: anthropics/claude-code-action@51705da45eecce209d4700538bf8377d5b5fc695 # v1
uses: anthropics/claude-code-action@2fee15510437d71399d9139ed60433470484a8fb # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: '*'
Expand Down
41 changes: 35 additions & 6 deletions scripts/check_agents_md_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import argparse
import json
import re
import shlex
import shutil
import sys
from dataclasses import dataclass
Expand Down Expand Up @@ -47,7 +48,9 @@ def managed_section(text: str) -> str | None:


def _clean_ref(value: str) -> str:
value = value.strip().strip("\"'")
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
value = re.sub(r"[:#]L?\d+(?:-L?\d+)?$", "", value)
return value

Expand All @@ -59,25 +62,46 @@ def _looks_like_path(value: str) -> bool:
return "/" in value or path.suffix.lower() in PATH_SUFFIXES


def _resolve_repo_path(repo_root: Path, ref: str) -> Path | None:
root = repo_root.resolve()
raw_path = Path(ref)
candidate = raw_path if raw_path.is_absolute() else root / raw_path
candidate = candidate.resolve()
try:
candidate.relative_to(root)
except ValueError:
return None
return candidate


def _path_exists(repo_root: Path, ref: str) -> bool:
return (repo_root / ref).exists()
candidate = _resolve_repo_path(repo_root, ref)
return candidate.exists() if candidate else False


def _command_parts(ref: str) -> list[str]:
try:
return shlex.split(ref)
except ValueError:
return ref.split()


def _command_exists(repo_root: Path, ref: str) -> bool:
parts = ref.split()
parts = _command_parts(ref)
if not parts:
return True
command = parts[0]
if command.startswith(("./", "../")) or "/" in command:
return (repo_root / command).exists()
candidate = _resolve_repo_path(repo_root, command)
return candidate.exists() if candidate else False
return shutil.which(command) is not None


def _check_command_ref(repo_root: Path, ref: str) -> list[Finding]:
findings: list[Finding] = []
if not _command_exists(repo_root, ref):
findings.append(Finding("command", ref, f"referenced command not found: {ref}"))
for arg in ref.split()[1:]:
for arg in _command_parts(ref)[1:]:
arg = _clean_ref(arg)
if "=" in arg:
_, arg = arg.split("=", 1)
Expand Down Expand Up @@ -140,7 +164,12 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)

repo_root = args.repo_root.resolve()
agents_md = args.agents_md.resolve() if args.agents_md else None
if args.agents_md:
agents_md = (
args.agents_md if args.agents_md.is_absolute() else repo_root / args.agents_md
).resolve()
else:
agents_md = None
findings = check_agents_md(repo_root, agents_md)

if args.as_json:
Expand Down
3 changes: 2 additions & 1 deletion scripts/orchestrator_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ def _require_nonempty_string(value: Any, field_name: str) -> str:


def _validate_repo(repo: str) -> str:
if "/" not in repo or repo.startswith("/") or repo.endswith("/"):
parts = repo.split("/")
if len(parts) != 2 or not all(parts):
raise OrchestratorSkillConfigError("repo must use owner/name format")
return repo

Expand Down
3 changes: 2 additions & 1 deletion scripts/reference_packs.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def _require_nonempty_string(value: Any, field_name: str) -> str:


def _validate_repo(repo: str) -> str:
if "/" not in repo or repo.startswith("/") or repo.endswith("/"):
parts = repo.split("/")
if len(parts) != 2 or not all(parts):
raise ReferencePackConfigError("repo must use owner/name format")
return repo

Expand Down
78 changes: 65 additions & 13 deletions scripts/runner_lib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import base64
import binascii
import contextlib
import dataclasses
import datetime as dt
import hashlib
Expand Down Expand Up @@ -83,6 +84,34 @@ def _validate_provider(provider: str) -> str:
return normalized


def _resolve_child_path(root: Path, path: str | Path, *, description: str) -> Path:
root_resolved = root.resolve()
raw_path = Path(path)
candidate = raw_path if raw_path.is_absolute() else root_resolved / raw_path
candidate = candidate.resolve()
try:
candidate.relative_to(root_resolved)
except ValueError as exc:
raise ValueError(f"{description} must stay within {root_resolved}") from exc
return candidate


def _resolve_reference_checkout_path(workspace_path: Path, checkout_path: str | Path) -> Path:
reference_root = (workspace_path / ".reference").resolve()
candidate = _resolve_child_path(
workspace_path,
checkout_path,
description="reference checkout path",
)
try:
candidate.relative_to(reference_root)
except ValueError as exc:
raise ValueError("reference checkout path must stay within .reference") from exc
if candidate == reference_root:
raise ValueError("reference checkout path must identify a child of .reference")
return candidate


def _runner_key(pr_number: int, head_sha: str, provider: str) -> str:
payload = f"{provider}:{pr_number}:{head_sha}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
Expand Down Expand Up @@ -314,7 +343,7 @@ def _materialize_single_checkout_plan(
)
_run_git(["git", "-C", str(clone_dir), "sparse-checkout", "reapply"], env=git_env)

destination_root = workspace_path / checkout_path
destination_root = _resolve_reference_checkout_path(workspace_path, checkout_path)
if destination_root.exists():
shutil.rmtree(destination_root)
destination_root.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -356,11 +385,6 @@ def materialize_orchestrator_skill(
return None

if plan.pack:
materialize_reference_packs(
workspace_path,
reference_pack_name=plan.pack,
token=token,
)
reference_packs = _load_reference_packs_module()
snapshot = reference_packs.load_reference_packs(workspace_path)
matching = [
Expand All @@ -370,7 +394,17 @@ def materialize_orchestrator_skill(
]
if not matching:
raise ValueError(f"orchestrator skill reference pack not found: {plan.pack}")
checkout_path = workspace_path / matching[0].checkout_path
checkout_path = _resolve_reference_checkout_path(
workspace_path,
matching[0].checkout_path,
)
with contextlib.suppress(FileNotFoundError):
shutil.rmtree(checkout_path)
materialize_reference_packs(
workspace_path,
reference_pack_name=plan.pack,
token=token,
)
else:
checkout_path = _materialize_single_checkout_plan(
workspace_path,
Expand Down Expand Up @@ -413,12 +447,23 @@ def assemble_prompt(
)

if context.get("materialize_orchestrator_skill"):
materialize_orchestrator_skill(
orchestrator_summary_path = materialize_orchestrator_skill(
workspace,
pack_override=context.get("orchestrator_skill_pack") or None,
enabled_override=context.get("orchestrator_skill_enabled"),
token=token,
)
else:
orchestrator_summary_raw = context.get("orchestrator_skill_summary_path")
orchestrator_summary_path = (
Path(str(orchestrator_summary_raw)) if orchestrator_summary_raw else None
)
if orchestrator_summary_path:
orchestrator_summary_path = _resolve_child_path(
workspace,
orchestrator_summary_path,
description="orchestrator_skill_summary_path",
)

output_file = str(
context.get("output_file") or _prompt_output_name(provider, context.get("pr_number"))
Expand Down Expand Up @@ -448,12 +493,11 @@ def assemble_prompt(
if reference_summary.is_file():
parts.extend(["\n\n## Reference Packs\n", _read_text(reference_summary).rstrip()])

orchestrator_summary = workspace / ".reference" / "ORCHESTRATOR_SKILL.md"
if orchestrator_summary.is_file():
if orchestrator_summary_path and orchestrator_summary_path.is_file():
parts.extend(
[
"\n\n## Orchestrator Skill Context\n",
_read_text(orchestrator_summary).rstrip(),
_read_text(orchestrator_summary_path).rstrip(),
]
)

Expand Down Expand Up @@ -504,7 +548,14 @@ def _parse_jsonl_output(raw_output: str) -> tuple[list[str], list[str]]:
event_type = str(event.get("type") or event.get("status") or "").lower()
text = _extract_text_from_json_event(event)
if "error" in event_type or event.get("error"):
errors.append(text or json.dumps(event, sort_keys=True))
error_value = event.get("error")
nested_error = (
_extract_text_from_json_event(error_value)
if isinstance(error_value, dict)
else None
)
direct_error = error_value.strip() if isinstance(error_value, str) else None
errors.append(direct_error or nested_error or text or json.dumps(event, sort_keys=True))
elif text:
messages.append(text)
if not parsed_any:
Expand All @@ -522,7 +573,7 @@ def parse_runner_output(provider: str, raw_output: str) -> RunnerResult:
clipped = raw[:64000] if len(raw) > 64000 else raw

messages, errors = _parse_jsonl_output(clipped) if provider == "codex" else ([], [])
final_message = messages[-1] if messages else clipped.strip()
final_message = errors[0] if errors else (messages[-1] if messages else clipped.strip())

if not errors and re.search(
r"(^::error::|\bTraceback\b|\bError:|\bException\b)",
Expand Down Expand Up @@ -943,6 +994,7 @@ def _cmd_assemble(args: argparse.Namespace) -> int:
"materialize_orchestrator_skill": args.materialize_orchestrator_skill,
"orchestrator_skill_pack": args.orchestrator_skill_pack or None,
"orchestrator_skill_enabled": _parse_optional_bool(args.orchestrator_skill_enabled),
"orchestrator_skill_summary_path": os.environ.get("ORCHESTRATOR_SKILL_SUMMARY_PATH"),
"github_token": os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN"),
}
prompt = assemble_prompt(args.reference_pack_name, context, args.provider)
Expand Down
Loading
Loading