Skip to content

Commit efc468b

Browse files
chore: sync workflow templates
Automated merge of sync PR Sync hash: b288cbd7cfbf
2 parents 6066047 + fdbfd30 commit efc468b

8 files changed

Lines changed: 491 additions & 91 deletions

File tree

.github/workflows/agents-guard.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ jobs:
111111
github.event_name == 'pull_request_target' &&
112112
steps.eligibility.outputs.should-run == 'true' &&
113113
steps.api_client_base.outputs.available != 'true'
114-
uses: "stranske/Workflows/.github/actions/setup-api-client@d68de1904bcdbe16bfe2462b73aa18f41f8a0a47" # v1
114+
uses: "stranske/Workflows/.github/actions/setup-api-client@c2537cc959f2ce05926c4639d25b90678abc97bc" # v1
115115
with:
116116
secrets: ${{ toJSON(secrets) }}
117117
github_token: ${{ github.token }}
@@ -180,7 +180,7 @@ jobs:
180180
steps.eligibility.outputs.should-run == 'true' &&
181181
github.event_name == 'pull_request' &&
182182
steps.api_client_head.outputs.available != 'true'
183-
uses: "stranske/Workflows/.github/actions/setup-api-client@d68de1904bcdbe16bfe2462b73aa18f41f8a0a47" # v1
183+
uses: "stranske/Workflows/.github/actions/setup-api-client@c2537cc959f2ce05926c4639d25b90678abc97bc" # v1
184184
with:
185185
secrets: ${{ toJSON(secrets) }}
186186
github_token: ${{ github.token }}

.github/workflows/maint-76-claude-code-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ jobs:
189189
- name: Run Claude Code Review
190190
id: claude
191191
continue-on-error: true
192-
uses: anthropics/claude-code-action@51705da45eecce209d4700538bf8377d5b5fc695 # v1
192+
uses: anthropics/claude-code-action@2fee15510437d71399d9139ed60433470484a8fb # v1
193193
with:
194194
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
195195
allowed_bots: '*'

scripts/check_agents_md_freshness.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import argparse
77
import json
88
import re
9+
import shlex
910
import shutil
1011
import sys
1112
from dataclasses import dataclass
@@ -47,7 +48,9 @@ def managed_section(text: str) -> str | None:
4748

4849

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

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

6164

65+
def _resolve_repo_path(repo_root: Path, ref: str) -> Path | None:
66+
root = repo_root.resolve()
67+
raw_path = Path(ref)
68+
candidate = raw_path if raw_path.is_absolute() else root / raw_path
69+
candidate = candidate.resolve()
70+
try:
71+
candidate.relative_to(root)
72+
except ValueError:
73+
return None
74+
return candidate
75+
76+
6277
def _path_exists(repo_root: Path, ref: str) -> bool:
63-
return (repo_root / ref).exists()
78+
candidate = _resolve_repo_path(repo_root, ref)
79+
return candidate.exists() if candidate else False
80+
81+
82+
def _command_parts(ref: str) -> list[str]:
83+
try:
84+
return shlex.split(ref)
85+
except ValueError:
86+
return ref.split()
6487

6588

6689
def _command_exists(repo_root: Path, ref: str) -> bool:
67-
parts = ref.split()
90+
parts = _command_parts(ref)
6891
if not parts:
6992
return True
7093
command = parts[0]
7194
if command.startswith(("./", "../")) or "/" in command:
72-
return (repo_root / command).exists()
95+
candidate = _resolve_repo_path(repo_root, command)
96+
return candidate.exists() if candidate else False
7397
return shutil.which(command) is not None
7498

7599

76100
def _check_command_ref(repo_root: Path, ref: str) -> list[Finding]:
77101
findings: list[Finding] = []
78102
if not _command_exists(repo_root, ref):
79103
findings.append(Finding("command", ref, f"referenced command not found: {ref}"))
80-
for arg in ref.split()[1:]:
104+
for arg in _command_parts(ref)[1:]:
81105
arg = _clean_ref(arg)
82106
if "=" in arg:
83107
_, arg = arg.split("=", 1)
@@ -140,7 +164,12 @@ def main(argv: list[str] | None = None) -> int:
140164
args = parser.parse_args(argv)
141165

142166
repo_root = args.repo_root.resolve()
143-
agents_md = args.agents_md.resolve() if args.agents_md else None
167+
if args.agents_md:
168+
agents_md = (
169+
args.agents_md if args.agents_md.is_absolute() else repo_root / args.agents_md
170+
).resolve()
171+
else:
172+
agents_md = None
144173
findings = check_agents_md(repo_root, agents_md)
145174

146175
if args.as_json:

scripts/orchestrator_skill.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def _require_nonempty_string(value: Any, field_name: str) -> str:
8080

8181

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

scripts/reference_packs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def _require_nonempty_string(value: Any, field_name: str) -> str:
8383

8484

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

scripts/runner_lib/core.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import argparse
66
import base64
77
import binascii
8+
import contextlib
89
import dataclasses
910
import datetime as dt
1011
import hashlib
@@ -83,6 +84,34 @@ def _validate_provider(provider: str) -> str:
8384
return normalized
8485

8586

