repro-engine 1.2.0__py3-none-any.whl

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.
Files changed (151) hide show
  1. repro/__init__.py +8 -0
  2. repro/__main__.py +3 -0
  3. repro/agents/__init__.py +67 -0
  4. repro/agents/backend.py +129 -0
  5. repro/agents/bias.py +109 -0
  6. repro/agents/cassette.py +413 -0
  7. repro/agents/cost.py +66 -0
  8. repro/agents/dispatch.py +148 -0
  9. repro/agents/events.py +165 -0
  10. repro/agents/evidence.py +254 -0
  11. repro/agents/gap.py +181 -0
  12. repro/agents/lockdown.py +131 -0
  13. repro/agents/openai_compat.py +474 -0
  14. repro/agents/prober.py +173 -0
  15. repro/agents/prompts/__init__.py +143 -0
  16. repro/agents/prompts/gap.md +20 -0
  17. repro/agents/prompts/lock.json +7 -0
  18. repro/agents/prompts/probe.md +22 -0
  19. repro/agents/prompts/qa.md +24 -0
  20. repro/agents/prompts/reader.md +28 -0
  21. repro/agents/prompts/reviewer.md +34 -0
  22. repro/agents/qa.py +173 -0
  23. repro/agents/reader.py +167 -0
  24. repro/agents/reviewer.py +191 -0
  25. repro/agents/sdk.py +236 -0
  26. repro/agents/setup.py +117 -0
  27. repro/agents/sources.py +102 -0
  28. repro/agents/stages.py +539 -0
  29. repro/agents/tools.py +231 -0
  30. repro/agents/untrusted.py +48 -0
  31. repro/agents/validate.py +169 -0
  32. repro/analysis/__init__.py +34 -0
  33. repro/analysis/_grimp_worker.py +66 -0
  34. repro/analysis/api.py +275 -0
  35. repro/analysis/blast.py +110 -0
  36. repro/analysis/changes.py +315 -0
  37. repro/analysis/imports.py +218 -0
  38. repro/analysis/modules.py +59 -0
  39. repro/analysis/static.py +54 -0
  40. repro/analysis/symbols.py +192 -0
  41. repro/api/__init__.py +5 -0
  42. repro/api/app.py +212 -0
  43. repro/api/cockpit/assets/JetBrainsMono-OFL.txt +93 -0
  44. repro/api/cockpit/assets/atlas.worker-CP1cHLLd.js +1 -0
  45. repro/api/cockpit/assets/demo-run-C9SGu-Og.jsonl +2334 -0
  46. repro/api/cockpit/assets/fleet-Bn3DZ83b.css +1 -0
  47. repro/api/cockpit/assets/fleet-C5arwxhB.js +1 -0
  48. repro/api/cockpit/assets/force-DkXbt76I.js +1 -0
  49. repro/api/cockpit/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  50. repro/api/cockpit/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  51. repro/api/cockpit/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  52. repro/api/cockpit/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  53. repro/api/cockpit/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  54. repro/api/cockpit/assets/jsxRuntime-B9Idad31.js +2 -0
  55. repro/api/cockpit/assets/jsxRuntime-DHHyguPz.css +1 -0
  56. repro/api/cockpit/assets/layout.worker-DLYbolgN.js +1 -0
  57. repro/api/cockpit/assets/main-BbRvaUOf.css +1 -0
  58. repro/api/cockpit/assets/main-CpPW1Zab.js +4376 -0
  59. repro/api/cockpit/assets/stress-run-TCZCEJwc.jsonl +18542 -0
  60. repro/api/cockpit/fleet.html +15 -0
  61. repro/api/cockpit/index.html +15 -0
  62. repro/api/stream.py +195 -0
  63. repro/api/token.py +48 -0
  64. repro/api/ui.py +17 -0
  65. repro/bus.py +150 -0
  66. repro/card/__init__.py +30 -0
  67. repro/card/assemble.py +224 -0
  68. repro/card/exports.py +329 -0
  69. repro/card/job.py +76 -0
  70. repro/card/markdown.py +175 -0
  71. repro/cli.py +923 -0
  72. repro/clock.py +15 -0
  73. repro/config/__init__.py +25 -0
  74. repro/config/settings.py +297 -0
  75. repro/doctor.py +461 -0
  76. repro/gitops/__init__.py +36 -0
  77. repro/gitops/changes.py +186 -0
  78. repro/gitops/git.py +121 -0
  79. repro/gitops/snapshot.py +398 -0
  80. repro/gitops/worktree.py +94 -0
  81. repro/ids.py +85 -0
  82. repro/integrations/__init__.py +1 -0
  83. repro/integrations/github/__init__.py +72 -0
  84. repro/integrations/github/action.py +183 -0
  85. repro/integrations/github/api.py +190 -0
  86. repro/integrations/github/mirror.py +104 -0
  87. repro/integrations/github/pr.py +127 -0
  88. repro/integrations/github/report.py +256 -0
  89. repro/integrations/mcp_server.py +224 -0
  90. repro/jsts.py +271 -0
  91. repro/logs.py +144 -0
  92. repro/orchestrator/__init__.py +43 -0
  93. repro/orchestrator/jobs.py +207 -0
  94. repro/orchestrator/runner.py +449 -0
  95. repro/paths.py +101 -0
  96. repro/pipeline.py +288 -0
  97. repro/policy/__init__.py +23 -0
  98. repro/policy/gap.py +173 -0
  99. repro/policy/invariants.py +379 -0
  100. repro/policy/loader.py +330 -0
  101. repro/policy/repro-yml.schema.json +100 -0
  102. repro/policy/stage.py +160 -0
  103. repro/policy/status.py +263 -0
  104. repro/provision/__init__.py +32 -0
  105. repro/provision/deps.py +274 -0
  106. repro/provision/env.py +541 -0
  107. repro/provision/project.py +416 -0
  108. repro/py.typed +0 -0
  109. repro/readable.py +73 -0
  110. repro/run_stage.py +1024 -0
  111. repro/runner/__init__.py +25 -0
  112. repro/runner/behaviour.py +341 -0
  113. repro/runner/observations.py +648 -0
  114. repro/runner/outcomes.py +193 -0
  115. repro/runner/perf.py +358 -0
  116. repro/runner/probes.py +681 -0
  117. repro/runner/pytest.py +549 -0
  118. repro/runner/pytest_config.py +125 -0
  119. repro/runner/selection.py +360 -0
  120. repro/runner/sides.py +63 -0
  121. repro/runner/testdiff.py +262 -0
  122. repro/runner/trace.py +195 -0
  123. repro/runner/twosided.py +435 -0
  124. repro/sandbox/__init__.py +30 -0
  125. repro/sandbox/_hook/repro_encode.py +734 -0
  126. repro/sandbox/_hook/repro_probe.py +77 -0
  127. repro/sandbox/_hook/repro_pytest.py +459 -0
  128. repro/sandbox/_hook/repro_trace.py +333 -0
  129. repro/sandbox/_hook/sitecustomize.py +199 -0
  130. repro/sandbox/_rlimits.py +55 -0
  131. repro/sandbox/detect.py +105 -0
  132. repro/sandbox/docker.py +462 -0
  133. repro/sandbox/evidence.py +68 -0
  134. repro/sandbox/image/Dockerfile +20 -0
  135. repro/sandbox/image.py +44 -0
  136. repro/sandbox/limits.py +33 -0
  137. repro/sandbox/process.py +336 -0
  138. repro/sandbox/winjob.py +242 -0
  139. repro/scrub.py +86 -0
  140. repro/store/__init__.py +24 -0
  141. repro/store/artifacts.py +145 -0
  142. repro/store/cbor.py +191 -0
  143. repro/store/db.py +118 -0
  144. repro/store/reader.py +132 -0
  145. repro/store/schema.py +165 -0
  146. repro/store/writer.py +452 -0
  147. repro/symbolic.py +288 -0
  148. repro_engine-1.2.0.dist-info/METADATA +196 -0
  149. repro_engine-1.2.0.dist-info/RECORD +151 -0
  150. repro_engine-1.2.0.dist-info/WHEEL +4 -0
  151. repro_engine-1.2.0.dist-info/entry_points.txt +3 -0
