prizmkit 1.1.123 → 1.1.125
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 +75 -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/scripts/generate-recovery-prompt.py +3 -4
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +4 -3
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +4 -3
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +7 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -4
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +4 -4
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -4
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +6 -5
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +2 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -9
- package/bundled/dev-pipeline/tests/test_unified_cli.py +31 -16
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/prizmkit-code-review/SKILL.md +113 -110
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +19 -0
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +24 -21
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +95 -67
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +162 -0
- package/package.json +1 -1
- package/src/scaffold.js +15 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +0 -90
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +0 -186
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render the final review-report.md from minimal review data."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
VERDICTS = {"PASS", "NEEDS_FIXES", "BLOCKED"}
|
|
13
|
+
DISPOSITIONS = ("fixed", "rejected", "unresolved")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReportStateError(ValueError):
|
|
17
|
+
"""Raised when review data cannot produce a valid final report."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require_count(data: dict[str, Any], name: str) -> int:
|
|
21
|
+
value = data.get(name)
|
|
22
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
23
|
+
raise ReportStateError(f"{name} must be a non-negative integer")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_data(data: Any) -> dict[str, Any]:
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
raise ReportStateError("review data must be a JSON object")
|
|
30
|
+
|
|
31
|
+
verdict = data.get("verdict")
|
|
32
|
+
if verdict not in VERDICTS:
|
|
33
|
+
raise ReportStateError(f"verdict has invalid value: {verdict!r}")
|
|
34
|
+
|
|
35
|
+
rounds_completed = _require_count(data, "rounds_completed")
|
|
36
|
+
if rounds_completed > 10:
|
|
37
|
+
raise ReportStateError("rounds_completed cannot exceed ten")
|
|
38
|
+
|
|
39
|
+
findings = data.get("findings")
|
|
40
|
+
if not isinstance(findings, list):
|
|
41
|
+
raise ReportStateError("findings must be an array")
|
|
42
|
+
|
|
43
|
+
summary = data.get("summary")
|
|
44
|
+
if not isinstance(summary, str) or not summary.strip():
|
|
45
|
+
raise ReportStateError("summary must be a non-empty string")
|
|
46
|
+
|
|
47
|
+
for index, finding in enumerate(findings, start=1):
|
|
48
|
+
if not isinstance(finding, dict):
|
|
49
|
+
raise ReportStateError(f"finding {index} must be an object")
|
|
50
|
+
required = {
|
|
51
|
+
"title",
|
|
52
|
+
"severity",
|
|
53
|
+
"location",
|
|
54
|
+
"problem",
|
|
55
|
+
"failure_scenario",
|
|
56
|
+
"disposition",
|
|
57
|
+
}
|
|
58
|
+
missing = sorted(required - finding.keys())
|
|
59
|
+
if missing:
|
|
60
|
+
raise ReportStateError(
|
|
61
|
+
f"finding {index} missing fields: {', '.join(missing)}"
|
|
62
|
+
)
|
|
63
|
+
disposition = finding["disposition"]
|
|
64
|
+
if not isinstance(disposition, str) or not disposition.startswith(DISPOSITIONS):
|
|
65
|
+
raise ReportStateError(f"finding {index} has invalid disposition")
|
|
66
|
+
|
|
67
|
+
unresolved = sum(
|
|
68
|
+
finding["disposition"].startswith("unresolved") for finding in findings
|
|
69
|
+
)
|
|
70
|
+
blocker = data.get("blocker")
|
|
71
|
+
|
|
72
|
+
if verdict == "PASS" and unresolved:
|
|
73
|
+
raise ReportStateError("PASS cannot contain unresolved findings")
|
|
74
|
+
if verdict == "NEEDS_FIXES" and not unresolved:
|
|
75
|
+
raise ReportStateError("NEEDS_FIXES requires an unresolved finding")
|
|
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")
|
|
83
|
+
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _finding_totals(findings: list[dict[str, Any]]) -> tuple[int, int, int]:
|
|
88
|
+
fixed = sum(finding["disposition"].startswith("fixed") for finding in findings)
|
|
89
|
+
rejected = sum(
|
|
90
|
+
finding["disposition"].startswith("rejected") for finding in findings
|
|
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)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def parse_args() -> argparse.Namespace:
|
|
136
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
137
|
+
parser.add_argument("output", type=Path, help="review-report.md destination")
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"input",
|
|
140
|
+
nargs="?",
|
|
141
|
+
type=Path,
|
|
142
|
+
help="optional JSON input path; reads standard input when omitted",
|
|
143
|
+
)
|
|
144
|
+
return parser.parse_args()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main() -> int:
|
|
148
|
+
args = parse_args()
|
|
149
|
+
try:
|
|
150
|
+
raw = args.input.read_text(encoding="utf-8") if args.input else sys.stdin.read()
|
|
151
|
+
data = json.loads(raw)
|
|
152
|
+
report = render_report(data)
|
|
153
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
args.output.write_text(report, encoding="utf-8")
|
|
155
|
+
except (OSError, json.JSONDecodeError, ReportStateError, KeyError) as error:
|
|
156
|
+
print(json.dumps({"error": str(error)}, sort_keys=True))
|
|
157
|
+
return 1
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main())
|
package/package.json
CHANGED
package/src/scaffold.js
CHANGED
|
@@ -226,10 +226,12 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
|
|
|
226
226
|
|
|
227
227
|
// Load Claude command adapter for skill conversion
|
|
228
228
|
let convertSkillToCommand;
|
|
229
|
+
let getClaudeSkillAgentDefinitions;
|
|
229
230
|
let convertSkillToCodex;
|
|
230
231
|
if (platform === 'claude') {
|
|
231
232
|
const commandAdapter = await loadAdapter('claude', 'command-adapter.js');
|
|
232
233
|
convertSkillToCommand = commandAdapter.convertSkillToCommand;
|
|
234
|
+
getClaudeSkillAgentDefinitions = commandAdapter.getClaudeSkillAgentDefinitions;
|
|
233
235
|
} else if (platform === 'codex') {
|
|
234
236
|
const codexSkillAdapter = await loadAdapter('codex', 'skill-adapter.js');
|
|
235
237
|
convertSkillToCodex = codexSkillAdapter.convertSkill;
|
|
@@ -321,6 +323,19 @@ export async function installSkills(platform, skills, projectRoot, dryRun, runti
|
|
|
321
323
|
await fs.copy(path.join(corePath, subdir), path.join(assetTargetDir, subdir));
|
|
322
324
|
}
|
|
323
325
|
}
|
|
326
|
+
|
|
327
|
+
const roleManifestPath = path.join(corePath, 'assets', 'reviewer-role.json');
|
|
328
|
+
const roleManifest = await fs.pathExists(roleManifestPath)
|
|
329
|
+
? await fs.readJson(roleManifestPath)
|
|
330
|
+
: null;
|
|
331
|
+
const agentDefinitions = getClaudeSkillAgentDefinitions(skillName, roleManifest);
|
|
332
|
+
if (agentDefinitions.length > 0) {
|
|
333
|
+
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
334
|
+
await fs.ensureDir(agentsDir);
|
|
335
|
+
for (const definition of agentDefinitions) {
|
|
336
|
+
await fs.writeFile(path.join(agentsDir, definition.fileName), definition.content);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
324
339
|
console.log(chalk.green(` ✓ .claude/commands/${skillName}.md`));
|
|
325
340
|
} else if (platform === 'codex') {
|
|
326
341
|
const content = await fs.readFile(skillMdPath, 'utf8');
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# Reviewer Workspace Protocol
|
|
2
|
-
|
|
3
|
-
Use this protocol whenever `/prizmkit-code-review` delegates review to an independent Reviewer. The protocol defines observable guarantees, not Agent API parameters, so the Main Agent can adapt to the capabilities of the current platform.
|
|
4
|
-
|
|
5
|
-
## Protocol Invariants
|
|
6
|
-
|
|
7
|
-
1. **Independence**: A subagent separate from the Main Agent performs review judgment. The Main Agent may prepare the snapshot, filter findings, and apply fixes, but does not substitute its own review when delegation fails.
|
|
8
|
-
2. **Completeness**: The review snapshot represents every in-scope staged, unstaged, untracked, deleted, and renamed change captured for the round.
|
|
9
|
-
3. **Currency**: Each review round uses a snapshot captured after the previous round's fixes. Never reuse a prior-round snapshot as evidence of current state.
|
|
10
|
-
4. **Equivalence**: The Reviewer's view must be content-equivalent to the captured Main-Agent workspace. Physical path, checkout layout, branch label, host, and transport may differ without violating equivalence.
|
|
11
|
-
5. **Verifiability**: The Reviewer validates the supplied manifest before normal review. Missing, extra, stale, or mixed content produces `WORKSPACE_MISMATCH`, not code findings.
|
|
12
|
-
6. **Read-only review**: The Reviewer does not modify files or apply fixes. This keeps review evidence stable and preserves clear fix ownership.
|
|
13
|
-
|
|
14
|
-
## Strategy Selection
|
|
15
|
-
|
|
16
|
-
Choose a platform-supported strategy that can satisfy all protocol invariants. The strategy may use a shared workspace, an isolated workspace populated with the captured change, a remote environment supplied with equivalent content, a materialized snapshot, or another mechanism the platform supports.
|
|
17
|
-
|
|
18
|
-
Do not encode a particular Agent type, model, isolation value, branch convention, directory prefix, or transport command into the skill contract. Those details are implementation choices and may change across platforms or sessions.
|
|
19
|
-
|
|
20
|
-
Prefer the least elaborate strategy that can prove equivalence. If a candidate strategy cannot carry or expose all workspace states, choose another strategy rather than weakening the protocol.
|
|
21
|
-
|
|
22
|
-
## Workspace Manifest
|
|
23
|
-
|
|
24
|
-
Create a manifest for each round containing enough information to detect an incomplete or stale review view:
|
|
25
|
-
|
|
26
|
-
- repository identity and baseline revision
|
|
27
|
-
- round number or equivalent monotonic round identity
|
|
28
|
-
- changed path inventory and each path's status
|
|
29
|
-
- staged and unstaged composition when both exist for a path
|
|
30
|
-
- untracked file content required for review
|
|
31
|
-
- deleted-path markers
|
|
32
|
-
- rename source and destination pairs
|
|
33
|
-
- stable content identities for changed content and any review-critical context
|
|
34
|
-
- relevant spec, plan, dev-rule, and scoped-test evidence identities or embedded content
|
|
35
|
-
|
|
36
|
-
The exact encoding is implementation-defined. Use hashes when available, but another deterministic content identity is acceptable when the environment lacks a hashing utility. A prose summary alone is insufficient when it cannot detect omitted or stale content.
|
|
37
|
-
|
|
38
|
-
## Review Payload
|
|
39
|
-
|
|
40
|
-
Provide the Reviewer with both:
|
|
41
|
-
|
|
42
|
-
1. The manifest used for verification.
|
|
43
|
-
2. A reviewable representation of every manifest entry.
|
|
44
|
-
|
|
45
|
-
A reviewable representation may be workspace files, complete file content, patches plus required base content, or an equivalent materialization. Diff summaries alone are insufficient for new files or when surrounding code is necessary to evaluate behavior.
|
|
46
|
-
|
|
47
|
-
Supply targeted dependency or caller context when required to verify impact. Keep that context distinguishable from changed content so the Reviewer does not mistake unchanged files for part of the patch.
|
|
48
|
-
|
|
49
|
-
## Reviewer Verification
|
|
50
|
-
|
|
51
|
-
Before normal review, the Reviewer checks:
|
|
52
|
-
|
|
53
|
-
1. Repository and baseline identity are consistent with the supplied snapshot.
|
|
54
|
-
2. Every changed path in the manifest is available in the review payload.
|
|
55
|
-
3. No unexplained changed path appears outside the manifest.
|
|
56
|
-
4. Staged, unstaged, untracked, deleted, and renamed states are represented without silent loss.
|
|
57
|
-
5. Content identities match the manifest.
|
|
58
|
-
6. The snapshot belongs to the current review round.
|
|
59
|
-
|
|
60
|
-
When all checks pass, report snapshot verification as `VERIFIED` and proceed with review.
|
|
61
|
-
|
|
62
|
-
When a check cannot be completed or fails, stop normal review and return:
|
|
63
|
-
|
|
64
|
-
```text
|
|
65
|
-
### Result: WORKSPACE_MISMATCH
|
|
66
|
-
|
|
67
|
-
### Snapshot Verification
|
|
68
|
-
- Status: FAILED
|
|
69
|
-
- Mismatch: <missing, extra, stale, or unverifiable manifest entries>
|
|
70
|
-
- Required action: <what the Main Agent must re-capture, transport, or verify>
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Do not emit ordinary code findings after `WORKSPACE_MISMATCH`, because findings from an unverified snapshot could misrepresent the current change.
|
|
74
|
-
|
|
75
|
-
## Round Refresh
|
|
76
|
-
|
|
77
|
-
After accepted fixes are applied:
|
|
78
|
-
|
|
79
|
-
1. Discard the previous manifest as current evidence.
|
|
80
|
-
2. Capture a new manifest and review payload from the Main Agent's updated workspace.
|
|
81
|
-
3. Establish a fresh independent Reviewer view using any valid strategy.
|
|
82
|
-
4. Repeat Reviewer verification before the next review.
|
|
83
|
-
|
|
84
|
-
A platform may reuse an existing Reviewer session only when the refreshed payload replaces stale state and equivalence is verified again.
|
|
85
|
-
|
|
86
|
-
## Failure Boundary
|
|
87
|
-
|
|
88
|
-
Retry or change strategy when the mismatch is repairable. Stop when the platform cannot provide an independent Reviewer with a verifiable equivalent snapshot.
|
|
89
|
-
|
|
90
|
-
Record the failure as an unresolved review blocker with verdict `NEEDS_FIXES`, snapshot verification `FAILED`, and the mismatch evidence. Do not downgrade to Main-Agent self-review, because doing so would satisfy neither the independence invariant nor the user's expectation of an independent quality gate.
|
|
@@ -1,186 +0,0 @@
|
|
|
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
|
|
44
|
-
|
|
45
|
-
Divergence detection:
|
|
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
|
-
"""
|
|
49
|
-
|
|
50
|
-
import json
|
|
51
|
-
import sys
|
|
52
|
-
|
|
53
|
-
DEFAULT_MAX_ROUNDS = 3
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
target_index = current_round - 1
|
|
70
|
-
while len(findings_history) < target_index:
|
|
71
|
-
findings_history.append(0)
|
|
72
|
-
|
|
73
|
-
if len(findings_history) == target_index:
|
|
74
|
-
findings_history.append(findings_count)
|
|
75
|
-
else:
|
|
76
|
-
findings_history[target_index] = findings_count
|
|
77
|
-
|
|
78
|
-
return findings_history
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def check_exit_condition(data):
|
|
82
|
-
"""Determine whether the loop should exit. Pure function — no side effects.
|
|
83
|
-
|
|
84
|
-
Round is model-supplied — the script does not auto-increment it because
|
|
85
|
-
the gate is called twice per loop iteration (after Step 2 and Step 3).
|
|
86
|
-
"""
|
|
87
|
-
reviewer_result = data.get("reviewer_result")
|
|
88
|
-
accepted_count = data.get("accepted_count", 0)
|
|
89
|
-
findings_count = data.get("findings_count", 0)
|
|
90
|
-
current_round = data.get("round", 0)
|
|
91
|
-
findings_history = list(data.get("findings_history", []))
|
|
92
|
-
max_rounds = data.get("max_rounds", DEFAULT_MAX_ROUNDS)
|
|
93
|
-
filtering_done = data.get("filtering_done", reviewer_result == "PASS")
|
|
94
|
-
|
|
95
|
-
findings_history = record_findings_count(
|
|
96
|
-
findings_history, current_round, findings_count
|
|
97
|
-
)
|
|
98
|
-
divergence_warning = check_divergence(findings_history)
|
|
99
|
-
|
|
100
|
-
# Condition 1: Reviewer returned PASS
|
|
101
|
-
if reviewer_result == "PASS":
|
|
102
|
-
return {
|
|
103
|
-
"endLoop": True,
|
|
104
|
-
"reason": f"Round {current_round}: Reviewer returned PASS",
|
|
105
|
-
"verdict": "PASS",
|
|
106
|
-
"round": current_round,
|
|
107
|
-
"maxRounds": max_rounds,
|
|
108
|
-
"divergenceWarning": False,
|
|
109
|
-
"findings_history": findings_history,
|
|
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
|
|
150
|
-
return {
|
|
151
|
-
"endLoop": False,
|
|
152
|
-
"reason": f"Round {current_round}: {accepted_count} findings accepted, continuing to round {next_round}",
|
|
153
|
-
"verdict": None,
|
|
154
|
-
"round": current_round,
|
|
155
|
-
"maxRounds": max_rounds,
|
|
156
|
-
"divergenceWarning": divergence_warning,
|
|
157
|
-
"findings_history": findings_history,
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def main():
|
|
162
|
-
# Read input from stdin
|
|
163
|
-
try:
|
|
164
|
-
data = json.load(sys.stdin)
|
|
165
|
-
except json.JSONDecodeError as e:
|
|
166
|
-
print(
|
|
167
|
-
json.dumps(
|
|
168
|
-
{
|
|
169
|
-
"endLoop": False,
|
|
170
|
-
"reason": f"Invalid JSON input: {e}",
|
|
171
|
-
"verdict": None,
|
|
172
|
-
"round": 0,
|
|
173
|
-
"maxRounds": DEFAULT_MAX_ROUNDS,
|
|
174
|
-
"divergenceWarning": False,
|
|
175
|
-
"findings_history": [],
|
|
176
|
-
}
|
|
177
|
-
)
|
|
178
|
-
)
|
|
179
|
-
sys.exit(1)
|
|
180
|
-
|
|
181
|
-
result = check_exit_condition(data)
|
|
182
|
-
print(json.dumps(result))
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if __name__ == "__main__":
|
|
186
|
-
main()
|