prizmkit 1.1.122 → 1.1.124
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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +16 -5
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +121 -9
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +46 -38
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +365 -22
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +118 -109
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +21 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +33 -17
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +109 -63
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +179 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +80 -52
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +348 -154
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +188 -0
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +222 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
|
@@ -1,186 +1,380 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Loop Exit Gatekeeper for prizmkit-code-review skill.
|
|
4
|
-
|
|
5
|
-
Stateless design: all loop state is passed via stdin and returned via stdout.
|
|
6
|
-
The caller (AI model) tracks `findings_history` and `max_rounds` between calls.
|
|
7
|
-
No runtime files are created — the script is a pure function over JSON.
|
|
8
|
-
|
|
9
|
-
The model supplies the current round in each call — the script does not auto-increment
|
|
10
|
-
rounds, because the gate is called twice per loop iteration (after Reviewer returns
|
|
11
|
-
and after Main Agent filters), and only the model knows when a full cycle completes.
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
echo '{"reviewer_result":"NEEDS_FIXES","accepted_count":2,"findings_count":5,"round":1,"findings_history":[],"max_rounds":3,"filtering_done":true}' | python3 check_loop.py
|
|
15
|
-
|
|
16
|
-
Input JSON schema:
|
|
17
|
-
{
|
|
18
|
-
"reviewer_result": "PASS" | "NEEDS_FIXES",
|
|
19
|
-
"accepted_count": int,
|
|
20
|
-
"findings_count": int,
|
|
21
|
-
"round": int,
|
|
22
|
-
"findings_history": [int, ...],
|
|
23
|
-
"max_rounds": int,
|
|
24
|
-
"filtering_done": bool
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
Output JSON schema:
|
|
28
|
-
{
|
|
29
|
-
"endLoop": bool,
|
|
30
|
-
"reason": str,
|
|
31
|
-
"verdict": "PASS" | "NEEDS_FIXES" | null,
|
|
32
|
-
"round": int,
|
|
33
|
-
"maxRounds": int,
|
|
34
|
-
"divergenceWarning": bool,
|
|
35
|
-
"findings_history": [int, ...]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
Exit conditions (checked in order):
|
|
39
|
-
1. Reviewer returned PASS → endLoop=true, verdict=PASS
|
|
40
|
-
2. NEEDS_FIXES before filtering → endLoop=false, wait for filtering
|
|
41
|
-
3. Filtered findings all rejected → endLoop=true, verdict=PASS
|
|
42
|
-
4. Max rounds reached after filtering → endLoop=true, verdict=NEEDS_FIXES
|
|
43
|
-
5. Otherwise → endLoop=false
|
|
2
|
+
"""Strict execution and convergence gate for prizmkit-code-review protocol v2."""
|
|
44
3
|
|
|
45
|
-
|
|
46
|
-
If findings_count strictly increases for 3 consecutive rounds, set divergenceWarning=true.
|
|
47
|
-
This is a warning only — it does not force loop exit.
|
|
48
|
-
"""
|
|
4
|
+
from __future__ import annotations
|
|
49
5
|
|
|
50
6
|
import json
|
|
51
7
|
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
PROTOCOL_VERSION = 2
|
|
11
|
+
REVIEW_SAFETY_LIMIT = 10
|
|
12
|
+
|
|
13
|
+
CAPABILITY_FIELDS = {
|
|
14
|
+
"mutation_capability",
|
|
15
|
+
"delegation_equivalent_capability",
|
|
16
|
+
"downstream_execution_creation",
|
|
17
|
+
"orchestration_reentry_capability",
|
|
18
|
+
"review_execution",
|
|
19
|
+
"status",
|
|
20
|
+
}
|
|
21
|
+
PREFLIGHT_FIELDS = {
|
|
22
|
+
"protocol_version",
|
|
23
|
+
"gate_stage",
|
|
24
|
+
"round",
|
|
25
|
+
"reviewers_started_this_round",
|
|
26
|
+
"reviewers_started_total",
|
|
27
|
+
"fresh_reviewer",
|
|
28
|
+
"workspace_snapshot_ready",
|
|
29
|
+
"capability_attestation",
|
|
30
|
+
}
|
|
31
|
+
RESULT_BASE_FIELDS = {
|
|
32
|
+
"protocol_version",
|
|
33
|
+
"gate_stage",
|
|
34
|
+
"filtering_done",
|
|
35
|
+
"round",
|
|
36
|
+
"reviewers_started_this_round",
|
|
37
|
+
"reviewers_started_total",
|
|
38
|
+
"fresh_reviewer",
|
|
39
|
+
"descendant_execution_units_observed",
|
|
40
|
+
"delegation_depth_observed",
|
|
41
|
+
"capability_attestation",
|
|
42
|
+
"orchestration_status",
|
|
43
|
+
"workspace_status",
|
|
44
|
+
"review_complete",
|
|
45
|
+
"reviewer_result",
|
|
46
|
+
"findings_count",
|
|
47
|
+
}
|
|
48
|
+
RESULT_FILTER_FIELDS = {"accepted_count", "rejected_count"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProtocolError(ValueError):
|
|
52
|
+
"""Raised when a payload violates the strict protocol schema."""
|
|
52
53
|
|
|
53
|
-
DEFAULT_MAX_ROUNDS = 3
|
|
54
54
|
|
|
55
|
+
def _is_int(value: Any) -> bool:
|
|
56
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
55
57
|
|
|
56
|
-
def check_divergence(findings_history):
|
|
57
|
-
"""Return True if the last 3 entries in findings_history are strictly increasing."""
|
|
58
|
-
if len(findings_history) < 3:
|
|
59
|
-
return False
|
|
60
|
-
last_three = findings_history[-3:]
|
|
61
|
-
return last_three[0] < last_three[1] < last_three[2]
|
|
62
58
|
|
|
59
|
+
def _require_exact_fields(data: dict[str, Any], required: set[str]) -> None:
|
|
60
|
+
missing = sorted(required - data.keys())
|
|
61
|
+
unknown = sorted(data.keys() - required)
|
|
62
|
+
if missing:
|
|
63
|
+
raise ProtocolError(f"missing required fields: {', '.join(missing)}")
|
|
64
|
+
if unknown:
|
|
65
|
+
raise ProtocolError(f"unknown fields: {', '.join(unknown)}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_bool(data: dict[str, Any], name: str) -> bool:
|
|
69
|
+
value = data[name]
|
|
70
|
+
if not isinstance(value, bool):
|
|
71
|
+
raise ProtocolError(f"{name} must be a boolean")
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _require_count(data: dict[str, Any], name: str) -> int:
|
|
76
|
+
value = data[name]
|
|
77
|
+
if not _is_int(value) or value < 0:
|
|
78
|
+
raise ProtocolError(f"{name} must be a non-negative integer")
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _require_round(data: dict[str, Any]) -> int:
|
|
83
|
+
current_round = _require_count(data, "round")
|
|
84
|
+
if not 1 <= current_round <= REVIEW_SAFETY_LIMIT:
|
|
85
|
+
raise ProtocolError(
|
|
86
|
+
f"round must be between 1 and {REVIEW_SAFETY_LIMIT}"
|
|
87
|
+
)
|
|
88
|
+
return current_round
|
|
63
89
|
|
|
64
|
-
def record_findings_count(findings_history, current_round, findings_count):
|
|
65
|
-
"""Record findings_count once per round index, updating duplicate calls for the same round."""
|
|
66
|
-
if current_round <= 0:
|
|
67
|
-
return findings_history
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
def _require_enum(data: dict[str, Any], name: str, allowed: set[str]) -> str:
|
|
92
|
+
value = data[name]
|
|
93
|
+
if value not in allowed:
|
|
94
|
+
choices = ", ".join(sorted(allowed))
|
|
95
|
+
raise ProtocolError(f"{name} must be one of: {choices}")
|
|
96
|
+
return value
|
|
72
97
|
|
|
73
|
-
if len(findings_history) == target_index:
|
|
74
|
-
findings_history.append(findings_count)
|
|
75
|
-
else:
|
|
76
|
-
findings_history[target_index] = findings_count
|
|
77
98
|
|
|
78
|
-
|
|
99
|
+
def _validate_protocol_header(data: dict[str, Any], stage: str) -> None:
|
|
100
|
+
if data["protocol_version"] != PROTOCOL_VERSION:
|
|
101
|
+
raise ProtocolError(
|
|
102
|
+
f"protocol_version must be {PROTOCOL_VERSION}"
|
|
103
|
+
)
|
|
104
|
+
if data["gate_stage"] != stage:
|
|
105
|
+
raise ProtocolError(f"gate_stage must be {stage}")
|
|
79
106
|
|
|
80
107
|
|
|
81
|
-
def
|
|
82
|
-
|
|
108
|
+
def _validate_attestation(value: Any) -> bool:
|
|
109
|
+
if not isinstance(value, dict):
|
|
110
|
+
raise ProtocolError("capability_attestation must be an object")
|
|
111
|
+
_require_exact_fields(value, CAPABILITY_FIELDS)
|
|
83
112
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
unavailable_fields = (
|
|
114
|
+
"mutation_capability",
|
|
115
|
+
"delegation_equivalent_capability",
|
|
116
|
+
"downstream_execution_creation",
|
|
117
|
+
"orchestration_reentry_capability",
|
|
118
|
+
)
|
|
119
|
+
for name in unavailable_fields:
|
|
120
|
+
_require_enum(value, name, {"available", "unavailable"})
|
|
121
|
+
_require_enum(
|
|
122
|
+
value,
|
|
123
|
+
"review_execution",
|
|
124
|
+
{"foreground-single-authority", "unsupported"},
|
|
125
|
+
)
|
|
126
|
+
_require_enum(value, "status", {"FAILED", "VERIFIED"})
|
|
94
127
|
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
return (
|
|
129
|
+
value["status"] == "VERIFIED"
|
|
130
|
+
and value["review_execution"] == "foreground-single-authority"
|
|
131
|
+
and all(value[name] == "unavailable" for name in unavailable_fields)
|
|
97
132
|
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# Condition 2: Reviewer returned findings, but filtering has not happened yet
|
|
113
|
-
if reviewer_result == "NEEDS_FIXES" and not filtering_done:
|
|
114
|
-
return {
|
|
115
|
-
"endLoop": False,
|
|
116
|
-
"reason": f"Round {current_round}: reviewer returned findings; filter before applying exit conditions",
|
|
117
|
-
"verdict": None,
|
|
118
|
-
"round": current_round,
|
|
119
|
-
"maxRounds": max_rounds,
|
|
120
|
-
"divergenceWarning": divergence_warning,
|
|
121
|
-
"findings_history": findings_history,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
# Condition 3: All findings rejected after filtering
|
|
125
|
-
if reviewer_result == "NEEDS_FIXES" and accepted_count == 0:
|
|
126
|
-
return {
|
|
127
|
-
"endLoop": True,
|
|
128
|
-
"reason": f"Round {current_round}: all findings rejected by main agent",
|
|
129
|
-
"verdict": "PASS",
|
|
130
|
-
"round": current_round,
|
|
131
|
-
"maxRounds": max_rounds,
|
|
132
|
-
"divergenceWarning": False,
|
|
133
|
-
"findings_history": findings_history,
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
# Condition 4: Max rounds reached
|
|
137
|
-
if current_round >= max_rounds:
|
|
138
|
-
return {
|
|
139
|
-
"endLoop": True,
|
|
140
|
-
"reason": f"Hard limit: max {max_rounds} rounds reached",
|
|
141
|
-
"verdict": "NEEDS_FIXES",
|
|
142
|
-
"round": current_round,
|
|
143
|
-
"maxRounds": max_rounds,
|
|
144
|
-
"divergenceWarning": divergence_warning,
|
|
145
|
-
"findings_history": findings_history,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
# Condition 5: Continue loop
|
|
149
|
-
next_round = current_round + 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _response(
|
|
136
|
+
*,
|
|
137
|
+
action: str,
|
|
138
|
+
verdict: str | None,
|
|
139
|
+
blocker_code: str | None = None,
|
|
140
|
+
launch_authorized: bool = False,
|
|
141
|
+
result_usable: bool = False,
|
|
142
|
+
end_loop: bool = False,
|
|
143
|
+
reason: str,
|
|
144
|
+
) -> dict[str, Any]:
|
|
150
145
|
return {
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
146
|
+
"action": action,
|
|
147
|
+
"launchAuthorized": launch_authorized,
|
|
148
|
+
"resultUsable": result_usable,
|
|
149
|
+
"endLoop": end_loop,
|
|
150
|
+
"verdict": verdict,
|
|
151
|
+
"blockerCode": blocker_code,
|
|
152
|
+
"reason": reason,
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
|
|
161
|
-
def
|
|
162
|
-
|
|
156
|
+
def check_preflight(data: dict[str, Any]) -> dict[str, Any]:
|
|
157
|
+
"""Authorize one safe Reviewer launch or return a fail-closed blocker."""
|
|
158
|
+
_require_exact_fields(data, PREFLIGHT_FIELDS)
|
|
159
|
+
_validate_protocol_header(data, "preflight")
|
|
160
|
+
current_round = _require_round(data)
|
|
161
|
+
started_this_round = _require_count(data, "reviewers_started_this_round")
|
|
162
|
+
started_total = _require_count(data, "reviewers_started_total")
|
|
163
|
+
fresh_reviewer = _require_bool(data, "fresh_reviewer")
|
|
164
|
+
snapshot_ready = _require_bool(data, "workspace_snapshot_ready")
|
|
165
|
+
attested = _validate_attestation(data["capability_attestation"])
|
|
166
|
+
|
|
167
|
+
if started_this_round != 0:
|
|
168
|
+
raise ProtocolError(
|
|
169
|
+
"preflight requires reviewers_started_this_round to equal 0"
|
|
170
|
+
)
|
|
171
|
+
if started_total != current_round - 1:
|
|
172
|
+
raise ProtocolError(
|
|
173
|
+
"preflight reviewers_started_total must equal round - 1"
|
|
174
|
+
)
|
|
175
|
+
if not fresh_reviewer:
|
|
176
|
+
return _response(
|
|
177
|
+
action="STOP",
|
|
178
|
+
verdict="NEEDS_FIXES",
|
|
179
|
+
blocker_code="REVIEW_INFRASTRUCTURE_BLOCKED",
|
|
180
|
+
end_loop=True,
|
|
181
|
+
reason="Reviewer execution is not fresh",
|
|
182
|
+
)
|
|
183
|
+
if not snapshot_ready:
|
|
184
|
+
return _response(
|
|
185
|
+
action="STOP",
|
|
186
|
+
verdict="NEEDS_FIXES",
|
|
187
|
+
blocker_code="REVIEW_INFRASTRUCTURE_BLOCKED",
|
|
188
|
+
end_loop=True,
|
|
189
|
+
reason="Workspace snapshot is not ready",
|
|
190
|
+
)
|
|
191
|
+
if not attested:
|
|
192
|
+
return _response(
|
|
193
|
+
action="STOP",
|
|
194
|
+
verdict="NEEDS_FIXES",
|
|
195
|
+
blocker_code="REVIEW_INFRASTRUCTURE_BLOCKED",
|
|
196
|
+
end_loop=True,
|
|
197
|
+
reason="Mandatory Reviewer capability boundary is not verified",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return _response(
|
|
201
|
+
action="AUTHORIZE_LAUNCH",
|
|
202
|
+
verdict=None,
|
|
203
|
+
launch_authorized=True,
|
|
204
|
+
reason=f"Round {current_round} may launch one fresh Reviewer",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_result_structure(data: dict[str, Any]) -> tuple[int, int, bool]:
|
|
209
|
+
filtering_done = _require_bool(data, "filtering_done")
|
|
210
|
+
required_fields = RESULT_BASE_FIELDS | (
|
|
211
|
+
RESULT_FILTER_FIELDS if filtering_done else set()
|
|
212
|
+
)
|
|
213
|
+
_require_exact_fields(data, required_fields)
|
|
214
|
+
_validate_protocol_header(data, "result")
|
|
215
|
+
|
|
216
|
+
current_round = _require_round(data)
|
|
217
|
+
started_this_round = _require_count(data, "reviewers_started_this_round")
|
|
218
|
+
started_total = _require_count(data, "reviewers_started_total")
|
|
219
|
+
fresh_reviewer = _require_bool(data, "fresh_reviewer")
|
|
220
|
+
_require_count(data, "descendant_execution_units_observed")
|
|
221
|
+
_require_count(data, "delegation_depth_observed")
|
|
222
|
+
_require_bool(data, "review_complete")
|
|
223
|
+
findings_count = _require_count(data, "findings_count")
|
|
224
|
+
reviewer_result = _require_enum(
|
|
225
|
+
data, "reviewer_result", {"NEEDS_FIXES", "PASS"}
|
|
226
|
+
)
|
|
227
|
+
_require_enum(
|
|
228
|
+
data,
|
|
229
|
+
"orchestration_status",
|
|
230
|
+
{"COMPLIANT", "UNVERIFIABLE", "VIOLATION"},
|
|
231
|
+
)
|
|
232
|
+
_require_enum(data, "workspace_status", {"MATCHED", "MISMATCH"})
|
|
233
|
+
_validate_attestation(data["capability_attestation"])
|
|
234
|
+
|
|
235
|
+
if started_this_round != 1:
|
|
236
|
+
raise ProtocolError(
|
|
237
|
+
"result requires exactly one Reviewer started in the round"
|
|
238
|
+
)
|
|
239
|
+
if started_total != current_round:
|
|
240
|
+
raise ProtocolError("result reviewers_started_total must equal round")
|
|
241
|
+
if not fresh_reviewer:
|
|
242
|
+
raise ProtocolError("result requires a fresh Reviewer execution")
|
|
243
|
+
if reviewer_result == "PASS" and findings_count != 0:
|
|
244
|
+
raise ProtocolError("PASS requires findings_count to equal 0")
|
|
245
|
+
if reviewer_result == "NEEDS_FIXES" and findings_count == 0:
|
|
246
|
+
raise ProtocolError("NEEDS_FIXES requires at least one finding")
|
|
247
|
+
|
|
248
|
+
if filtering_done:
|
|
249
|
+
accepted_count = _require_count(data, "accepted_count")
|
|
250
|
+
rejected_count = _require_count(data, "rejected_count")
|
|
251
|
+
if accepted_count + rejected_count != findings_count:
|
|
252
|
+
raise ProtocolError(
|
|
253
|
+
"accepted_count + rejected_count must equal findings_count"
|
|
254
|
+
)
|
|
255
|
+
if reviewer_result == "PASS" and (accepted_count or rejected_count):
|
|
256
|
+
raise ProtocolError("PASS cannot contain finding classifications")
|
|
257
|
+
|
|
258
|
+
return current_round, findings_count, filtering_done
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def check_result(data: dict[str, Any]) -> dict[str, Any]:
|
|
262
|
+
"""Validate topology and workspace before filtering, then decide convergence."""
|
|
263
|
+
current_round, _, filtering_done = _validate_result_structure(data)
|
|
264
|
+
attested = _validate_attestation(data["capability_attestation"])
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
not attested
|
|
268
|
+
or data["descendant_execution_units_observed"] != 0
|
|
269
|
+
or data["delegation_depth_observed"] != 0
|
|
270
|
+
or data["orchestration_status"] != "COMPLIANT"
|
|
271
|
+
):
|
|
272
|
+
return _response(
|
|
273
|
+
action="STOP",
|
|
274
|
+
verdict="NEEDS_FIXES",
|
|
275
|
+
blocker_code="REVIEW_ORCHESTRATION_VIOLATION",
|
|
276
|
+
end_loop=True,
|
|
277
|
+
reason="Reviewer execution topology is non-compliant or unverifiable",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if data["workspace_status"] != "MATCHED":
|
|
281
|
+
return _response(
|
|
282
|
+
action="STOP",
|
|
283
|
+
verdict="NEEDS_FIXES",
|
|
284
|
+
blocker_code="WORKSPACE_MISMATCH",
|
|
285
|
+
end_loop=True,
|
|
286
|
+
reason="Reviewer workspace does not match the authorized snapshot",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if not data["review_complete"]:
|
|
290
|
+
return _response(
|
|
291
|
+
action="STOP",
|
|
292
|
+
verdict="NEEDS_FIXES",
|
|
293
|
+
blocker_code="INVALID_REVIEW_RESULT",
|
|
294
|
+
end_loop=True,
|
|
295
|
+
reason="Reviewer output is incomplete",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not filtering_done:
|
|
299
|
+
if data["reviewer_result"] == "PASS":
|
|
300
|
+
return _response(
|
|
301
|
+
action="PASS",
|
|
302
|
+
verdict="PASS",
|
|
303
|
+
result_usable=True,
|
|
304
|
+
end_loop=True,
|
|
305
|
+
reason=f"Round {current_round} completed with no findings",
|
|
306
|
+
)
|
|
307
|
+
return _response(
|
|
308
|
+
action="AUTHORIZE_FILTERING",
|
|
309
|
+
verdict=None,
|
|
310
|
+
result_usable=True,
|
|
311
|
+
reason=f"Round {current_round} findings are authorized for filtering",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if data["accepted_count"] == 0:
|
|
315
|
+
return _response(
|
|
316
|
+
action="PASS",
|
|
317
|
+
verdict="PASS",
|
|
318
|
+
result_usable=True,
|
|
319
|
+
end_loop=True,
|
|
320
|
+
reason=f"Round {current_round} converged with zero accepted findings",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if current_round == REVIEW_SAFETY_LIMIT:
|
|
324
|
+
return _response(
|
|
325
|
+
action="STOP",
|
|
326
|
+
verdict="NEEDS_FIXES",
|
|
327
|
+
blocker_code="REVIEW_SAFETY_LIMIT_REACHED",
|
|
328
|
+
result_usable=True,
|
|
329
|
+
end_loop=True,
|
|
330
|
+
reason="Accepted findings remain at the internal safety limit",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return _response(
|
|
334
|
+
action="CONTINUE",
|
|
335
|
+
verdict=None,
|
|
336
|
+
result_usable=True,
|
|
337
|
+
reason=(
|
|
338
|
+
f"Round {current_round} has {data['accepted_count']} accepted "
|
|
339
|
+
"findings; repair and start a fresh complete review"
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def check_gate(data: Any) -> dict[str, Any]:
|
|
345
|
+
"""Validate a strict v2 payload and dispatch to the requested stage."""
|
|
346
|
+
if not isinstance(data, dict):
|
|
347
|
+
raise ProtocolError("input must be a JSON object")
|
|
348
|
+
stage = data.get("gate_stage")
|
|
349
|
+
if stage == "preflight":
|
|
350
|
+
return check_preflight(data)
|
|
351
|
+
if stage == "result":
|
|
352
|
+
return check_result(data)
|
|
353
|
+
raise ProtocolError("gate_stage must be preflight or result")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def main() -> int:
|
|
163
357
|
try:
|
|
164
358
|
data = json.load(sys.stdin)
|
|
165
|
-
|
|
359
|
+
result = check_gate(data)
|
|
360
|
+
except (json.JSONDecodeError, ProtocolError) as error:
|
|
166
361
|
print(
|
|
167
362
|
json.dumps(
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
363
|
+
_response(
|
|
364
|
+
action="STOP",
|
|
365
|
+
verdict="NEEDS_FIXES",
|
|
366
|
+
blocker_code="INVALID_REVIEW_RESULT",
|
|
367
|
+
end_loop=True,
|
|
368
|
+
reason=f"Invalid protocol payload: {error}",
|
|
369
|
+
),
|
|
370
|
+
sort_keys=True,
|
|
177
371
|
)
|
|
178
372
|
)
|
|
179
|
-
|
|
373
|
+
return 1
|
|
180
374
|
|
|
181
|
-
result =
|
|
182
|
-
|
|
375
|
+
print(json.dumps(result, sort_keys=True))
|
|
376
|
+
return 0
|
|
183
377
|
|
|
184
378
|
|
|
185
379
|
if __name__ == "__main__":
|
|
186
|
-
main()
|
|
380
|
+
sys.exit(main())
|