87+
def _resolve_child_path(root: Path, path: str | Path, *, description: str) -> Path:
88+
root_resolved = root.resolve()
89+
raw_path = Path(path)
90+
candidate = raw_path if raw_path.is_absolute() else root_resolved / raw_path
91+
candidate = candidate.resolve()
92+
try:
93+
candidate.relative_to(root_resolved)
94+
except ValueError as exc:
95+
raise ValueError(f"{description} must stay within {root_resolved}") from exc
96+
return candidate
97+
98+
99+
def _resolve_reference_checkout_path(workspace_path: Path, checkout_path: str | Path) -> Path:
100+
reference_root = (workspace_path / ".reference").resolve()
101+
candidate = _resolve_child_path(
102+
workspace_path,
103+
checkout_path,
104+
description="reference checkout path",
105+
)
106+
try:
107+
candidate.relative_to(reference_root)
108+
except ValueError as exc:
109+
raise ValueError("reference checkout path must stay within .reference") from exc
110+
if candidate == reference_root:
111+
raise ValueError("reference checkout path must identify a child of .reference")
112+
return candidate
113+
114+
86115
def _runner_key(pr_number: int, head_sha: str, provider: str) -> str:
87116
payload = f"{provider}:{pr_number}:{head_sha}"
88117
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
@@ -314,7 +343,7 @@ def _materialize_single_checkout_plan(
314343
)
315344
_run_git(["git", "-C", str(clone_dir), "sparse-checkout", "reapply"], env=git_env)
316345

317-
destination_root = workspace_path / checkout_path
346+
destination_root = _resolve_reference_checkout_path(workspace_path, checkout_path)
318347
if destination_root.exists():
319348
shutil.rmtree(destination_root)
320349
destination_root.mkdir(parents=True, exist_ok=True)
@@ -356,11 +385,6 @@ def materialize_orchestrator_skill(
356385
return None
357386

358387
if plan.pack:
359-
materialize_reference_packs(
360-
workspace_path,
361-
reference_pack_name=plan.pack,
362-
token=token,
363-
)
364388
reference_packs = _load_reference_packs_module()
365389
snapshot = reference_packs.load_reference_packs(workspace_path)
366390
matching = [
@@ -370,7 +394,17 @@ def materialize_orchestrator_skill(
370394
]
371395
if not matching:
372396
raise ValueError(f"orchestrator skill reference pack not found: {plan.pack}")
373-
checkout_path = workspace_path / matching[0].checkout_path
397+
checkout_path = _resolve_reference_checkout_path(
398+
workspace_path,
399+
matching[0].checkout_path,
400+
)
401+
with contextlib.suppress(FileNotFoundError):
402+
shutil.rmtree(checkout_path)
403+
materialize_reference_packs(
404+
workspace_path,
405+
reference_pack_name=plan.pack,
406+
token=token,
407+
)
374408
else:
375409
checkout_path = _materialize_single_checkout_plan(
376410
workspace_path,
@@ -413,12 +447,23 @@ def assemble_prompt(
413447
)
414448

415449
if context.get("materialize_orchestrator_skill"):
416-
materialize_orchestrator_skill(
450+
orchestrator_summary_path = materialize_orchestrator_skill(
417451
workspace,
418452
pack_override=context.get("orchestrator_skill_pack") or None,
419453
enabled_override=context.get("orchestrator_skill_enabled"),
420454
token=token,
421455
)
456+
else:
457+
orchestrator_summary_raw = context.get("orchestrator_skill_summary_path")
458+
orchestrator_summary_path = (
459+
Path(str(orchestrator_summary_raw)) if orchestrator_summary_raw else None
460+
)
461+
if orchestrator_summary_path:
462+
orchestrator_summary_path = _resolve_child_path(
463+
workspace,
464+
orchestrator_summary_path,
465+
description="orchestrator_skill_summary_path",
466+
)
422467

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

451-
orchestrator_summary = workspace / ".reference" / "ORCHESTRATOR_SKILL.md"
452-
if orchestrator_summary.is_file():
496+
if orchestrator_summary_path and orchestrator_summary_path.is_file():
453497
parts.extend(
454498
[
455499
"\n\n## Orchestrator Skill Context\n",
456-
_read_text(orchestrator_summary).rstrip(),
500+
_read_text(orchestrator_summary_path).rstrip(),
457501
]
458502
)
459503

@@ -504,7 +548,14 @@ def _parse_jsonl_output(raw_output: str) -> tuple[list[str], list[str]]:
504548
event_type = str(event.get("type") or event.get("status") or "").lower()
505549
text = _extract_text_from_json_event(event)
506550
if "error" in event_type or event.get("error"):
507-
errors.append(text or json.dumps(event, sort_keys=True))
551+
error_value = event.get("error")
552+
nested_error = (
553+
_extract_text_from_json_event(error_value)
554+
if isinstance(error_value, dict)
555+
else None
556+
)
557+
direct_error = error_value.strip() if isinstance(error_value, str) else None
558+
errors.append(direct_error or nested_error or text or json.dumps(event, sort_keys=True))
508559
elif text:
509560
messages.append(text)
510561
if not parsed_any:
@@ -522,7 +573,7 @@ def parse_runner_output(provider: str, raw_output: str) -> RunnerResult:
522573
clipped = raw[:64000] if len(raw) > 64000 else raw
523574

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

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

0 commit comments

Comments
 (0)