codereview-loop 0.13.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.
- codereview/__init__.py +6 -0
- codereview/__main__.py +4 -0
- codereview/acceptance.py +649 -0
- codereview/agentsmd.py +259 -0
- codereview/bench.py +881 -0
- codereview/claims.py +182 -0
- codereview/cli.py +2927 -0
- codereview/core.py +9070 -0
- codereview/crash.py +247 -0
- codereview/discovery.py +289 -0
- codereview/disputes.py +749 -0
- codereview/diversity.py +183 -0
- codereview/evidence.py +1023 -0
- codereview/feedback.py +416 -0
- codereview/github.py +286 -0
- codereview/hardening.py +45 -0
- codereview/herdrhost.py +373 -0
- codereview/hooks.py +261 -0
- codereview/intake.py +208 -0
- codereview/mcp_tools.py +455 -0
- codereview/mcp_transport.py +272 -0
- codereview/notify.py +237 -0
- codereview/pilot.py +549 -0
- codereview/policy.py +1086 -0
- codereview/probe.py +534 -0
- codereview/providers.py +1681 -0
- codereview/record_schema.py +904 -0
- codereview/registry.py +294 -0
- codereview/risk.py +368 -0
- codereview/sarif.py +309 -0
- codereview/seatstate.py +398 -0
- codereview/setup_wizard.py +355 -0
- codereview/support.py +328 -0
- codereview/techlead.py +1041 -0
- codereview/tiering.py +265 -0
- codereview/userconfig.py +307 -0
- codereview_loop-0.13.0.dist-info/METADATA +502 -0
- codereview_loop-0.13.0.dist-info/RECORD +44 -0
- codereview_loop-0.13.0.dist-info/WHEEL +5 -0
- codereview_loop-0.13.0.dist-info/entry_points.txt +3 -0
- codereview_loop-0.13.0.dist-info/licenses/LICENSE +21 -0
- codereview_loop-0.13.0.dist-info/top_level.txt +2 -0
- reviewloop/__init__.py +6 -0
- reviewloop/__main__.py +6 -0
codereview/agentsmd.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codereview AGENTS.md ingestion — project guidance as reviewer
|
|
3
|
+
context (Epic L1).
|
|
4
|
+
|
|
5
|
+
At run start the reviewer panel gets the repository's own
|
|
6
|
+
guidance: `AGENTS.md` at the repo root and nested along the
|
|
7
|
+
changed paths, loaded as context ALONGSIDE (never instead of)
|
|
8
|
+
`.codereview.toml`. Every target repo in the audience already
|
|
9
|
+
carries the file; before L1 the gate never read it, so project
|
|
10
|
+
rules a human reviewer would apply were invisible to the panel.
|
|
11
|
+
|
|
12
|
+
Trust model — the file is UNTRUSTED content, on exactly the
|
|
13
|
+
diff's terms:
|
|
14
|
+
|
|
15
|
+
* It passes through the review path's own redaction
|
|
16
|
+
(`core.redact_secrets`) before anything is sent to a
|
|
17
|
+
provider.
|
|
18
|
+
* It is delimited in the prompt as data (see core's
|
|
19
|
+
`_AGENTS_CONTEXT_HEADER`), never as instructions to the
|
|
20
|
+
harness: text inside it addressed to the reviewer or the
|
|
21
|
+
harness is ignored by construction of the prompt, and —
|
|
22
|
+
the load-bearing half — nothing in this module or in the
|
|
23
|
+
prompt path lets the file touch a gate rule, the panel
|
|
24
|
+
composition, or a record field. The TOML policy remains
|
|
25
|
+
the only machine-enforced layer; AGENTS.md content can
|
|
26
|
+
inform a finding, never change an outcome.
|
|
27
|
+
* A `## Code Review Rules` section is surfaced at the head
|
|
28
|
+
of the attached context as advisory review rules the panel
|
|
29
|
+
may cite in findings (quoting the rule's text in the
|
|
30
|
+
finding's evidence; the evidence_quote itself still comes
|
|
31
|
+
from the diff, as always).
|
|
32
|
+
|
|
33
|
+
Bounds and provenance:
|
|
34
|
+
|
|
35
|
+
* Size-capped per file (AGENTS_PER_FILE_CAP) and in total
|
|
36
|
+
(AGENTS_TOTAL_CAP); truncation is marked inside the text
|
|
37
|
+
and recorded per file — nothing is silently dropped: the
|
|
38
|
+
collector returns a ledger entry for every discovered
|
|
39
|
+
file, including skipped ones, with the reason.
|
|
40
|
+
* Provenance is written to the run record by run_review:
|
|
41
|
+
per ingested file, its repo-relative path and the sha256
|
|
42
|
+
of the file's raw bytes as read from disk (pre-redaction,
|
|
43
|
+
pre-cap — the hash identifies the source file, not the
|
|
44
|
+
excerpt).
|
|
45
|
+
|
|
46
|
+
Ordering — nearest-wins: discovered files are ordered from
|
|
47
|
+
the repo root toward the changed code (directory depth, then
|
|
48
|
+
path). Where two files give conflicting guidance about a
|
|
49
|
+
changed file, the AGENTS.md NEAREST that file's directory
|
|
50
|
+
governs; the prompt states the rule and the order it is
|
|
51
|
+
shown. Discovery itself is deterministic: the root plus the
|
|
52
|
+
ancestor directories of the patch's changed paths — never a
|
|
53
|
+
whole-tree scan, so a file off every changed path is not
|
|
54
|
+
ingested.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import hashlib
|
|
60
|
+
import os
|
|
61
|
+
import re
|
|
62
|
+
|
|
63
|
+
from .core import (_archived_rule, _is_within, redact_secrets,
|
|
64
|
+
split_patch_sections)
|
|
65
|
+
|
|
66
|
+
AGENTS_FILENAME = "AGENTS.md"
|
|
67
|
+
AGENTS_PER_FILE_CAP = 16000
|
|
68
|
+
AGENTS_TOTAL_CAP = 32000
|
|
69
|
+
# Below this much remaining budget a further file is skipped
|
|
70
|
+
# outright instead of sending a sliver (same floor the file
|
|
71
|
+
# context collector uses).
|
|
72
|
+
_MIN_BUDGET_ROOM = 1000
|
|
73
|
+
|
|
74
|
+
_RULES_HEADING = re.compile(r"^##\s+Code Review Rules\s*$",
|
|
75
|
+
re.IGNORECASE)
|
|
76
|
+
_SECTION_END = re.compile(r"^#{1,2}\s")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_rules_section(text):
|
|
80
|
+
"""The body of the `## Code Review Rules` section, or None.
|
|
81
|
+
|
|
82
|
+
The section runs from its heading to the next level-1 or
|
|
83
|
+
level-2 heading (or end of file); deeper sub-headings stay
|
|
84
|
+
part of the section. The heading line itself is not part
|
|
85
|
+
of the returned body — the prompt attributes the quote to
|
|
86
|
+
its file explicitly.
|
|
87
|
+
"""
|
|
88
|
+
lines = text.splitlines()
|
|
89
|
+
start = None
|
|
90
|
+
for i, line in enumerate(lines):
|
|
91
|
+
if _RULES_HEADING.match(line.strip()):
|
|
92
|
+
start = i + 1
|
|
93
|
+
break
|
|
94
|
+
if start is None:
|
|
95
|
+
return None
|
|
96
|
+
body = []
|
|
97
|
+
for line in lines[start:]:
|
|
98
|
+
if _SECTION_END.match(line.strip()):
|
|
99
|
+
break
|
|
100
|
+
body.append(line)
|
|
101
|
+
out = "\n".join(body).strip("\n")
|
|
102
|
+
return out if out.strip() else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _ancestor_dirs(changed_paths):
|
|
106
|
+
"""Repo-relative directories to look for AGENTS.md in: the
|
|
107
|
+
root plus every ancestor directory of every changed path."""
|
|
108
|
+
dirs = {""}
|
|
109
|
+
for path in changed_paths:
|
|
110
|
+
parts = [p for p in str(path).split("/")
|
|
111
|
+
if p not in ("", ".")]
|
|
112
|
+
for i in range(len(parts)):
|
|
113
|
+
dirs.add("/".join(parts[:i]))
|
|
114
|
+
return dirs
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def discover_agents_files(repo, changed_paths):
|
|
118
|
+
"""The deterministic discovery list: repo-relative paths of
|
|
119
|
+
the AGENTS.md files that exist at the root or nested along
|
|
120
|
+
`changed_paths`, ordered nearest-wins (root first, then by
|
|
121
|
+
directory depth, then path — later in the list is nearer
|
|
122
|
+
the changed code and governs on conflict)."""
|
|
123
|
+
found = []
|
|
124
|
+
for d in _ancestor_dirs(changed_paths):
|
|
125
|
+
rel = f"{d}/{AGENTS_FILENAME}" if d else AGENTS_FILENAME
|
|
126
|
+
if os.path.isfile(os.path.join(repo, *rel.split("/"))):
|
|
127
|
+
found.append(rel)
|
|
128
|
+
found.sort(key=lambda p: (p.count("/"), p))
|
|
129
|
+
return found
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def collect_agents_context(repo, patch,
|
|
133
|
+
per_file_cap=AGENTS_PER_FILE_CAP,
|
|
134
|
+
total_cap=AGENTS_TOTAL_CAP):
|
|
135
|
+
"""Collect the AGENTS.md context for a review of `patch`.
|
|
136
|
+
|
|
137
|
+
Returns (payload, entries). `entries` is the ledger — one
|
|
138
|
+
dict per discovered file stating its provenance (sha256 of
|
|
139
|
+
the raw bytes), size, how much was sent, and any truncation
|
|
140
|
+
or skip reason. `payload` is None when no file was ingested
|
|
141
|
+
(no AGENTS.md on any changed path, or every discovered file
|
|
142
|
+
skipped): the caller then passes nothing and the run is
|
|
143
|
+
byte-identical to the pre-L1 behaviour. Otherwise payload
|
|
144
|
+
is {"files": [ledger entries of the ingested files, in
|
|
145
|
+
prompt order], "total_chars": chars of file text sent,
|
|
146
|
+
"truncated": bool, "prompt_text": the delimited section
|
|
147
|
+
run_review appends to each reviewer prompt}.
|
|
148
|
+
|
|
149
|
+
Assembly under the total cap: the surfaced `## Code Review
|
|
150
|
+
Rules` blocks are the highest-value part, so they are
|
|
151
|
+
charged against the budget first; full file texts fill the
|
|
152
|
+
remaining budget in discovery order. A file whose text no
|
|
153
|
+
longer fits is sent truncated to the remaining room, or —
|
|
154
|
+
below the room floor — not sent at all; either way its
|
|
155
|
+
ledger entry records truncated="budget" and the file still
|
|
156
|
+
appears in `entries` (and its rules block, if any, has
|
|
157
|
+
already been charged).
|
|
158
|
+
"""
|
|
159
|
+
changed = [name for name, _text in split_patch_sections(patch)
|
|
160
|
+
if name and name != "unknown"]
|
|
161
|
+
entries = []
|
|
162
|
+
candidates = [] # (entry, redacted_text, rules_body)
|
|
163
|
+
for rel in discover_agents_files(repo, changed):
|
|
164
|
+
entry = {"path": rel, "sha256": None, "chars": 0,
|
|
165
|
+
"sent_chars": 0, "truncated": None,
|
|
166
|
+
"skipped": None, "rules_section": False}
|
|
167
|
+
entries.append(entry)
|
|
168
|
+
if _archived_rule(rel):
|
|
169
|
+
entry["skipped"] = "archived"
|
|
170
|
+
continue
|
|
171
|
+
full = os.path.realpath(
|
|
172
|
+
os.path.join(repo, *rel.split("/")))
|
|
173
|
+
root = os.path.realpath(repo)
|
|
174
|
+
if not _is_within(full, root):
|
|
175
|
+
entry["skipped"] = "outside_repo"
|
|
176
|
+
continue
|
|
177
|
+
try:
|
|
178
|
+
with open(full, "rb") as fh:
|
|
179
|
+
raw = fh.read()
|
|
180
|
+
except OSError:
|
|
181
|
+
entry["skipped"] = "unreadable"
|
|
182
|
+
continue
|
|
183
|
+
entry["sha256"] = hashlib.sha256(raw).hexdigest()
|
|
184
|
+
text = raw.decode("utf-8", "replace")
|
|
185
|
+
if "\x00" in text:
|
|
186
|
+
entry["skipped"] = "binary"
|
|
187
|
+
continue
|
|
188
|
+
entry["chars"] = len(text)
|
|
189
|
+
clean = redact_secrets(text)
|
|
190
|
+
rules = extract_rules_section(clean)
|
|
191
|
+
entry["rules_section"] = rules is not None
|
|
192
|
+
candidates.append((entry, clean, rules))
|
|
193
|
+
|
|
194
|
+
used = 0
|
|
195
|
+
contributed = set() # ids of entries that reached the prompt
|
|
196
|
+
rules_blocks = []
|
|
197
|
+
for entry, _clean, rules in candidates:
|
|
198
|
+
if not rules:
|
|
199
|
+
continue
|
|
200
|
+
block = f"From {entry['path']}:\n{rules}"
|
|
201
|
+
room = total_cap - used
|
|
202
|
+
if len(block) > room:
|
|
203
|
+
if room < _MIN_BUDGET_ROOM:
|
|
204
|
+
entry["truncated"] = "budget"
|
|
205
|
+
continue
|
|
206
|
+
block = (rules[:room] + "\n[codereview: advisory "
|
|
207
|
+
"rules truncated — showing the first "
|
|
208
|
+
f"{room} chars]")
|
|
209
|
+
entry["truncated"] = "budget"
|
|
210
|
+
rules_blocks.append(block)
|
|
211
|
+
contributed.add(id(entry))
|
|
212
|
+
used += len(block) + 2 # joiner slack
|
|
213
|
+
|
|
214
|
+
file_blocks = []
|
|
215
|
+
for entry, clean, _rules in candidates:
|
|
216
|
+
sent = clean
|
|
217
|
+
if len(sent) > per_file_cap:
|
|
218
|
+
sent = sent[:per_file_cap] + (
|
|
219
|
+
"\n[codereview: AGENTS.md context truncated — "
|
|
220
|
+
f"showing the first {per_file_cap} of "
|
|
221
|
+
f"{len(clean)} chars]")
|
|
222
|
+
entry["truncated"] = "file_cap"
|
|
223
|
+
room = total_cap - used
|
|
224
|
+
if len(sent) > room:
|
|
225
|
+
if room < _MIN_BUDGET_ROOM:
|
|
226
|
+
entry["truncated"] = "budget"
|
|
227
|
+
entry["sent_chars"] = 0
|
|
228
|
+
continue
|
|
229
|
+
sent = clean[:room] + (
|
|
230
|
+
"\n[codereview: AGENTS.md context truncated — "
|
|
231
|
+
f"showing the first {room} of {len(clean)} "
|
|
232
|
+
"chars]")
|
|
233
|
+
entry["truncated"] = "budget"
|
|
234
|
+
used += len(sent) + 2 # joiner slack
|
|
235
|
+
file_blocks.append(f"===== {entry['path']} =====\n{sent}")
|
|
236
|
+
entry["sent_chars"] = len(sent)
|
|
237
|
+
contributed.add(id(entry))
|
|
238
|
+
|
|
239
|
+
if not contributed:
|
|
240
|
+
return None, entries
|
|
241
|
+
parts = []
|
|
242
|
+
if rules_blocks:
|
|
243
|
+
parts.append(
|
|
244
|
+
"Advisory review rules (## Code Review Rules "
|
|
245
|
+
"sections, quoted verbatim):\n\n"
|
|
246
|
+
+ "\n\n".join(rules_blocks))
|
|
247
|
+
parts.extend(file_blocks)
|
|
248
|
+
# Provenance covers every file that reached the prompt —
|
|
249
|
+
# via its full text, its rules block, or both — in
|
|
250
|
+
# discovery (nearest-wins) order.
|
|
251
|
+
ingested = [e for e, _c, _r in candidates
|
|
252
|
+
if id(e) in contributed]
|
|
253
|
+
payload = {
|
|
254
|
+
"files": ingested,
|
|
255
|
+
"total_chars": sum(e["sent_chars"] for e in ingested),
|
|
256
|
+
"truncated": any(e["truncated"] for e in entries),
|
|
257
|
+
"prompt_text": "\n\n".join(parts),
|
|
258
|
+
}
|
|
259
|
+
return payload, entries
|