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/seatstate.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codereview seat-state supervision (Epic K2).
|
|
3
|
+
|
|
4
|
+
Every seat in a run carries a live state:
|
|
5
|
+
|
|
6
|
+
pending -> working -> done | blocked:<class> | failed | timed-out
|
|
7
|
+
|
|
8
|
+
with blocked classes quota, auth, input, stalled. The state is
|
|
9
|
+
(a) emitted live through a progress callback (the CLI prints it),
|
|
10
|
+
and (b) persisted into the run record per seat (`seat_state` on
|
|
11
|
+
each reviewer report), next to the existing error_detail
|
|
12
|
+
machinery. A blocked seat is a failed job: it degrades the panel
|
|
13
|
+
through the ordinary reviewer-failure path — never silently
|
|
14
|
+
skipped, never a silent PASS.
|
|
15
|
+
|
|
16
|
+
Detection is OURS, over the seat subprocess's own signals —
|
|
17
|
+
exit code, captured stdout/stderr, output activity, timeout.
|
|
18
|
+
The failure-text table below is per-CLI and probe-grounded
|
|
19
|
+
(Epic K spec, U6 probe matrix, 2026-11-05): every pattern cites
|
|
20
|
+
the evidence it was taken from, and a CLI with no verified text
|
|
21
|
+
for a condition gets NO pattern for it — the seat then relies on
|
|
22
|
+
process signals alone. Herdr pane state is never an input here
|
|
23
|
+
(K3a territory: seats are not herdr-hosted yet, so there is no
|
|
24
|
+
pane to read; when hosting lands, pane state may be recorded as
|
|
25
|
+
an advisory annotation, never as the basis of a classification).
|
|
26
|
+
|
|
27
|
+
Vocabulary notes:
|
|
28
|
+
|
|
29
|
+
* A `blocked:stalled` EPISODE (no output growth for
|
|
30
|
+
STALL_AFTER_S) is a liveness signal, not a failure: a seat
|
|
31
|
+
that prints only its final answer (Claude Code's -p mode)
|
|
32
|
+
is silent while it thinks. The episode is recorded in the
|
|
33
|
+
transitions and the seat returns to `working` if output
|
|
34
|
+
resumes; `blocked:stalled` becomes the TERMINAL state only
|
|
35
|
+
when the run's timeout kills a seat that has been silent
|
|
36
|
+
past the bound.
|
|
37
|
+
* `blocked:input` exists in the vocabulary with its detection
|
|
38
|
+
path wired (a verified prompt pattern matched on stderr
|
|
39
|
+
while the process waits) but has NO patterns today: no
|
|
40
|
+
U6 probe produced verified interactive-prompt text for any
|
|
41
|
+
of the three CLIs, and patterns are never invented.
|
|
42
|
+
* Patterns are matched against stderr while the seat runs
|
|
43
|
+
(stderr is the CLI's diagnostics channel — never the verdict
|
|
44
|
+
channel) and, once the process has exited non-zero, against
|
|
45
|
+
stderr then stdout (Claude Code prints in-run failures as
|
|
46
|
+
the result on stdout; a successful run's stdout is never
|
|
47
|
+
scanned, so a verdict quoting failure text from the diff
|
|
48
|
+
under review cannot misclassify its own seat).
|
|
49
|
+
* Matching is ANCHORED to the CLI's own voice (Phase 4,
|
|
50
|
+
P4-06): a pattern counts only when, on its line, the text
|
|
51
|
+
before the match is an unquoted lead-in — timestamps, a log
|
|
52
|
+
level, a logger path, the vendor's own lead-ins ("You've",
|
|
53
|
+
"HTTP error:") — never quoted matter. The verbatim vendor
|
|
54
|
+
messages carry such lead-ins, so position-0 matching would
|
|
55
|
+
reject the very evidence the table ships on; what the
|
|
56
|
+
anchor rejects is the core appearing inside quotes: a
|
|
57
|
+
warning about a named component ("MCP server 'docs' not
|
|
58
|
+
logged in") states the COMPONENT's state, not the CLI's,
|
|
59
|
+
and verdict or diff text quoting the words is content
|
|
60
|
+
under review. An unanchored substring match killed a
|
|
61
|
+
healthy seat on exactly that warning.
|
|
62
|
+
* JSON error envelopes (Phase 4 rerun, P4-06b): a line that
|
|
63
|
+
is a JSON object reporting a failure — {"is_error": true,
|
|
64
|
+
...} (Claude Code's result envelope) or {"error": ...}
|
|
65
|
+
with no verdict fields — is scanned by its decoded string
|
|
66
|
+
VALUES, so the CLI's own message is not rejected merely
|
|
67
|
+
for sitting inside JSON quotes. Keys are never scanned,
|
|
68
|
+
values still face the lead-in test, and a verdict object
|
|
69
|
+
is never value-scanned: its free text quotes the diff.
|
|
70
|
+
* Quotation contexts (P4-06c): a line whose first non-space
|
|
71
|
+
character is '+', '>' or '|' is diff or markdown content,
|
|
72
|
+
and a lead-in containing a code call (print(...)) is
|
|
73
|
+
echoed source — neither is the CLI speaking. '-' is NOT
|
|
74
|
+
disqualified: it is also an ordinary vendor bullet.
|
|
75
|
+
* ACCEPTED RESIDUAL (P4-06c, adopted with the tester's
|
|
76
|
+
rerun solution): the bare, UNQUOTED third-party state
|
|
77
|
+
report — "MCP server docs not logged in; continuing" —
|
|
78
|
+
still matches. No lead-in rule separates it from a
|
|
79
|
+
genuine CLI message, because prose lead-ins are
|
|
80
|
+
load-bearing for verified vendor captures (codex's
|
|
81
|
+
"unexpected status 401 Unauthorized: ..." carries one),
|
|
82
|
+
so the anchor cannot demand clause-initial position. The
|
|
83
|
+
one discriminator that would catch this exact string — a
|
|
84
|
+
negative lookahead for "; continuing" — encodes a single
|
|
85
|
+
vendor's phrasing rather than a property of failure text,
|
|
86
|
+
and was considered and rejected on that basis. The quoted
|
|
87
|
+
form ('docs') is rejected by the anchor above; only the
|
|
88
|
+
unquoted form remains, and only while the seat then stays
|
|
89
|
+
silent for the full block grace does it cost the seat.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
from __future__ import annotations
|
|
93
|
+
|
|
94
|
+
import contextvars
|
|
95
|
+
import json
|
|
96
|
+
import os
|
|
97
|
+
import re
|
|
98
|
+
import threading
|
|
99
|
+
import time
|
|
100
|
+
|
|
101
|
+
STATES = ("pending", "working", "blocked", "done", "failed",
|
|
102
|
+
"timed-out")
|
|
103
|
+
BLOCKED_CLASSES = ("quota", "auth", "input", "stalled")
|
|
104
|
+
|
|
105
|
+
# Live-supervision bounds (seconds). STALL_AFTER_S is the
|
|
106
|
+
# no-output bound for a stalled episode / a stalled terminal at
|
|
107
|
+
# timeout; BLOCK_GRACE_S is how long a matched blocked pattern
|
|
108
|
+
# must be followed by total output silence before the seat is
|
|
109
|
+
# terminated early (the CLI's own retry bursts keep output
|
|
110
|
+
# growing, so a genuinely stuck seat is what reaches the grace).
|
|
111
|
+
STALL_AFTER_S = 180.0
|
|
112
|
+
BLOCK_GRACE_S = 10.0
|
|
113
|
+
|
|
114
|
+
# ----------------------------------------------------------------
|
|
115
|
+
# U6 failure-pattern table. Provenance classes:
|
|
116
|
+
# VERIFIED-LIVE observed from the real binary on this VM
|
|
117
|
+
# VERIFIED-DOCS verbatim from the vendor's official docs
|
|
118
|
+
# VERIFIED-SOURCE verbatim from the vendor's own source tree
|
|
119
|
+
# Patterns match on an apostrophe-free core where the vendor
|
|
120
|
+
# text uses a curly apostrophe (U+2019), so either rendering
|
|
121
|
+
# matches. Matching is case-insensitive throughout.
|
|
122
|
+
# ----------------------------------------------------------------
|
|
123
|
+
FAILURE_PATTERNS = {
|
|
124
|
+
"claude": (
|
|
125
|
+
# VERIFIED-DOCS: code.claude.com/docs/en/errors, "Usage
|
|
126
|
+
# limits" — 'You've hit your session limit' / 'weekly' /
|
|
127
|
+
# 'Opus' / 'Sonnet' / 'individual usage limit' variants.
|
|
128
|
+
{"id": "claude-quota-limit", "class": "quota",
|
|
129
|
+
"pattern": r"hit your (session|weekly|opus|sonnet|"
|
|
130
|
+
r"individual usage) limit"},
|
|
131
|
+
# VERIFIED-DOCS: same page — 'spend limit reached'.
|
|
132
|
+
{"id": "claude-quota-spend-limit", "class": "quota",
|
|
133
|
+
"pattern": r"spend limit reached"},
|
|
134
|
+
# VERIFIED-DOCS: same page — 'Credit balance is too low'.
|
|
135
|
+
{"id": "claude-quota-credit-balance", "class": "quota",
|
|
136
|
+
"pattern": r"credit balance is too low"},
|
|
137
|
+
# VERIFIED-DOCS: same page, "Authentication" —
|
|
138
|
+
# 'Not logged in · Please run /login'.
|
|
139
|
+
{"id": "claude-auth-not-logged-in", "class": "auth",
|
|
140
|
+
"pattern": r"not logged in"},
|
|
141
|
+
# VERIFIED-DOCS: same page — 'Invalid API key'.
|
|
142
|
+
{"id": "claude-auth-invalid-api-key", "class": "auth",
|
|
143
|
+
"pattern": r"invalid api key"},
|
|
144
|
+
# VERIFIED-DOCS: same page — 'API Error: 401 Invalid
|
|
145
|
+
# authentication credentials'.
|
|
146
|
+
{"id": "claude-auth-invalid-credentials", "class": "auth",
|
|
147
|
+
"pattern": r"invalid authentication credentials"},
|
|
148
|
+
# VERIFIED-DOCS: same page — 'Login expired · Please
|
|
149
|
+
# run /login' and 'Claude.ai login expired'.
|
|
150
|
+
{"id": "claude-auth-login-expired", "class": "auth",
|
|
151
|
+
"pattern": r"login expired"},
|
|
152
|
+
),
|
|
153
|
+
"codex": (
|
|
154
|
+
# VERIFIED-SOURCE: openai/codex, codex-rs/protocol/src/
|
|
155
|
+
# error.rs — every UsageLimitReached message begins
|
|
156
|
+
# "You've hit your usage limit" (curly apostrophe in the
|
|
157
|
+
# source; the pattern is the apostrophe-free core).
|
|
158
|
+
{"id": "codex-quota-usage-limit", "class": "quota",
|
|
159
|
+
"pattern": r"hit your usage limit"},
|
|
160
|
+
# VERIFIED-SOURCE: same file — 'Quota exceeded. Check
|
|
161
|
+
# your plan and billing details.' (QuotaExceeded).
|
|
162
|
+
{"id": "codex-quota-exceeded", "class": "quota",
|
|
163
|
+
"pattern": r"quota exceeded\. check your plan and "
|
|
164
|
+
r"billing details"},
|
|
165
|
+
# VERIFIED-LIVE: codex-cli 0.164.0-alpha.9 on this VM,
|
|
166
|
+
# logged out (2026-11-05): stderr carried 'HTTP error:
|
|
167
|
+
# 401 Unauthorized' and 'unexpected status 401
|
|
168
|
+
# Unauthorized: Missing bearer <redacted> ...' before
|
|
169
|
+
# exiting 1 with stdout empty.
|
|
170
|
+
{"id": "codex-auth-401", "class": "auth",
|
|
171
|
+
"pattern": r"401 unauthorized"},
|
|
172
|
+
{"id": "codex-auth-missing-bearer", "class": "auth",
|
|
173
|
+
"pattern": r"missing bearer"},
|
|
174
|
+
),
|
|
175
|
+
"gemini": (
|
|
176
|
+
# VERIFIED-DOCS: geminicli.com/docs/resources/faq quotes
|
|
177
|
+
# the failure verbatim: 'API error: 429 - Resource
|
|
178
|
+
# exhausted' (RESOURCE_EXHAUSTED). Gemini's unauthen-
|
|
179
|
+
# ticated-headless failure text is UNVERIFIED in the
|
|
180
|
+
# official docs, so gemini carries no auth pattern.
|
|
181
|
+
{"id": "gemini-quota-resource-exhausted", "class": "quota",
|
|
182
|
+
"pattern": r"resource_exhausted|resource exhausted"},
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_COMPILED = {
|
|
187
|
+
cli: tuple({**p, "rx": re.compile(p["pattern"], re.IGNORECASE)}
|
|
188
|
+
for p in patterns)
|
|
189
|
+
for cli, patterns in FAILURE_PATTERNS.items()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cli_key_for_argv(argv):
|
|
194
|
+
"""The pattern-table key for a seat command line: the
|
|
195
|
+
basename of argv[0] when it names a tabled CLI, else None
|
|
196
|
+
(a wrapper script, an unknown CLI — process signals only)."""
|
|
197
|
+
if not argv:
|
|
198
|
+
return None
|
|
199
|
+
base = os.path.basename(str(argv[0]))
|
|
200
|
+
if base.endswith(".exe"):
|
|
201
|
+
base = base[:-4]
|
|
202
|
+
return base if base in _COMPILED else None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_QUOTE_CHARS = ('"', "“", "”", "`")
|
|
206
|
+
|
|
207
|
+
# A function-call opening: an identifier (dotted paths
|
|
208
|
+
# included) followed by '('. Vendor lead-ins are labels,
|
|
209
|
+
# timestamps and logger paths — none contains one.
|
|
210
|
+
_CALL_LEAD_IN_RX = re.compile(r"[A-Za-z_][\w.]*\s*\(")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _unquoted_lead_in(prefix):
|
|
214
|
+
"""True when `prefix` (the line text before a match) states
|
|
215
|
+
no quoted matter: no quote characters, and every apostrophe
|
|
216
|
+
word-internal (You've — the vendor's own voice), never a
|
|
217
|
+
quote opening or closing a span ('docs', "…")."""
|
|
218
|
+
# Diff / markdown quoting (P4-06c): when the line's first
|
|
219
|
+
# non-space character is a unified-diff '+' marker, a '>'
|
|
220
|
+
# quote marker or a '|' table/quote marker, the text is
|
|
221
|
+
# content under review, not the CLI speaking. (The prefix
|
|
222
|
+
# starts at the line's start, so its own first non-space
|
|
223
|
+
# character is the line's.) '-' is left alone: it is also
|
|
224
|
+
# an ordinary bullet in vendor output.
|
|
225
|
+
if prefix.lstrip()[:1] in ("+", ">", "|"):
|
|
226
|
+
return False
|
|
227
|
+
# Code-call lead-ins (P4-06c): a line like
|
|
228
|
+
# print(Not logged in) echoes source, it does not state a
|
|
229
|
+
# failure — a call's opening before the match disqualifies
|
|
230
|
+
# the lead-in.
|
|
231
|
+
if _CALL_LEAD_IN_RX.search(prefix):
|
|
232
|
+
return False
|
|
233
|
+
for i, ch in enumerate(prefix):
|
|
234
|
+
if ch in _QUOTE_CHARS:
|
|
235
|
+
return False
|
|
236
|
+
if ch in ("'", "’"):
|
|
237
|
+
left = prefix[i - 1] if i else ""
|
|
238
|
+
right = prefix[i + 1] if i + 1 < len(prefix) else ""
|
|
239
|
+
if not (left.isalpha() and right.isalpha()):
|
|
240
|
+
return False
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _json_strings(obj, out):
|
|
245
|
+
if isinstance(obj, str):
|
|
246
|
+
out.append(obj)
|
|
247
|
+
elif isinstance(obj, dict):
|
|
248
|
+
for v in obj.values():
|
|
249
|
+
_json_strings(v, out)
|
|
250
|
+
elif isinstance(obj, list):
|
|
251
|
+
for v in obj:
|
|
252
|
+
_json_strings(v, out)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _is_error_envelope(obj):
|
|
256
|
+
"""True for a JSON object that is the CLI/API reporting a
|
|
257
|
+
failure — {"is_error": true, ...} (Claude Code's result
|
|
258
|
+
envelope) or {"error": ...} without verdict fields — and
|
|
259
|
+
False for a review verdict, whose free-text fields quote
|
|
260
|
+
the diff under review and must never be scanned (P4-06)."""
|
|
261
|
+
if "findings" in obj or "verdict" in obj:
|
|
262
|
+
return False
|
|
263
|
+
return obj.get("is_error") is True or "error" in obj
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _candidate_lines(text):
|
|
267
|
+
"""Lines to scan (P4-06b). A line that is a JSON object —
|
|
268
|
+
Claude Code's `--output-format json` result, or an API-style
|
|
269
|
+
{"error": ...} — is scanned by its decoded string VALUES, so
|
|
270
|
+
the CLI's own message is not rejected for sitting inside the
|
|
271
|
+
JSON string quotes — but only for error envelopes (see
|
|
272
|
+
_is_error_envelope); a verdict object is left as it was.
|
|
273
|
+
Key names are not scanned, and text
|
|
274
|
+
inside the values is still split into lines and subject to
|
|
275
|
+
the same lead-in test. Non-JSON lines pass through."""
|
|
276
|
+
out = []
|
|
277
|
+
for line in text.splitlines():
|
|
278
|
+
st = line.strip()
|
|
279
|
+
if st.startswith("{") and st.endswith("}"):
|
|
280
|
+
try:
|
|
281
|
+
obj = json.loads(st)
|
|
282
|
+
except ValueError:
|
|
283
|
+
obj = None
|
|
284
|
+
if isinstance(obj, dict) and _is_error_envelope(obj):
|
|
285
|
+
vals = []
|
|
286
|
+
_json_strings(obj, vals)
|
|
287
|
+
for v in vals:
|
|
288
|
+
out.extend(v.splitlines())
|
|
289
|
+
continue
|
|
290
|
+
out.append(line)
|
|
291
|
+
return out
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def match_failure(cli_key, text):
|
|
295
|
+
"""First tabled pattern matching `text` for `cli_key`, as
|
|
296
|
+
{class, pattern, matched} — or None. Unknown CLI or no
|
|
297
|
+
match both mean: no textual classification, by design.
|
|
298
|
+
|
|
299
|
+
A match counts only under the P4-06 anchor: scanning line
|
|
300
|
+
by line, the text before the match on its line must be an
|
|
301
|
+
unquoted lead-in (see the module docstring). Pattern
|
|
302
|
+
priority is the table's, exactly as when the whole text was
|
|
303
|
+
searched at once."""
|
|
304
|
+
if not cli_key or not text:
|
|
305
|
+
return None
|
|
306
|
+
lines = _candidate_lines(text)
|
|
307
|
+
for entry in _COMPILED.get(cli_key, ()):
|
|
308
|
+
rx = entry["rx"]
|
|
309
|
+
for line in lines:
|
|
310
|
+
for m in rx.finditer(line):
|
|
311
|
+
if _unquoted_lead_in(line[:m.start()]):
|
|
312
|
+
return {"class": entry["class"],
|
|
313
|
+
"pattern": entry["id"],
|
|
314
|
+
"matched": m.group(0)[:80]}
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def classify_exit(cli_key, returncode, stdout_text, stderr_text):
|
|
319
|
+
"""Terminal classification for an exited seat process.
|
|
320
|
+
|
|
321
|
+
Returns (state, class, evidence). A tabled pattern on
|
|
322
|
+
stderr (then stdout — Claude's failure-as-result channel)
|
|
323
|
+
classifies blocked:<class>; anything else is a plain
|
|
324
|
+
failure with the exit code as evidence."""
|
|
325
|
+
for source, text in (("stderr", stderr_text),
|
|
326
|
+
("stdout", stdout_text)):
|
|
327
|
+
hit = match_failure(cli_key, text or "")
|
|
328
|
+
if hit:
|
|
329
|
+
return "blocked", hit["class"], {
|
|
330
|
+
"exit_code": returncode,
|
|
331
|
+
"pattern": hit["pattern"],
|
|
332
|
+
"matched": hit["matched"],
|
|
333
|
+
"source": source}
|
|
334
|
+
return "failed", "error", {"exit_code": returncode}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class SeatStateLog:
|
|
338
|
+
"""One job's state trace: the transitions list IS both the
|
|
339
|
+
live stream (each emit fires `on_event`) and the record's
|
|
340
|
+
`transitions` (spec §4.3 agreement rule — the record can
|
|
341
|
+
never claim less than what was shown live, because both
|
|
342
|
+
read this object). Thread-safe: a seat's runner thread
|
|
343
|
+
emits while the job owner reads the summary."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, on_event=None):
|
|
346
|
+
self._lock = threading.Lock()
|
|
347
|
+
self._t0 = time.monotonic()
|
|
348
|
+
self._transitions = []
|
|
349
|
+
self._terminal = None
|
|
350
|
+
self.on_event = on_event
|
|
351
|
+
|
|
352
|
+
def _at_ms(self):
|
|
353
|
+
return int((time.monotonic() - self._t0) * 1000)
|
|
354
|
+
|
|
355
|
+
def emit(self, state, cls=None, evidence=None, terminal=False):
|
|
356
|
+
tr = {"state": state, "class": cls, "at_ms": self._at_ms()}
|
|
357
|
+
if evidence:
|
|
358
|
+
tr["evidence"] = dict(evidence)
|
|
359
|
+
if terminal:
|
|
360
|
+
tr["terminal"] = True
|
|
361
|
+
with self._lock:
|
|
362
|
+
self._transitions.append(tr)
|
|
363
|
+
if terminal:
|
|
364
|
+
self._terminal = tr
|
|
365
|
+
if self.on_event is not None:
|
|
366
|
+
try:
|
|
367
|
+
self.on_event(dict(tr))
|
|
368
|
+
except Exception: # noqa: BLE001 — a broken progress
|
|
369
|
+
pass # sink never breaks a review job
|
|
370
|
+
return tr
|
|
371
|
+
|
|
372
|
+
def summary(self):
|
|
373
|
+
"""The record form: terminal state/class/evidence plus
|
|
374
|
+
the full transition trace."""
|
|
375
|
+
with self._lock:
|
|
376
|
+
transitions = [dict(t) for t in self._transitions]
|
|
377
|
+
term = self._terminal or (
|
|
378
|
+
transitions[-1] if transitions else
|
|
379
|
+
{"state": "pending", "class": None})
|
|
380
|
+
return {"state": term["state"],
|
|
381
|
+
"class": term.get("class"),
|
|
382
|
+
"evidence": dict(term.get("evidence") or {}),
|
|
383
|
+
"transitions": transitions}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# The job currently executing on this thread/context, if any.
|
|
387
|
+
# core._review_job installs its log here for the duration of the
|
|
388
|
+
# job; providers.command_agent reads it to report live episodes
|
|
389
|
+
# (stalls) and attaches its terminal classification to the
|
|
390
|
+
# ProviderError it raises (`seat_facts`), which the job owner
|
|
391
|
+
# turns into the terminal transition. ContextVar so concurrent
|
|
392
|
+
# jobs on pooled threads never share a log.
|
|
393
|
+
CURRENT = contextvars.ContextVar(
|
|
394
|
+
"codereview_seat_state_log", default=None)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def current_log():
|
|
398
|
+
return CURRENT.get()
|