repro/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Repro engine."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("repro-engine")
7
+ except PackageNotFoundError: # imported from a source tree that was never installed
8
+ __version__ = "0+unknown"
repro/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from repro.cli import app
2
+
3
+ app(prog_name="repro")
@@ -0,0 +1,67 @@
1
+ """Reviewer agents: the LLM backend, its lockdown and dispatch (Stage G; ADR-0007).
2
+
3
+ ``LLMBackend`` is the one way the engine reaches a model; ``AgentSdkBackend`` is its default,
4
+ the Claude Agent SDK under the lockdown in ``lockdown``; ``OpenAICompatibleBackend`` reaches
5
+ Groq or a local server (ADR-0016); and ``CassetteBackend`` records calls and replays them
6
+ offline. Prompts are versioned files (``prompts``). ``run_agent`` runs a call in one of a run's
7
+ dispatch slots, within its ``RunLedger``, and reports it as events.
8
+ ``EvidenceTools`` are what agents read: the run's evidence and the change's files (G03).
9
+ """
10
+
11
+ from repro.agents.backend import (
12
+ AgentRequest,
13
+ AgentResult,
14
+ Budget,
15
+ LLMBackend,
16
+ OnProgress,
17
+ Outcome,
18
+ Progress,
19
+ ReproTool,
20
+ Role,
21
+ Usage,
22
+ )
23
+ from repro.agents.cassette import (
24
+ CassetteBackend,
25
+ CassetteError,
26
+ MissingCassetteError,
27
+ StaleCassetteError,
28
+ )
29
+ from repro.agents.cost import RunLedger
30
+ from repro.agents.dispatch import SLOTS, Dispatch, concurrency_for, default_concurrency
31
+ from repro.agents.events import run_agent
32
+ from repro.agents.evidence import EvidenceView
33
+ from repro.agents.openai_compat import Endpoint, OpenAICompatibleBackend
34
+ from repro.agents.sdk import AgentSdkBackend
35
+ from repro.agents.sources import RepoSources, SourceError
36
+ from repro.agents.tools import EvidenceTools, ToolInputError
37
+
38
+ __all__ = [
39
+ "SLOTS",
40
+ "AgentRequest",
41
+ "AgentResult",
42
+ "AgentSdkBackend",
43
+ "Budget",
44
+ "CassetteBackend",
45
+ "CassetteError",
46
+ "Dispatch",
47
+ "Endpoint",
48
+ "EvidenceTools",
49
+ "EvidenceView",
50
+ "LLMBackend",
51
+ "MissingCassetteError",
52
+ "OnProgress",
53
+ "OpenAICompatibleBackend",
54
+ "Outcome",
55
+ "Progress",
56
+ "RepoSources",
57
+ "ReproTool",
58
+ "Role",
59
+ "RunLedger",
60
+ "SourceError",
61
+ "StaleCassetteError",
62
+ "ToolInputError",
63
+ "Usage",
64
+ "concurrency_for",
65
+ "default_concurrency",
66
+ "run_agent",
67
+ ]
@@ -0,0 +1,129 @@
1
+ """The interface every LLM call goes through (G01; ADR-0007, ARCHITECTURE §10.5).
2
+
3
+ An agent call is a request (a role, a system prompt, the user's text, the read-only tools it
4
+ may call, an optional output schema, a model, the prompt's version and a budget) and a result
5
+ (the structured output or text, how the run ended, its usage). The default implementation is
6
+ the Claude Agent SDK (``repro.agents.sdk``); cassettes (``repro.agents.cassette``) and tests
7
+ implement the same protocol, so the stages that use agents never depend on how the model is
8
+ reached.
9
+
10
+ ``on_progress`` reports what the agent is doing while it runs (thinking, a tool call): the
11
+ ``agent.state`` events the cockpit's dispatch and workspace panels draw (``repro.agents.events``).
12
+ """
13
+
14
+ import re
15
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Final, Literal, Protocol
18
+
19
+ Role = Literal["read", "build", "review", "gap", "probe", "qa"]
20
+ #: How a run ended: ``ok`` with its output; or why there is none.
21
+ Outcome = Literal["ok", "max_turns", "max_budget", "timeout", "invalid_output", "error"]
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class Budget:
26
+ """What one agent call may spend; every call has one (ADR-0007)."""
27
+
28
+ max_turns: int = 8
29
+ max_budget_usd: float = 1.0
30
+ timeout_s: float = 600.0
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.max_turns < 1 or self.max_budget_usd <= 0 or self.timeout_s <= 0:
34
+ raise ValueError("a budget's limits must be positive")
35
+
36
+
37
+ #: A tool's name: words of lowercase letters and digits joined by single underscores.
38
+ TOOL_NAME: Final = re.compile(r"(?=.{1,48}$)[a-z][a-z0-9]*(?:_[a-z0-9]+)*")
39
+ #: A prompt's version: its file's name and version number, as ``reviewer/3`` (G02).
40
+ PROMPT_VERSION: Final = re.compile(r"[a-z][a-z0-9_-]{0,47}/[1-9][0-9]{0,5}")
41
+ #: A tool handler: its validated input, then the text it returns to the model.
42
+ ToolHandler = Callable[[Mapping[str, Any]], Awaitable[str]]
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class ReproTool:
47
+ """A read-only, in-process tool the agent may call (``mcp__repro__<name>``)."""
48
+
49
+ name: str
50
+ description: str
51
+ input_schema: Mapping[str, Any]
52
+ handler: ToolHandler
53
+
54
+ def __post_init__(self) -> None:
55
+ if not TOOL_NAME.fullmatch(self.name):
56
+ raise ValueError(f"tool names are lower_snake_case, up to 48 characters: {self.name!r}")
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class AgentRequest:
61
+ role: Role
62
+ system_prompt: str
63
+ prompt: str
64
+ model: str
65
+ #: The version of the prompt the call was built from (``repro.agents.prompts``).
66
+ prompt_version: str
67
+ budget: Budget = field(default_factory=Budget)
68
+ tools: Sequence[ReproTool] = ()
69
+ #: JSON Schema (draft-07, ``$ref``-free) the final answer must satisfy, or None for text.
70
+ output_schema: Mapping[str, Any] | None = None
71
+ effort: Literal["low", "medium", "high", "xhigh", "max"] | None = None
72
+
73
+ def __post_init__(self) -> None:
74
+ if not PROMPT_VERSION.fullmatch(self.prompt_version):
75
+ raise ValueError(f"a prompt version is name/number: {self.prompt_version!r}")
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class Usage:
80
+ """Client-side estimates, never billing data (research/04 §1.8)."""
81
+
82
+ input_tokens: int = 0
83
+ output_tokens: int = 0
84
+ cache_read_tokens: int = 0
85
+ cost_usd: float = 0.0
86
+ #: Whether ``cost_usd`` rests on a known price; False: the tokens are counted, the cost is
87
+ #: unknown (0 here, and no ``cost.estimate``; G10, ADR-0016).
88
+ priced: bool = True
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class AgentResult:
93
+ outcome: Outcome
94
+ #: The schema-valid answer (with an ``output_schema``), else None.
95
+ structured: Any = None
96
+ text: str = ""
97
+ turns: int = 0
98
+ usage: Usage = field(default_factory=Usage)
99
+ #: Why there is no output, in a line (empty when ``ok``).
100
+ detail: str = ""
101
+ #: Tool calls the model made, in order: (tool name, whether it was allowed to run).
102
+ tool_calls: tuple[tuple[str, bool], ...] = ()
103
+ #: ``usage`` per model the call used (subagents included, research/04 §1.8).
104
+ by_model: Mapping[str, Usage] = field(default_factory=dict)
105
+ #: Served from a cassette: ``usage`` is what the call cost when it was recorded.
106
+ replayed: bool = False
107
+
108
+
109
+ @dataclass(frozen=True, slots=True)
110
+ class Progress:
111
+ """What an agent is doing now (``agent.state``'s ``thinking`` and ``tool_call``)."""
112
+
113
+ state: Literal["thinking", "tool_call"]
114
+ #: The model turn it happened in, from 1 (a turn is one model response).
115
+ turn: int
116
+ tool: str | None = None
117
+ detail: str | None = None
118
+
119
+
120
+ OnProgress = Callable[[Progress], None]
121
+
122
+
123
+ class LLMBackend(Protocol):
124
+ """Runs one agent call to its end; never raises for what the model does (the result says
125
+ so), only for a backend that cannot run at all."""
126
+
127
+ async def run(
128
+ self, request: AgentRequest, on_progress: OnProgress | None = None
129
+ ) -> AgentResult: ...
repro/agents/bias.py ADDED
@@ -0,0 +1,109 @@
1
+ """Bias controls for the reviewers' verdicts (G08; ARCHITECTURE §10.4, research/04 §2.3).
2
+
3
+ - **Order swap.** LLM judges favour a position: in MT-Bench, GPT-4's verdicts survived swapping
4
+ the order of what it compared in 65 % of cases. A ``contradicted`` verdict flags the change,
5
+ so the reviewer is asked again with the evidence and the other claims in the opposite order
6
+ (``repro.agents.reviewer``'s ``reverse``); unless both orders give it, the verdict degrades
7
+ to ``unverifiable`` (``degraded``). The ``agent_order_swap`` setting can ask for the second
8
+ order on every verdict, which is how the flip rate is measured, or not at all.
9
+ - **Panel.** With ``agent_panel``, three reviewers judge each claim (their models in the
10
+ setting's order, the second with the order reversed) and a verdict stands when two of them
11
+ give it (``majority``); otherwise it is ``unverifiable``.
12
+ - **Judge ≠ author.** Judges favour their own generations (Panickssery et al.). A change's
13
+ commits and PR text often name the model that wrote it, as Claude Code's
14
+ ``Co-Authored-By: Claude …`` trailer does; when every reviewer is of that model's family,
15
+ the card says so (``judge_is_author``). The families are read from model names only
16
+ (``family``), never from people's names or addresses.
17
+
18
+ Every verdict records how many judgements it rests on and how many of them gave it
19
+ (``agreement``).
20
+ """
21
+
22
+ import re
23
+ from collections import Counter
24
+ from collections.abc import Iterable, Mapping, Sequence
25
+ from typing import Any, Final
26
+
27
+ #: Model-name tokens and the family (the developer of the model) each belongs to.
28
+ FAMILIES: Final[tuple[tuple[str, str], ...]] = (
29
+ ("claude", "anthropic"),
30
+ ("gpt", "openai"),
31
+ ("codex", "openai"),
32
+ ("chatgpt", "openai"),
33
+ ("gemini", "google"),
34
+ ("qwen", "alibaba"),
35
+ ("llama", "meta"),
36
+ ("deepseek", "deepseek"),
37
+ ("mistral", "mistral"),
38
+ )
39
+ #: The lines that name a change's author: a co-author trailer, a "generated with" line.
40
+ _AUTHOR_LINE: Final = re.compile(
41
+ r"^\s*(?:co-authored-by:|.*\bgenerated (?:with|by)\b)(?P<who>.*)$", re.IGNORECASE | re.MULTILINE
42
+ )
43
+ MAX_RATIONALE: Final = 4000
44
+
45
+
46
+ def family(model: str) -> str | None:
47
+ """The family a model name belongs to (``openai/gpt-oss-120b``: openai), if known."""
48
+ words = re.split(r"[^a-z0-9]+", model.lower())
49
+ for token, name in FAMILIES: # the token, or the token and a version (qwen3, gpt4o)
50
+ if any(re.fullmatch(rf"{token}(?:\d[\da-z]*)?", word) for word in words):
51
+ return name
52
+ return None
53
+
54
+
55
+ def author_families(texts: Iterable[str]) -> set[str]:
56
+ """The model families the texts name as the change's authors (co-author trailers and
57
+ "generated with" lines only)."""
58
+ found = set()
59
+ for text in texts:
60
+ for match in _AUTHOR_LINE.finditer(text):
61
+ named = family(match.group("who"))
62
+ if named is not None:
63
+ found.add(named)
64
+ return found
65
+
66
+
67
+ def judge_is_author(models: Sequence[str], authors: set[str]) -> str | None:
68
+ """The family every judge shares with the change's authors, or None."""
69
+ families = {family(model) for model in models}
70
+ if len(families) == 1:
71
+ (shared,) = families
72
+ if shared is not None and shared in authors:
73
+ return shared
74
+ return None
75
+
76
+
77
+ def agreeing(verdict: Mapping[str, Any], runs: int, agree: int) -> dict[str, Any]:
78
+ """``verdict`` with how many judgements it rests on and how many of them gave it."""
79
+ return {**verdict, "agreement": {"runs": runs, "agreeing": agree}}
80
+
81
+
82
+ def degraded(first: Mapping[str, Any], why: str, runs: int, agree: int) -> dict[str, Any]:
83
+ """``first`` kept as ``unverifiable``: its judges did not agree (``why``). It still cites
84
+ and quotes what the first judgement did."""
85
+ rationale = f"Not kept as {first['verdict']}: {why}. The first judgement: {first['rationale']}"
86
+ return {
87
+ **first,
88
+ "verdict": "unverifiable",
89
+ "rationale": rationale[:MAX_RATIONALE],
90
+ "agreement": {"runs": runs, "agreeing": agree},
91
+ }
92
+
93
+
94
+ def majority(verdicts: Sequence[Mapping[str, Any]], runs: int) -> dict[str, Any] | None:
95
+ """The first verdict whose value at least two of ``verdicts`` give, with its agreement;
96
+ None when no two agree."""
97
+ counts = Counter(v["verdict"] for v in verdicts)
98
+ for verdict in verdicts:
99
+ if counts[verdict["verdict"]] >= 2: # noqa: PLR2004 - two of three
100
+ return agreeing(verdict, runs, counts[verdict["verdict"]])
101
+ return None
102
+
103
+
104
+ def described(verdicts: Sequence[tuple[str, Mapping[str, Any] | str]]) -> str:
105
+ """What each judge answered, for a rationale: ``m1: supported; m2: rejected (…)``."""
106
+ return "; ".join(
107
+ f"{model}: {answer['verdict'] if isinstance(answer, Mapping) else answer}"
108
+ for model, answer in verdicts
109
+ )