Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7e4e5c5
Fix report schema literal typing
Rayfts Sep 18, 2026
b924b3d
Fix async iterator protocol typing and format
Rayfts Sep 18, 2026
fe8f16d
Add PyYAML typing stubs for strict mypy
Rayfts Sep 18, 2026
b4a1ba6
Fix GitHub URL typing and format
Rayfts Sep 18, 2026
3c6f0f8
Make host UID detection type-safe on Windows
Rayfts Sep 18, 2026
ebe9d0a
Format CLI operations
Rayfts Sep 18, 2026
4ed8155
Format GitHub report rendering
Rayfts Sep 18, 2026
c71ab40
Format report writer
Rayfts Sep 18, 2026
8e853ad
Format harness investigation
Rayfts Sep 18, 2026
4396a44
Format reproduction support
Rayfts Sep 18, 2026
7a1bed9
Format reproduction engine below source line limit
Rayfts Sep 18, 2026
5c13e90
Fix Ruff line-length failures
Rayfts Sep 18, 2026
1081f9d
Fix Ruff line-length failures
Rayfts Sep 18, 2026
5d8823d
Fix Ruff line-length failure
Rayfts Sep 18, 2026
784b299
Keep Docker sandbox under source line limit
Rayfts Sep 18, 2026
5f5ef46
Align Ruff check and formatter
Rayfts Sep 18, 2026
8a82eef
Align Ruff check and formatter
Rayfts Sep 18, 2026
0b14bc1
Align Ruff check and formatter
Rayfts Sep 18, 2026
61dde78
Fix Ruff unused noqa
Rayfts Sep 18, 2026
bb19d5e
Apply Ruff formatting to GitHub client
Rayfts Sep 18, 2026
8421665
Apply Ruff formatting to archival adapter
Rayfts Sep 18, 2026
150247c
Apply Ruff formatting to reproduction engine
Rayfts Sep 18, 2026
0f0b247
Remove unused Ruff suppressions
Rayfts Sep 18, 2026
0c8f0dd
Remove unused Ruff suppression
Rayfts Sep 18, 2026
c80776f
Remove final unused Ruff suppression
Rayfts Sep 18, 2026
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dev = [
"pytest>=8.3,<10",
"pytest-asyncio>=0.25,<2",
"ruff>=0.11,<1",
"types-PyYAML>=6.0.12.20250516",
]

[project.scripts]
Expand Down
6 changes: 1 addition & 5 deletions src/reproforge/cli/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,7 @@ async def _execute_and_archive(
) -> ReproductionReport:
report = await ReproductionEngine(config).run(request, workspace=workspace)
selected_environment = config.selected_environment()
secret_values = tuple(
value
for key, value in selected_environment.items()
if key in config.security.allowed_secrets
)
secret_values = tuple(value for key, value in selected_environment.items() if key in config.security.allowed_secrets)
archive = archive_run(workspace, report, secret_values=secret_values)
store().upsert(report.run_id, archive, report.status.value)
print_report(report)
Expand Down
2 changes: 1 addition & 1 deletion src/reproforge/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator

REPORT_SCHEMA_VERSION = "1.0"
REPORT_SCHEMA_VERSION: Literal["1.0"] = "1.0"


class StrictModel(BaseModel):
Expand Down
18 changes: 9 additions & 9 deletions src/reproforge/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ def parse_issue_url(url: str) -> IssueRef:
match = _ISSUE_RE.match(url.strip())
if not match:
raise ValueError("expected a GitHub issue URL like https://github.com/org/repo/issues/123")
return IssueRef(
owner=match.group("owner"),
repo=match.group("repo"),
number=int(match.group("number")),
url=url,
return IssueRef.model_validate(
{
"owner": match.group("owner"),
"repo": match.group("repo"),
"number": int(match.group("number")),
"url": url,
}
)

async def fetch_issue(self, url: str) -> IssueMetadata:
Expand All @@ -63,13 +65,11 @@ async def fetch_issue(self, url: str) -> IssueMetadata:
)
if issue_response.status_code != 200:
raise GitHubError(
f"GitHub issue lookup failed with HTTP {issue_response.status_code}: "
f"{_safe_message(issue_response)}"
f"GitHub issue lookup failed with HTTP {issue_response.status_code}: {_safe_message(issue_response)}"
)
if repo_response.status_code != 200:
raise GitHubError(
f"GitHub repository lookup failed with HTTP {repo_response.status_code}: "
f"{_safe_message(repo_response)}"
f"GitHub repository lookup failed with HTTP {repo_response.status_code}: {_safe_message(repo_response)}"
)
issue = issue_response.json()
repo = repo_response.json()
Expand Down
7 changes: 3 additions & 4 deletions src/reproforge/harnesses/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class HarnessInvocation(StrictModel):
class HarnessRuntime(Protocol):
"""Execution boundary supplied by ReproForge's sandbox layer."""

