prizmkit 1.1.125 → 1.1.127
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/dev-pipeline/prizmkit_runtime/reset.py +49 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +75 -14
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +37 -0
- package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +267 -0
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +3 -2
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +6 -18
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -15
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +8 -9
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -9
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +15 -18
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -17
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +9 -9
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +8 -5
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +1 -1
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +1 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +206 -3
- package/bundled/dev-pipeline/tests/test_runtime_helper.py +36 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +10 -7
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +128 -95
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +89 -25
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +246 -114
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Initialize and append validated sections to review-report.md."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -10,153 +10,285 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
VERDICTS = {"PASS", "NEEDS_FIXES", "BLOCKED"}
|
|
13
|
-
|
|
13
|
+
RISKS = {"low", "medium", "high"}
|
|
14
|
+
EVENTS = {
|
|
15
|
+
"risk-assessment",
|
|
16
|
+
"main-review-round",
|
|
17
|
+
"repair-verification",
|
|
18
|
+
"reviewer-running",
|
|
19
|
+
"reviewer-result",
|
|
20
|
+
"infrastructure-failure",
|
|
21
|
+
"final-verification",
|
|
22
|
+
}
|
|
23
|
+
FINAL_HEADING = "## Final Result"
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
class ReportStateError(ValueError):
|
|
17
|
-
"""Raised when
|
|
27
|
+
"""Raised when an operation would produce an invalid report lifecycle."""
|
|
18
28
|
|
|
19
29
|
|
|
20
|
-
def
|
|
30
|
+
def _require_object(data: Any) -> dict[str, Any]:
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
raise ReportStateError("input must be a JSON object")
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _require_text(data: dict[str, Any], name: str) -> str:
|
|
37
|
+
value = data.get(name)
|
|
38
|
+
if not isinstance(value, str) or not value.strip():
|
|
39
|
+
raise ReportStateError(f"{name} must be a non-empty string")
|
|
40
|
+
return value.strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _require_count(data: dict[str, Any], name: str, *, maximum: int | None = None) -> int:
|
|
21
44
|
value = data.get(name)
|
|
22
45
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
23
46
|
raise ReportStateError(f"{name} must be a non-negative integer")
|
|
47
|
+
if maximum is not None and value > maximum:
|
|
48
|
+
raise ReportStateError(f"{name} cannot exceed {maximum}")
|
|
24
49
|
return value
|
|
25
50
|
|
|
26
51
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
def _require_risk(data: dict[str, Any]) -> str:
|
|
53
|
+
risk = data.get("risk")
|
|
54
|
+
if risk not in RISKS:
|
|
55
|
+
raise ReportStateError(f"risk has invalid value: {risk!r}")
|
|
56
|
+
return risk
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _report_text(path: Path) -> str:
|
|
60
|
+
try:
|
|
61
|
+
return path.read_text(encoding="utf-8")
|
|
62
|
+
except FileNotFoundError as error:
|
|
63
|
+
raise ReportStateError("report must be initialized before append") from error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ensure_appendable(path: Path) -> None:
|
|
67
|
+
text = _report_text(path)
|
|
68
|
+
if "## Status: IN_PROGRESS" not in text:
|
|
69
|
+
raise ReportStateError("report is not an initialized in-progress execution")
|
|
70
|
+
if FINAL_HEADING in text:
|
|
71
|
+
raise ReportStateError("cannot append after Final Result")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _append_section(path: Path, section: str) -> None:
|
|
75
|
+
with path.open("a", encoding="utf-8") as report:
|
|
76
|
+
report.write(section.rstrip() + "\n\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def initialize_report(path: Path) -> None:
|
|
80
|
+
"""Start a new execution and remove stale prior execution content."""
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
path.write_text("# Review Report\n\n## Status: IN_PROGRESS\n\n", encoding="utf-8")
|
|
30
83
|
|
|
31
|
-
verdict = data.get("verdict")
|
|
32
|
-
if verdict not in VERDICTS:
|
|
33
|
-
raise ReportStateError(f"verdict has invalid value: {verdict!r}")
|
|
34
84
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
def _render_event(data: dict[str, Any]) -> str:
|
|
86
|
+
event = data.get("event")
|
|
87
|
+
if event not in EVENTS:
|
|
88
|
+
raise ReportStateError(f"event has invalid value: {event!r}")
|
|
89
|
+
|
|
90
|
+
if event == "risk-assessment":
|
|
91
|
+
risk = _require_risk(data)
|
|
92
|
+
reasons = data.get("reasons")
|
|
93
|
+
if not isinstance(reasons, list) or not reasons or not all(
|
|
94
|
+
isinstance(reason, str) and reason.strip() for reason in reasons
|
|
95
|
+
):
|
|
96
|
+
raise ReportStateError("reasons must be a non-empty string array")
|
|
97
|
+
limit = {"low": 0, "medium": 1, "high": 2}[risk]
|
|
98
|
+
lines = [
|
|
99
|
+
"## Risk Assessment",
|
|
100
|
+
"",
|
|
101
|
+
f"- Risk: {risk}",
|
|
102
|
+
f"- Independent Reviewer Limit: {limit}",
|
|
103
|
+
"- Reasons:",
|
|
104
|
+
]
|
|
105
|
+
lines.extend(f" - {reason.strip()}" for reason in reasons)
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
if event == "main-review-round":
|
|
109
|
+
round_number = _require_count(data, "round", maximum=10)
|
|
110
|
+
if round_number < 1:
|
|
111
|
+
raise ReportStateError("round must be at least 1")
|
|
112
|
+
findings = _require_count(data, "findings")
|
|
113
|
+
accepted = _require_count(data, "accepted")
|
|
114
|
+
rejected = _require_count(data, "rejected")
|
|
115
|
+
if accepted + rejected != findings:
|
|
116
|
+
raise ReportStateError("accepted + rejected must equal findings")
|
|
117
|
+
return "\n".join(
|
|
118
|
+
[
|
|
119
|
+
f"## Main Review Round {round_number}",
|
|
120
|
+
"",
|
|
121
|
+
"- Status: COMPLETED",
|
|
122
|
+
f"- Findings: {findings}",
|
|
123
|
+
f"- Accepted: {accepted}",
|
|
124
|
+
f"- Rejected: {rejected}",
|
|
125
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if event == "repair-verification":
|
|
130
|
+
source = _require_text(data, "source")
|
|
131
|
+
if source not in {"main", "reviewer-1", "reviewer-2"}:
|
|
132
|
+
raise ReportStateError(f"source has invalid value: {source!r}")
|
|
133
|
+
return "\n".join(
|
|
134
|
+
[
|
|
135
|
+
"## Repair Verification",
|
|
136
|
+
"",
|
|
137
|
+
f"- Source: {source}",
|
|
138
|
+
f"- Fixed Findings: {_require_count(data, 'fixed_findings')}",
|
|
139
|
+
f"- Verification: {_require_text(data, 'verification')}",
|
|
140
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if event == "reviewer-running":
|
|
145
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
146
|
+
if reviewer < 1:
|
|
147
|
+
raise ReportStateError("reviewer must be 1 or 2")
|
|
148
|
+
risk = _require_risk(data)
|
|
149
|
+
if risk == "low" or (risk == "medium" and reviewer == 2):
|
|
150
|
+
raise ReportStateError("reviewer exceeds the semantic risk limit")
|
|
151
|
+
attempt = data.get("attempt", 1)
|
|
152
|
+
if attempt not in {1, 2}:
|
|
153
|
+
raise ReportStateError("attempt must be 1 or 2")
|
|
154
|
+
return "\n".join(
|
|
155
|
+
[
|
|
156
|
+
f"## Independent Reviewer {reviewer}",
|
|
157
|
+
"",
|
|
158
|
+
"- Status: RUNNING",
|
|
159
|
+
f"- Risk Trigger: {risk}",
|
|
160
|
+
f"- Attempt: {attempt}",
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if event == "reviewer-result":
|
|
165
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
166
|
+
if reviewer < 1:
|
|
167
|
+
raise ReportStateError("reviewer must be 1 or 2")
|
|
168
|
+
result = data.get("result")
|
|
169
|
+
if result not in {"PASS", "FINDINGS", "BLOCKED"}:
|
|
170
|
+
raise ReportStateError(f"result has invalid value: {result!r}")
|
|
171
|
+
findings = _require_count(data, "findings")
|
|
172
|
+
accepted = _require_count(data, "accepted")
|
|
173
|
+
rejected = _require_count(data, "rejected")
|
|
174
|
+
accepted_high = _require_count(data, "accepted_high")
|
|
175
|
+
if accepted + rejected != findings or accepted_high > accepted:
|
|
176
|
+
raise ReportStateError("reviewer finding counts are inconsistent")
|
|
177
|
+
status = "BLOCKED" if result == "BLOCKED" else "COMPLETED"
|
|
178
|
+
return "\n".join(
|
|
179
|
+
[
|
|
180
|
+
f"## Independent Reviewer {reviewer} Result",
|
|
181
|
+
"",
|
|
182
|
+
f"- Status: {status}",
|
|
183
|
+
f"- Result: {result}",
|
|
184
|
+
f"- Findings: {findings}",
|
|
185
|
+
f"- Accepted: {accepted}",
|
|
186
|
+
f"- Rejected: {rejected}",
|
|
187
|
+
f"- Accepted High: {accepted_high}",
|
|
188
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if event == "infrastructure-failure":
|
|
193
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
194
|
+
attempt = _require_count(data, "attempt", maximum=2)
|
|
195
|
+
if reviewer < 1 or attempt < 1:
|
|
196
|
+
raise ReportStateError("reviewer and attempt must be at least 1")
|
|
197
|
+
return "\n".join(
|
|
198
|
+
[
|
|
199
|
+
f"## Independent Reviewer {reviewer} Infrastructure Failure",
|
|
200
|
+
"",
|
|
201
|
+
f"- Attempt: {attempt}",
|
|
202
|
+
f"- Evidence: {_require_text(data, 'evidence')}",
|
|
203
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return "\n".join(
|
|
208
|
+
[
|
|
209
|
+
"## Final Verification",
|
|
210
|
+
"",
|
|
211
|
+
f"- Status: {_require_text(data, 'status')}",
|
|
212
|
+
f"- Evidence: {_require_text(data, 'evidence')}",
|
|
213
|
+
]
|
|
69
214
|
)
|
|
70
|
-
blocker = data.get("blocker")
|
|
71
215
|
|
|
216
|
+
|
|
217
|
+
def append_event(path: Path, data: Any) -> None:
|
|
218
|
+
"""Append one validated progress event to an active execution."""
|
|
219
|
+
_ensure_appendable(path)
|
|
220
|
+
_append_section(path, _render_event(_require_object(data)))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def append_final_result(path: Path, data: Any) -> None:
|
|
224
|
+
"""Append the sole terminal result for the current execution."""
|
|
225
|
+
_ensure_appendable(path)
|
|
226
|
+
valid = _require_object(data)
|
|
227
|
+
verdict = valid.get("verdict")
|
|
228
|
+
if verdict not in VERDICTS:
|
|
229
|
+
raise ReportStateError(f"verdict has invalid value: {verdict!r}")
|
|
230
|
+
risk = _require_risk(valid)
|
|
231
|
+
main_rounds = _require_count(valid, "main_review_rounds", maximum=10)
|
|
232
|
+
reviewers = _require_count(valid, "independent_reviewers_started", maximum=2)
|
|
233
|
+
if reviewers > {"low": 0, "medium": 1, "high": 2}[risk]:
|
|
234
|
+
raise ReportStateError("independent_reviewers_started exceeds risk limit")
|
|
235
|
+
accepted = _require_count(valid, "accepted_findings")
|
|
236
|
+
fixed = _require_count(valid, "fixed_findings")
|
|
237
|
+
rejected = _require_count(valid, "rejected_findings")
|
|
238
|
+
unresolved = _require_count(valid, "unresolved_findings")
|
|
72
239
|
if verdict == "PASS" and unresolved:
|
|
73
240
|
raise ReportStateError("PASS cannot contain unresolved findings")
|
|
74
241
|
if verdict == "NEEDS_FIXES" and not unresolved:
|
|
75
|
-
raise ReportStateError("NEEDS_FIXES requires
|
|
76
|
-
if verdict == "BLOCKED":
|
|
77
|
-
if not isinstance(blocker, str) or not blocker.strip():
|
|
78
|
-
raise ReportStateError("BLOCKED requires a non-empty blocker")
|
|
79
|
-
if unresolved:
|
|
80
|
-
raise ReportStateError("BLOCKED cannot classify code findings as unresolved")
|
|
81
|
-
elif blocker is not None:
|
|
82
|
-
raise ReportStateError("only BLOCKED may include blocker")
|
|
242
|
+
raise ReportStateError("NEEDS_FIXES requires unresolved findings")
|
|
83
243
|
|
|
84
|
-
|
|
244
|
+
section = "\n".join(
|
|
245
|
+
[
|
|
246
|
+
FINAL_HEADING,
|
|
247
|
+
"",
|
|
248
|
+
f"- Verdict: {verdict}",
|
|
249
|
+
f"- Risk: {risk}",
|
|
250
|
+
f"- Main Review Rounds: {main_rounds}",
|
|
251
|
+
f"- Independent Reviewers Started: {reviewers}",
|
|
252
|
+
f"- Accepted Findings: {accepted}",
|
|
253
|
+
f"- Fixed Findings: {fixed}",
|
|
254
|
+
f"- Rejected Findings: {rejected}",
|
|
255
|
+
f"- Unresolved Findings: {unresolved}",
|
|
256
|
+
f"- Summary: {_require_text(valid, 'summary')}",
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
_append_section(path, section)
|
|
85
260
|
|
|
86
261
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
unresolved = sum(
|
|
93
|
-
finding["disposition"].startswith("unresolved") for finding in findings
|
|
94
|
-
)
|
|
95
|
-
return fixed, rejected, unresolved
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def render_report(data: Any) -> str:
|
|
99
|
-
valid = validate_data(data)
|
|
100
|
-
findings = valid["findings"]
|
|
101
|
-
fixed, rejected, unresolved = _finding_totals(findings)
|
|
102
|
-
|
|
103
|
-
lines = [
|
|
104
|
-
"# Review Report",
|
|
105
|
-
"",
|
|
106
|
-
f"## Verdict: {valid['verdict']}",
|
|
107
|
-
f"## Rounds Completed: {valid['rounds_completed']}",
|
|
108
|
-
f"## Total Findings: {len(findings)} → Fixed: {fixed}, Rejected: {rejected}, Unresolved: {unresolved}",
|
|
109
|
-
"",
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
if findings:
|
|
113
|
-
lines.extend(["## Findings", ""])
|
|
114
|
-
for index, finding in enumerate(findings, start=1):
|
|
115
|
-
lines.extend(
|
|
116
|
-
[
|
|
117
|
-
f"### Finding {index}: {finding['title']}",
|
|
118
|
-
"",
|
|
119
|
-
f"- Severity: {finding['severity']}",
|
|
120
|
-
f"- Location: {finding['location']}",
|
|
121
|
-
f"- Problem: {finding['problem']}",
|
|
122
|
-
f"- Failure Scenario: {finding['failure_scenario']}",
|
|
123
|
-
f"- Disposition: {finding['disposition']}",
|
|
124
|
-
"",
|
|
125
|
-
]
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if valid["verdict"] == "BLOCKED":
|
|
129
|
-
lines.extend(["## Blocker", "", valid["blocker"], ""])
|
|
130
|
-
|
|
131
|
-
lines.extend(["## Summary", "", valid["summary"], ""])
|
|
132
|
-
return "\n".join(lines)
|
|
262
|
+
def _read_json_input() -> Any:
|
|
263
|
+
try:
|
|
264
|
+
return json.loads(sys.stdin.read())
|
|
265
|
+
except json.JSONDecodeError as error:
|
|
266
|
+
raise ReportStateError(f"invalid JSON input: {error}") from error
|
|
133
267
|
|
|
134
268
|
|
|
135
269
|
def parse_args() -> argparse.Namespace:
|
|
136
270
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
137
|
-
parser.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
type=Path,
|
|
142
|
-
help="optional JSON input path; reads standard input when omitted",
|
|
143
|
-
)
|
|
271
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
272
|
+
for command in ("init", "append", "finalize"):
|
|
273
|
+
subparser = subparsers.add_parser(command)
|
|
274
|
+
subparser.add_argument("report", type=Path)
|
|
144
275
|
return parser.parse_args()
|
|
145
276
|
|
|
146
277
|
|
|
147
278
|
def main() -> int:
|
|
148
279
|
args = parse_args()
|
|
149
280
|
try:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
281
|
+
if args.command == "init":
|
|
282
|
+
initialize_report(args.report)
|
|
283
|
+
elif args.command == "append":
|
|
284
|
+
append_event(args.report, _read_json_input())
|
|
285
|
+
else:
|
|
286
|
+
append_final_result(args.report, _read_json_input())
|
|
287
|
+
except (OSError, ReportStateError, KeyError) as error:
|
|
156
288
|
print(json.dumps({"error": str(error)}, sort_keys=True))
|
|
157
289
|
return 1
|
|
158
290
|
return 0
|
|
159
291
|
|
|
160
292
|
|
|
161
293
|
if __name__ == "__main__":
|
|
162
|
-
|
|
294
|
+
raise SystemExit(main())
|