async def stream(
def stream(
self,
invocation: HarnessInvocation,
*,
Expand All @@ -96,7 +96,7 @@ async def prepare(self, request: HarnessRunRequest) -> None: ...

def build_invocation(self, request: HarnessRunRequest) -> HarnessInvocation: ...

async def run(self, request: HarnessRunRequest) -> AsyncIterator[HarnessEvent]: ...
def run(self, request: HarnessRunRequest) -> AsyncIterator[HarnessEvent]: ...

async def cancel(self) -> None: ...

Expand Down Expand Up @@ -189,8 +189,7 @@ async def prepare(self, request: HarnessRunRequest) -> None:
def build_invocation(self, request: HarnessRunRequest) -> HarnessInvocation:
del request
raise RuntimeError(
f"{self.id} has no current supported headless launcher; "
"historical artifacts must be imported explicitly"
f"{self.id} has no current supported headless launcher; historical artifacts must be imported explicitly"
)

async def run(self, request: HarnessRunRequest) -> AsyncIterator[HarnessEvent]:
Expand Down
3 changes: 1 addition & 2 deletions src/reproforge/reports/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ def render_issue_comment(report: ReproductionReport) -> str:
[
"",
"**Observed failing command**",
f"`{' '.join(failing.command.argv)}` — "
f"{'timed out' if failing.timed_out else f'exit {failing.exit_code}'}",
f"`{' '.join(failing.command.argv)}` — {'timed out' if failing.timed_out else f'exit {failing.exit_code}'}",
]
)

Expand Down
11 changes: 2 additions & 9 deletions src/reproforge/reports/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,7 @@ def _write_commands(
report: ReproductionReport,
secret_values: tuple[str, ...],
) -> None:
lines = [
_jsonl_command(command, metadata, secret_values)
for metadata, command in _iter_commands(report)
]
lines = [_jsonl_command(command, metadata, secret_values) for metadata, command in _iter_commands(report)]
path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")


Expand Down Expand Up @@ -170,11 +167,7 @@ def _write_reproduction(
attempts = report.attempts
fallback = attempts[0] if attempts else None
source_attempt = next((attempt for attempt in attempts if attempt.reproduced), fallback)
commands = (
[]
if source_attempt is None
else [result.command.model_dump(mode="json") for result in source_attempt.commands]
)
commands = [] if source_attempt is None else [result.command.model_dump(mode="json") for result in source_attempt.commands]
_write_json(directory / "commands.json", commands, secret_values)
if report.minimized_reproduction:
(directory / "minimized.txt").write_text(
Expand Down
24 changes: 5 additions & 19 deletions src/reproforge/reproduction/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ async def run(
revision = await safe_revision(repository)
environment = environment_snapshot(self.config, inspection, revision)
selected_environment = self.config.selected_environment()
secret_values = tuple(
value
for key, value in selected_environment.items()
if key in self.config.security.allowed_secrets
)
secret_values = tuple(value for key, value in selected_environment.items() if key in self.config.security.allowed_secrets)

policy_error = runtime_policy_error(self.config)
if policy_error is not None:
Expand Down Expand Up @@ -142,10 +138,8 @@ async def run(
started=started,
environment=environment,
status=RunStatus.SETUP_FAILURE,
summary=(
"Baseline build/check failed before reproduction investigation: "
+ " ".join(failed_baseline.command.argv)
),
summary="Baseline build/check failed before reproduction investigation: "
+ " ".join(failed_baseline.command.argv),
setup_results=setup_results,
baseline_results=baseline_results,
caveats=[
Expand Down Expand Up @@ -267,11 +261,7 @@ async def _attempt(
commands: list[CommandSpec],
) -> AttemptRecord:
results = await run_commands(session, commands)
signals = [
signal
for index, result in enumerate(results)
for signal in inspect_command(result, command_index=index)
]
signals = [signal for index, result in enumerate(results) for signal in inspect_command(result, command_index=index)]
return AttemptRecord(attempt=number, commands=results, signals=signals, reproduced=bool(signals))

async def _maybe_minimize(
Expand Down Expand Up @@ -304,11 +294,7 @@ async def _minimize_commands(
async def preserves(candidate: Sequence[CommandSpec]) -> bool:
await self._reset_trial(session, snapshot)
results = await run_commands(session, list(candidate))
signals = [
signal
for index, result in enumerate(results)
for signal in inspect_command(result, command_index=index)
]
signals = [signal for index, result in enumerate(results) for signal in inspect_command(result, command_index=index)]
return any(signal.fingerprint == fingerprint for signal in signals)

return await ddmin(commands, preserves, max_trials=self.config.minimization.max_trials)
4 changes: 1 addition & 3 deletions src/reproforge/reproduction/investigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ async def investigate_harness(

plan = read_harness_plan(plan_path)
if plan is None:
caveats.append(
"Harness did not emit .reproforge/harness-plan.json; repository-detected test commands were used."
)
caveats.append("Harness did not emit .reproforge/harness-plan.json; repository-detected test commands were used.")
plan = HarnessPlan()
interpretation = plan.interpretation or ("\n".join(messages[-5:]) if messages else None)
return plan, interpretation, caveats, runtime.results
Expand Down
5 changes: 1 addition & 4 deletions src/reproforge/reproduction/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,7 @@ def workspace_policy_error(config: ReproForgeConfig, workspace: Path) -> str | N
for raw_path in forbidden_paths:
forbidden = Path(raw_path).expanduser().resolve(strict=False)
if _paths_intersect(resolved_workspace, forbidden):
return (
f"Workspace {resolved_workspace} intersects forbidden host path {forbidden}; "
"refusing to inspect or mount it."
)
return f"Workspace {resolved_workspace} intersects forbidden host path {forbidden}; refusing to inspect or mount it."
return None


Expand Down
8 changes: 5 additions & 3 deletions src/reproforge/sandbox/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@


class DockerSession:
"""Long-lived disposable container for one reproduction run."""

def __init__(
self,
backend: DockerSandbox,
Expand Down Expand Up @@ -295,4 +293,8 @@ def _host_user() -> str | None:

if os.name == "nt":
return None
return f"{os.getuid()}:{os.getgid()}"
getuid = getattr(os, "getuid", None)
getgid = getattr(os, "getgid", None)
if getuid is None or getgid is None:
return None
return f"{getuid()}:{getgid()}"
Loading