devlyn-cli 2.9.3 → 2.10.1
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/AGENTS.md +3 -3
- package/CLAUDE.md +5 -5
- package/README.md +5 -5
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +1 -1
- package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +1 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +11 -1
- package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +83 -1
- package/bin/devlyn.js +108 -15
- package/config/skills/_shared/adapters/README.md +2 -2
- package/config/skills/_shared/adapters/grok.md +99 -0
- package/config/skills/_shared/archive_run.py +35 -1
- package/config/skills/_shared/collect-codex-findings.py +231 -50
- package/config/skills/_shared/engine-doctor.sh +6 -3
- package/config/skills/_shared/engine-preflight.md +12 -5
- package/config/skills/_shared/expected.schema.json +7 -0
- package/config/skills/_shared/grok-anchor-guard.py +61 -0
- package/config/skills/_shared/phase-prompt-render.py +181 -0
- package/config/skills/_shared/resolve-bootstrap.py +630 -0
- package/config/skills/_shared/resolve-stop-hook.py +325 -0
- package/config/skills/_shared/spec-verify-check.py +308 -31
- package/config/skills/_shared/state-phase-write.py +2966 -27
- package/config/skills/_shared/terminal-claim-check.py +537 -0
- package/config/skills/_shared/verify-merge-findings.py +533 -16
- package/config/skills/devlyn:engines/SKILL.md +2 -2
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:queue/SKILL.md +12 -0
- package/config/skills/devlyn:resolve/SKILL.md +60 -78
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/plan.md +1 -5
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +4 -2
- package/config/skills/devlyn:resolve/references/phases/surface-close.md +34 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +70 -97
- package/config/skills/devlyn:resolve/references/state-schema.md +26 -19
- package/optional-skills/devlyn:reap/SKILL.md +1 -1
- package/optional-skills/devlyn:reap/scripts/reap.sh +25 -13
- package/optional-skills/devlyn:reap/scripts/scan.sh +15 -1
- package/package.json +1 -1
- package/scripts/lint-fixtures.sh +4 -1
- package/scripts/lint-skills.sh +269 -77
|
@@ -41,6 +41,7 @@ ALLOWED_PAIR_SKIP_REASONS = {
|
|
|
41
41
|
"auto_pair_other_engine_unavailable",
|
|
42
42
|
}
|
|
43
43
|
KNOWN_PAIR_TRIGGER_REASONS = {
|
|
44
|
+
"pair.default",
|
|
44
45
|
"mode.verify-only",
|
|
45
46
|
"mode.pair-verify",
|
|
46
47
|
"complexity.high",
|
|
@@ -109,6 +110,10 @@ def all_known_pair_trigger_reasons(reasons: list[str]) -> bool:
|
|
|
109
110
|
return all(is_known_pair_trigger_reason(reason) for reason in reasons)
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def state_uses_default_pair_contract(state: dict[str, Any]) -> bool:
|
|
114
|
+
return state.get("version") == "3.0"
|
|
115
|
+
|
|
116
|
+
|
|
112
117
|
def finding_rank(finding: dict[str, Any]) -> int:
|
|
113
118
|
severity = str(finding.get("severity") or "").upper()
|
|
114
119
|
if severity in {"CRITICAL", "HIGH"}:
|
|
@@ -164,6 +169,44 @@ def read_findings(devlyn: pathlib.Path) -> tuple[list[dict[str, Any]], dict[str,
|
|
|
164
169
|
source_verdicts[source], RANK_VERDICT[finding_rank(item)]
|
|
165
170
|
)
|
|
166
171
|
findings.extend(detect_pair_stdout_contract_violations(devlyn, source_verdicts))
|
|
172
|
+
pair_summary_path = devlyn / "pair-judge.summary.json"
|
|
173
|
+
pair_carrier_exists = any(
|
|
174
|
+
(devlyn / name).is_file()
|
|
175
|
+
for name in ("verify.pair.findings.jsonl", "verify.pair-judge.findings.jsonl")
|
|
176
|
+
)
|
|
177
|
+
if pair_carrier_exists and pair_summary_path.is_file():
|
|
178
|
+
try:
|
|
179
|
+
pair_summary = loads_strict_json(pair_summary_path.read_text(encoding="utf-8"))
|
|
180
|
+
except ValueError as exc:
|
|
181
|
+
summary_error = f"pair-judge.summary.json is malformed JSON: {exc}"
|
|
182
|
+
else:
|
|
183
|
+
pair_verdict = pair_summary.get("verdict") if isinstance(pair_summary, dict) else None
|
|
184
|
+
summary_error = None
|
|
185
|
+
if not isinstance(pair_summary, dict):
|
|
186
|
+
summary_error = "pair-judge.summary.json must be a JSON object."
|
|
187
|
+
elif not isinstance(pair_verdict, str) or pair_verdict not in {
|
|
188
|
+
"PASS", "PASS_WITH_ISSUES", "FAIL", "NEEDS_WORK", "BLOCKED"
|
|
189
|
+
}:
|
|
190
|
+
summary_error = (
|
|
191
|
+
"pair-judge.summary.json verdict must be PASS, PASS_WITH_ISSUES, "
|
|
192
|
+
"FAIL, NEEDS_WORK, or BLOCKED."
|
|
193
|
+
)
|
|
194
|
+
if summary_error is not None:
|
|
195
|
+
findings.append(
|
|
196
|
+
pair_blocker(
|
|
197
|
+
"verify-pair-summary-invalid",
|
|
198
|
+
summary_error,
|
|
199
|
+
pair_summary_path.name,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
source_verdicts["pair_judge"] = "BLOCKED"
|
|
203
|
+
elif (
|
|
204
|
+
source_verdicts["pair_judge"] != "TIMEOUT"
|
|
205
|
+
or rank(pair_verdict) > rank("TIMEOUT")
|
|
206
|
+
):
|
|
207
|
+
source_verdicts["pair_judge"] = worse(
|
|
208
|
+
source_verdicts["pair_judge"], pair_verdict
|
|
209
|
+
)
|
|
167
210
|
return findings, source_verdicts
|
|
168
211
|
|
|
169
212
|
|
|
@@ -338,6 +381,63 @@ def risk_profile_contract_violation(devlyn: pathlib.Path) -> dict[str, Any] | No
|
|
|
338
381
|
return None
|
|
339
382
|
|
|
340
383
|
|
|
384
|
+
def verify_state_contract_violation(devlyn: pathlib.Path) -> dict[str, Any] | None:
|
|
385
|
+
state_path = devlyn / "pipeline.state.json"
|
|
386
|
+
if not state_path.is_file():
|
|
387
|
+
return None
|
|
388
|
+
try:
|
|
389
|
+
state = loads_strict_json(state_path.read_text(encoding="utf-8"))
|
|
390
|
+
except ValueError:
|
|
391
|
+
return None
|
|
392
|
+
if not isinstance(state, dict) or not state_uses_default_pair_contract(state):
|
|
393
|
+
return None
|
|
394
|
+
source = state.get("source")
|
|
395
|
+
if not isinstance(source, dict) or source.get("type") != "generated":
|
|
396
|
+
return None
|
|
397
|
+
goal_path = source.get("goal_path")
|
|
398
|
+
goal_sha256 = source.get("goal_sha256")
|
|
399
|
+
if (
|
|
400
|
+
not isinstance(goal_path, str)
|
|
401
|
+
or not goal_path
|
|
402
|
+
or not isinstance(goal_sha256, str)
|
|
403
|
+
or re.fullmatch(r"[0-9a-f]{64}", goal_sha256) is None
|
|
404
|
+
):
|
|
405
|
+
rule = "verify.state.goal-persistence-missing"
|
|
406
|
+
return {
|
|
407
|
+
"id": rule,
|
|
408
|
+
"rule_id": rule,
|
|
409
|
+
"message": (
|
|
410
|
+
"schema-v3 generated runs require source.goal_path as a non-empty string "
|
|
411
|
+
"and source.goal_sha256 as 64 lowercase hexadecimal characters."
|
|
412
|
+
),
|
|
413
|
+
"file": "pipeline.state.json",
|
|
414
|
+
}
|
|
415
|
+
if state.get("complexity") in {"trivial", "medium"}:
|
|
416
|
+
phases = state.get("phases")
|
|
417
|
+
surface_close = phases.get("surface_close") if isinstance(phases, dict) else None
|
|
418
|
+
skipped = (
|
|
419
|
+
isinstance(surface_close, dict)
|
|
420
|
+
and surface_close.get("verdict") is None
|
|
421
|
+
and surface_close.get("started_at") is None
|
|
422
|
+
and surface_close.get("skipped_reason") == "auto_surface_close_claude_unavailable"
|
|
423
|
+
)
|
|
424
|
+
if (
|
|
425
|
+
not isinstance(surface_close, dict)
|
|
426
|
+
or (surface_close.get("verdict") is None and not skipped)
|
|
427
|
+
):
|
|
428
|
+
rule = "verify.state.surface-close-skipped"
|
|
429
|
+
return {
|
|
430
|
+
"id": rule,
|
|
431
|
+
"rule_id": rule,
|
|
432
|
+
"message": (
|
|
433
|
+
"schema-v3 generated trivial/medium runs require completed SURFACE_CLOSE "
|
|
434
|
+
"or its canonical automatic Claude-unavailable skip before VERIFY merge."
|
|
435
|
+
),
|
|
436
|
+
"file": "pipeline.state.json",
|
|
437
|
+
}
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
|
|
341
441
|
def source_spec_text(state: dict[str, Any]) -> str | None:
|
|
342
442
|
source = state.get("source") if isinstance(state.get("source"), dict) else {}
|
|
343
443
|
for key in ("spec_path", "criteria_path"):
|
|
@@ -424,6 +524,11 @@ def state_pair_trigger_reasons(
|
|
|
424
524
|
verify_state = state.get("verify") if isinstance(state.get("verify"), dict) else {}
|
|
425
525
|
risk_profile = state.get("risk_profile") if isinstance(state.get("risk_profile"), dict) else {}
|
|
426
526
|
reasons: list[str] = []
|
|
527
|
+
if (
|
|
528
|
+
state_uses_default_pair_contract(state)
|
|
529
|
+
and risk_profile.get("pair_default_enabled") is not False
|
|
530
|
+
):
|
|
531
|
+
reasons.append("pair.default")
|
|
427
532
|
if state.get("mode") == "verify-only":
|
|
428
533
|
reasons.append("mode.verify-only")
|
|
429
534
|
if state.get("pair_verify") is True:
|
|
@@ -455,11 +560,19 @@ def state_pair_trigger_reasons(
|
|
|
455
560
|
return reasons
|
|
456
561
|
|
|
457
562
|
|
|
563
|
+
def outcome_independent_reasons(devlyn: pathlib.Path) -> list[str]:
|
|
564
|
+
return [
|
|
565
|
+
reason
|
|
566
|
+
for reason in state_pair_trigger_reasons(devlyn, {})
|
|
567
|
+
if reason not in ("coverage.failed", "mechanical.warning", "judge.warning")
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
|
|
458
571
|
def pair_trigger_missing_contract_violation(
|
|
459
572
|
devlyn: pathlib.Path,
|
|
460
573
|
source_verdicts: dict[str, str | None],
|
|
461
574
|
) -> dict[str, Any] | None:
|
|
462
|
-
if rank(source_verdicts.get("mechanical")) >= 2
|
|
575
|
+
if rank(source_verdicts.get("mechanical")) >= 2:
|
|
463
576
|
return None
|
|
464
577
|
reasons = state_pair_trigger_reasons(devlyn, source_verdicts)
|
|
465
578
|
if not reasons:
|
|
@@ -558,15 +671,36 @@ def pair_trigger_skip_contract_violation(
|
|
|
558
671
|
),
|
|
559
672
|
"file": "pipeline.state.json",
|
|
560
673
|
}
|
|
561
|
-
if skipped_reason == "primary_judge_blocker"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
674
|
+
if skipped_reason == "primary_judge_blocker":
|
|
675
|
+
if state_uses_default_pair_contract(state):
|
|
676
|
+
return {
|
|
677
|
+
"id": "verify-pair-trigger-primary-judge-blocker-retired",
|
|
678
|
+
"message": (
|
|
679
|
+
"pair_trigger skipped_reason primary_judge_blocker is archived-v2.0 "
|
|
680
|
+
"state only; schema-v3 runs must dispatch the pair-JUDGE."
|
|
681
|
+
),
|
|
682
|
+
"file": "pipeline.state.json",
|
|
683
|
+
}
|
|
684
|
+
if rank(source_verdicts.get("judge")) < 2:
|
|
685
|
+
return {
|
|
686
|
+
"id": "verify-pair-trigger-primary-judge-blocker-unsupported",
|
|
687
|
+
"message": (
|
|
688
|
+
"pair_trigger skipped_reason primary_judge_blocker requires a "
|
|
689
|
+
"verdict-binding primary JUDGE finding."
|
|
690
|
+
),
|
|
691
|
+
"file": "pipeline.state.json",
|
|
692
|
+
}
|
|
693
|
+
preknown_reasons = outcome_independent_reasons(devlyn)
|
|
694
|
+
if preknown_reasons:
|
|
695
|
+
return {
|
|
696
|
+
"id": "verify-pair-trigger-primary-judge-blocker-preknown",
|
|
697
|
+
"message": (
|
|
698
|
+
"pair_trigger cannot skip the pair-JUDGE for a primary JUDGE blocker "
|
|
699
|
+
"when outcome-independent reasons applied at spawn: "
|
|
700
|
+
+ ", ".join(preknown_reasons)
|
|
701
|
+
),
|
|
702
|
+
"file": "pipeline.state.json",
|
|
703
|
+
}
|
|
570
704
|
return None
|
|
571
705
|
|
|
572
706
|
|
|
@@ -574,7 +708,7 @@ def pair_trigger_reason_completeness_violation(
|
|
|
574
708
|
devlyn: pathlib.Path,
|
|
575
709
|
source_verdicts: dict[str, str | None],
|
|
576
710
|
) -> dict[str, Any] | None:
|
|
577
|
-
if rank(source_verdicts.get("mechanical")) >= 2
|
|
711
|
+
if rank(source_verdicts.get("mechanical")) >= 2:
|
|
578
712
|
return None
|
|
579
713
|
state_path = devlyn / "pipeline.state.json"
|
|
580
714
|
if not state_path.is_file():
|
|
@@ -614,10 +748,15 @@ def pair_trigger_reason_completeness_violation(
|
|
|
614
748
|
}
|
|
615
749
|
|
|
616
750
|
|
|
617
|
-
def pair_blocker(
|
|
751
|
+
def pair_blocker(
|
|
752
|
+
id_: str,
|
|
753
|
+
message: str,
|
|
754
|
+
file_: str | None = None,
|
|
755
|
+
rule_id: str = "verify.pair.emission-contract",
|
|
756
|
+
) -> dict[str, Any]:
|
|
618
757
|
return {
|
|
619
758
|
"id": id_,
|
|
620
|
-
"rule_id":
|
|
759
|
+
"rule_id": rule_id,
|
|
621
760
|
"severity": "CRITICAL",
|
|
622
761
|
"confidence": "high",
|
|
623
762
|
"file": file_,
|
|
@@ -715,6 +854,17 @@ def detect_pair_stdout_contract_violations(
|
|
|
715
854
|
risk_profile_violation["file"],
|
|
716
855
|
)
|
|
717
856
|
]
|
|
857
|
+
state_violation = verify_state_contract_violation(devlyn)
|
|
858
|
+
if state_violation is not None:
|
|
859
|
+
source_verdicts["pair_judge"] = "BLOCKED"
|
|
860
|
+
return [
|
|
861
|
+
pair_blocker(
|
|
862
|
+
state_violation["id"],
|
|
863
|
+
state_violation["message"],
|
|
864
|
+
state_violation["file"],
|
|
865
|
+
state_violation["rule_id"],
|
|
866
|
+
)
|
|
867
|
+
]
|
|
718
868
|
if not required and not pair_trigger_present(devlyn):
|
|
719
869
|
missing_violation = pair_trigger_missing_contract_violation(devlyn, source_verdicts)
|
|
720
870
|
if missing_violation is not None:
|
|
@@ -892,7 +1042,15 @@ def self_test() -> int:
|
|
|
892
1042
|
# the real shape write_state() sees on every VERIFY completion, not
|
|
893
1043
|
# the pre-populated {} the other scenarios below seed for brevity.
|
|
894
1044
|
(devlyn / "pipeline.state.json").write_text(
|
|
895
|
-
json.dumps({
|
|
1045
|
+
json.dumps({
|
|
1046
|
+
"phases": {
|
|
1047
|
+
"verify": {
|
|
1048
|
+
"verdict": None,
|
|
1049
|
+
"sub_verdicts": None,
|
|
1050
|
+
"judge_durations_ms": {"judge": 23, "pair_judge": 31},
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}),
|
|
896
1054
|
encoding="utf-8",
|
|
897
1055
|
)
|
|
898
1056
|
(devlyn / "verify.findings.jsonl").write_text("", encoding="utf-8")
|
|
@@ -904,13 +1062,130 @@ def self_test() -> int:
|
|
|
904
1062
|
assert state["phases"]["verify"]["sub_verdicts"] == {
|
|
905
1063
|
"mechanical": "PASS", "judge": "PASS", "pair_judge": None,
|
|
906
1064
|
}, state
|
|
1065
|
+
assert state["phases"]["verify"]["judge_durations_ms"] == {
|
|
1066
|
+
"judge": 23, "pair_judge": 31,
|
|
1067
|
+
}, state
|
|
1068
|
+
|
|
1069
|
+
# iter-0072 Amendment 3: generated schema-v3 runs mechanically prove
|
|
1070
|
+
# raw-goal persistence and required SURFACE_CLOSE dispatch.
|
|
1071
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1072
|
+
json.dumps({
|
|
1073
|
+
"version": "3.0",
|
|
1074
|
+
"complexity": "large",
|
|
1075
|
+
"source": {"type": "generated"},
|
|
1076
|
+
"phases": {"verify": {"verdict": None, "sub_verdicts": None}},
|
|
1077
|
+
}),
|
|
1078
|
+
encoding="utf-8",
|
|
1079
|
+
)
|
|
1080
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
1081
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
1082
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
1083
|
+
assert summary["source_verdicts"]["pair_judge"] == "BLOCKED", summary
|
|
1084
|
+
assert any(
|
|
1085
|
+
finding.get("rule_id") == "verify.state.goal-persistence-missing"
|
|
1086
|
+
for finding in findings
|
|
1087
|
+
), findings
|
|
1088
|
+
|
|
1089
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1090
|
+
json.dumps({
|
|
1091
|
+
"version": "3.0",
|
|
1092
|
+
"complexity": "large",
|
|
1093
|
+
"source": {"type": "generated", "goal_path": "", "goal_sha256": "A" * 64},
|
|
1094
|
+
}),
|
|
1095
|
+
encoding="utf-8",
|
|
1096
|
+
)
|
|
1097
|
+
violation = verify_state_contract_violation(devlyn)
|
|
1098
|
+
assert violation is not None, violation
|
|
1099
|
+
assert violation["rule_id"] == "verify.state.goal-persistence-missing", violation
|
|
1100
|
+
|
|
1101
|
+
generated_source = {
|
|
1102
|
+
"type": "generated",
|
|
1103
|
+
"goal_path": ".devlyn/goal.raw.txt",
|
|
1104
|
+
"goal_sha256": "a" * 64,
|
|
1105
|
+
}
|
|
1106
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1107
|
+
json.dumps({
|
|
1108
|
+
"version": "3.0",
|
|
1109
|
+
"complexity": "medium",
|
|
1110
|
+
"source": generated_source,
|
|
1111
|
+
"phases": {
|
|
1112
|
+
"surface_close": None,
|
|
1113
|
+
"verify": {"verdict": None, "sub_verdicts": None},
|
|
1114
|
+
},
|
|
1115
|
+
}),
|
|
1116
|
+
encoding="utf-8",
|
|
1117
|
+
)
|
|
1118
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
1119
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
1120
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
1121
|
+
assert any(
|
|
1122
|
+
finding.get("rule_id") == "verify.state.surface-close-skipped"
|
|
1123
|
+
for finding in findings
|
|
1124
|
+
), findings
|
|
1125
|
+
|
|
1126
|
+
for surface_entry in (
|
|
1127
|
+
{"verdict": "PASS"},
|
|
1128
|
+
{
|
|
1129
|
+
"verdict": None,
|
|
1130
|
+
"started_at": None,
|
|
1131
|
+
"skipped_reason": "auto_surface_close_claude_unavailable",
|
|
1132
|
+
},
|
|
1133
|
+
):
|
|
1134
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1135
|
+
json.dumps({
|
|
1136
|
+
"version": "3.0",
|
|
1137
|
+
"complexity": "medium",
|
|
1138
|
+
"source": generated_source,
|
|
1139
|
+
"phases": {"surface_close": surface_entry},
|
|
1140
|
+
}),
|
|
1141
|
+
encoding="utf-8",
|
|
1142
|
+
)
|
|
1143
|
+
assert verify_state_contract_violation(devlyn) is None
|
|
1144
|
+
|
|
1145
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1146
|
+
json.dumps({
|
|
1147
|
+
"version": "3.0",
|
|
1148
|
+
"complexity": "medium",
|
|
1149
|
+
"source": generated_source,
|
|
1150
|
+
"phases": {
|
|
1151
|
+
"surface_close": {
|
|
1152
|
+
"verdict": None,
|
|
1153
|
+
"started_at": None,
|
|
1154
|
+
"skipped_reason": "claude-unavailable",
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
}),
|
|
1158
|
+
encoding="utf-8",
|
|
1159
|
+
)
|
|
1160
|
+
assert verify_state_contract_violation(devlyn)["rule_id"] == "verify.state.surface-close-skipped"
|
|
1161
|
+
|
|
1162
|
+
for state_shape in (
|
|
1163
|
+
{
|
|
1164
|
+
"version": "3.0", "complexity": "medium",
|
|
1165
|
+
"source": {"type": "spec"}, "phases": {"surface_close": None},
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
"version": "3.0", "complexity": "large",
|
|
1169
|
+
"source": generated_source, "phases": {"surface_close": None},
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
"version": "2.0", "complexity": "medium",
|
|
1173
|
+
"source": {"type": "generated"}, "phases": {"surface_close": None},
|
|
1174
|
+
},
|
|
1175
|
+
):
|
|
1176
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1177
|
+
json.dumps(state_shape), encoding="utf-8",
|
|
1178
|
+
)
|
|
1179
|
+
assert verify_state_contract_violation(devlyn) is None
|
|
907
1180
|
|
|
908
1181
|
# 2026-07-04 field bug (iter-0060 G1): an AUTO pair trigger skipped on
|
|
909
1182
|
# OTHER-engine unavailability spawns no second judge — pair_judge must
|
|
910
1183
|
# be recorded null, never a synthesized PASS.
|
|
911
1184
|
(devlyn / "pipeline.state.json").write_text(
|
|
912
1185
|
json.dumps({
|
|
1186
|
+
"version": "3.0",
|
|
913
1187
|
"mode": "spec",
|
|
1188
|
+
"risk_profile": {"pair_default_enabled": True},
|
|
914
1189
|
"phases": {
|
|
915
1190
|
"verify": {
|
|
916
1191
|
"verdict": None,
|
|
@@ -1936,18 +2211,128 @@ def self_test() -> int:
|
|
|
1936
2211
|
for finding in findings
|
|
1937
2212
|
), findings
|
|
1938
2213
|
|
|
2214
|
+
# Self-test: preknown_primary_blocker_requires_pair.
|
|
2215
|
+
(devlyn / "verify.pair.findings.jsonl").unlink(missing_ok=True)
|
|
1939
2216
|
(devlyn / "verify.findings.jsonl").write_text(
|
|
1940
|
-
json.dumps({"id": "
|
|
2217
|
+
json.dumps({"id": "j-preknown", "severity": "HIGH"}) + "\n",
|
|
2218
|
+
encoding="utf-8",
|
|
2219
|
+
)
|
|
2220
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2221
|
+
json.dumps({
|
|
2222
|
+
"version": "2.0",
|
|
2223
|
+
"mode": "spec",
|
|
2224
|
+
"pair_verify": True,
|
|
2225
|
+
"phases": {
|
|
2226
|
+
"verify": {
|
|
2227
|
+
"verdict": "PASS",
|
|
2228
|
+
"sub_verdicts": {},
|
|
2229
|
+
"pair_trigger": {
|
|
2230
|
+
"eligible": False,
|
|
2231
|
+
"reasons": [],
|
|
2232
|
+
"skipped_reason": "primary_judge_blocker",
|
|
2233
|
+
},
|
|
2234
|
+
}
|
|
2235
|
+
},
|
|
2236
|
+
}),
|
|
2237
|
+
encoding="utf-8",
|
|
2238
|
+
)
|
|
2239
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2240
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2241
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2242
|
+
assert any(
|
|
2243
|
+
finding.get("id") == "verify-pair-trigger-primary-judge-blocker-preknown"
|
|
2244
|
+
and "mode.pair-verify" in str(finding.get("message"))
|
|
2245
|
+
for finding in findings
|
|
2246
|
+
), findings
|
|
2247
|
+
|
|
2248
|
+
# Self-test: schema-v3 default pair reason is required before merge.
|
|
2249
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2250
|
+
json.dumps({
|
|
2251
|
+
"version": "3.0",
|
|
2252
|
+
"mode": "spec",
|
|
2253
|
+
"pair_verify": True,
|
|
2254
|
+
"phases": {
|
|
2255
|
+
"verify": {
|
|
2256
|
+
"verdict": "PASS",
|
|
2257
|
+
"sub_verdicts": {},
|
|
2258
|
+
"pair_trigger": {
|
|
2259
|
+
"eligible": True,
|
|
2260
|
+
"reasons": ["mode.pair-verify"],
|
|
2261
|
+
"skipped_reason": None,
|
|
2262
|
+
},
|
|
2263
|
+
}
|
|
2264
|
+
},
|
|
2265
|
+
}),
|
|
2266
|
+
encoding="utf-8",
|
|
2267
|
+
)
|
|
2268
|
+
(devlyn / "verify.pair.findings.jsonl").write_text(
|
|
2269
|
+
json.dumps({"id": "p-preknown", "severity": "LOW"}) + "\n",
|
|
2270
|
+
encoding="utf-8",
|
|
2271
|
+
)
|
|
2272
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2273
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2274
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2275
|
+
assert any(
|
|
2276
|
+
finding.get("id") == "verify-pair-trigger-reasons-incomplete"
|
|
2277
|
+
and "pair.default" in str(finding.get("message"))
|
|
2278
|
+
for finding in findings
|
|
2279
|
+
), findings
|
|
2280
|
+
state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
2281
|
+
state["phases"]["verify"]["pair_trigger"]["reasons"].insert(0, "pair.default")
|
|
2282
|
+
(devlyn / "pipeline.state.json").write_text(json.dumps(state), encoding="utf-8")
|
|
2283
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2284
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2285
|
+
assert summary["verdict"] == "NEEDS_WORK", summary
|
|
2286
|
+
assert summary["source_verdicts"]["judge"] == "NEEDS_WORK", summary
|
|
2287
|
+
assert summary["source_verdicts"]["pair_judge"] == "PASS_WITH_ISSUES", summary
|
|
2288
|
+
assert '"id":"p-preknown"' in (
|
|
2289
|
+
devlyn / "verify-merged.findings.jsonl"
|
|
2290
|
+
).read_text(encoding="utf-8"), findings
|
|
2291
|
+
|
|
2292
|
+
# Self-test: archived-v2 sequential primary blocker remains legal.
|
|
2293
|
+
(devlyn / "verify.pair.findings.jsonl").unlink()
|
|
2294
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2295
|
+
json.dumps({
|
|
2296
|
+
"version": "2.0",
|
|
2297
|
+
"mode": "spec",
|
|
2298
|
+
"phases": {
|
|
2299
|
+
"verify": {
|
|
2300
|
+
"verdict": "PASS",
|
|
2301
|
+
"sub_verdicts": {},
|
|
2302
|
+
"pair_trigger": {
|
|
2303
|
+
"eligible": False,
|
|
2304
|
+
"reasons": [],
|
|
2305
|
+
"skipped_reason": "primary_judge_blocker",
|
|
2306
|
+
},
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
}),
|
|
1941
2310
|
encoding="utf-8",
|
|
1942
2311
|
)
|
|
1943
2312
|
findings, source_verdicts = read_findings(devlyn)
|
|
1944
2313
|
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
1945
2314
|
assert summary["verdict"] == "NEEDS_WORK", summary
|
|
2315
|
+
assert summary["source_verdicts"]["pair_judge"] is None, summary
|
|
1946
2316
|
assert not any(
|
|
1947
|
-
finding.get("id")
|
|
2317
|
+
finding.get("id") in {
|
|
2318
|
+
"verify-pair-trigger-primary-judge-blocker-unsupported",
|
|
2319
|
+
"verify-pair-trigger-primary-judge-blocker-preknown",
|
|
2320
|
+
}
|
|
2321
|
+
for finding in findings
|
|
2322
|
+
), findings
|
|
2323
|
+
# Replay the same state as schema v3 and require the retired-skip blocker.
|
|
2324
|
+
state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
2325
|
+
state["version"] = "3.0"
|
|
2326
|
+
(devlyn / "pipeline.state.json").write_text(json.dumps(state), encoding="utf-8")
|
|
2327
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2328
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2329
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2330
|
+
assert any(
|
|
2331
|
+
finding.get("id") == "verify-pair-trigger-primary-judge-blocker-retired"
|
|
1948
2332
|
for finding in findings
|
|
1949
2333
|
), findings
|
|
1950
2334
|
(devlyn / "verify.findings.jsonl").write_text("", encoding="utf-8")
|
|
2335
|
+
(devlyn / "verify.pair.findings.jsonl").write_text("", encoding="utf-8")
|
|
1951
2336
|
|
|
1952
2337
|
(devlyn / "pipeline.state.json").write_text(
|
|
1953
2338
|
json.dumps({
|
|
@@ -1984,6 +2369,138 @@ def self_test() -> int:
|
|
|
1984
2369
|
state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
1985
2370
|
assert summary["verdict"] == "PASS", summary
|
|
1986
2371
|
assert state["phases"]["verify"]["sub_verdicts"]["pair_judge"] == "PASS", state
|
|
2372
|
+
|
|
2373
|
+
# iter-0083: canonical summary verdict conservation.
|
|
2374
|
+
iter_0083_paths = (
|
|
2375
|
+
"pipeline.state.json",
|
|
2376
|
+
"verify-mechanical.findings.jsonl",
|
|
2377
|
+
"verify.findings.jsonl",
|
|
2378
|
+
"verify.pair.findings.jsonl",
|
|
2379
|
+
"verify.pair-judge.findings.jsonl",
|
|
2380
|
+
"pair-judge.summary.json",
|
|
2381
|
+
"codex-primary-judge.summary.json",
|
|
2382
|
+
"grok-judge.summary.json",
|
|
2383
|
+
"verify.pair.timeout.json",
|
|
2384
|
+
"codex-judge.stdout",
|
|
2385
|
+
"claude-judge.stdout",
|
|
2386
|
+
)
|
|
2387
|
+
|
|
2388
|
+
def iter_0083_reset() -> None:
|
|
2389
|
+
for name in iter_0083_paths:
|
|
2390
|
+
(devlyn / name).unlink(missing_ok=True)
|
|
2391
|
+
|
|
2392
|
+
def iter_0083_case(
|
|
2393
|
+
summary_payload: object | None,
|
|
2394
|
+
severity: str | None,
|
|
2395
|
+
*,
|
|
2396
|
+
carrier: str | None = "verify.pair.findings.jsonl",
|
|
2397
|
+
other_summary: str | None = None,
|
|
2398
|
+
timeout: bool = False,
|
|
2399
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
2400
|
+
iter_0083_reset()
|
|
2401
|
+
if carrier is not None:
|
|
2402
|
+
content = (
|
|
2403
|
+
json.dumps({"id": "iter-0083", "severity": severity}) + "\n"
|
|
2404
|
+
if severity
|
|
2405
|
+
else ""
|
|
2406
|
+
)
|
|
2407
|
+
(devlyn / carrier).write_text(content, encoding="utf-8")
|
|
2408
|
+
if summary_payload is not None:
|
|
2409
|
+
content = (
|
|
2410
|
+
summary_payload
|
|
2411
|
+
if isinstance(summary_payload, str)
|
|
2412
|
+
else json.dumps(summary_payload)
|
|
2413
|
+
)
|
|
2414
|
+
(devlyn / "pair-judge.summary.json").write_text(content, encoding="utf-8")
|
|
2415
|
+
if other_summary is not None:
|
|
2416
|
+
(devlyn / other_summary).write_text(
|
|
2417
|
+
json.dumps({"verdict": "BLOCKED"}), encoding="utf-8"
|
|
2418
|
+
)
|
|
2419
|
+
if timeout:
|
|
2420
|
+
(devlyn / "verify.pair.timeout.json").write_text(
|
|
2421
|
+
json.dumps({"engine": "claude", "budget_seconds": 600}),
|
|
2422
|
+
encoding="utf-8",
|
|
2423
|
+
)
|
|
2424
|
+
case_findings, case_source_verdicts = read_findings(devlyn)
|
|
2425
|
+
return case_findings, write_outputs(devlyn, case_findings, case_source_verdicts)
|
|
2426
|
+
|
|
2427
|
+
for case_id, verdict, severity, expected in (
|
|
2428
|
+
("P1", "NEEDS_WORK", "INFO", "NEEDS_WORK"),
|
|
2429
|
+
("P2", "NEEDS_WORK", "LOW", "NEEDS_WORK"),
|
|
2430
|
+
("P3", "NEEDS_WORK", "MEDIUM", "NEEDS_WORK"),
|
|
2431
|
+
("P4", "PASS", "HIGH", "NEEDS_WORK"),
|
|
2432
|
+
("P5", "BLOCKED", "INFO", "BLOCKED"),
|
|
2433
|
+
("P6", "FAIL", "INFO", "NEEDS_WORK"),
|
|
2434
|
+
("P7", "PASS", None, "PASS"),
|
|
2435
|
+
):
|
|
2436
|
+
_, summary = iter_0083_case({"verdict": verdict}, severity)
|
|
2437
|
+
assert summary["source_verdicts"]["pair_judge"] == expected, case_id
|
|
2438
|
+
assert summary["verdict"] == expected, case_id
|
|
2439
|
+
|
|
2440
|
+
for case_id, severity, expected in (
|
|
2441
|
+
("P8-INFO", "INFO", "PASS"),
|
|
2442
|
+
("P8-LOW", "LOW", "PASS_WITH_ISSUES"),
|
|
2443
|
+
):
|
|
2444
|
+
_, summary = iter_0083_case(None, severity)
|
|
2445
|
+
assert summary["source_verdicts"]["pair_judge"] == expected, case_id
|
|
2446
|
+
assert summary["verdict"] == expected, case_id
|
|
2447
|
+
|
|
2448
|
+
def assert_iter_0083_blocked(
|
|
2449
|
+
case_id: str,
|
|
2450
|
+
case_findings: list[dict[str, Any]],
|
|
2451
|
+
summary: dict[str, Any],
|
|
2452
|
+
) -> None:
|
|
2453
|
+
assert summary["source_verdicts"]["pair_judge"] == "BLOCKED", case_id
|
|
2454
|
+
assert summary["verdict"] == "BLOCKED", case_id
|
|
2455
|
+
assert any(
|
|
2456
|
+
finding.get("source") == "pair_judge"
|
|
2457
|
+
and finding.get("severity") == "CRITICAL"
|
|
2458
|
+
and finding.get("file") == "pair-judge.summary.json"
|
|
2459
|
+
for finding in case_findings
|
|
2460
|
+
), case_id
|
|
2461
|
+
|
|
2462
|
+
for case_id, payload in (
|
|
2463
|
+
("N1-malformed", "{"),
|
|
2464
|
+
("N1-non-object", []),
|
|
2465
|
+
("N1-unknown", {"verdict": "UNKNOWN"}),
|
|
2466
|
+
("N2", {}),
|
|
2467
|
+
("N7", {"verdict": "TIMEOUT"}),
|
|
2468
|
+
):
|
|
2469
|
+
case_findings, summary = iter_0083_case(payload, "INFO")
|
|
2470
|
+
assert_iter_0083_blocked(case_id, case_findings, summary)
|
|
2471
|
+
|
|
2472
|
+
_, summary = iter_0083_case({"verdict": "BLOCKED"}, None, carrier=None)
|
|
2473
|
+
assert summary["source_verdicts"]["pair_judge"] is None, summary
|
|
2474
|
+
assert summary["verdict"] == "PASS", summary
|
|
2475
|
+
|
|
2476
|
+
_, summary = iter_0083_case(
|
|
2477
|
+
{"verdict": "PASS"},
|
|
2478
|
+
"INFO",
|
|
2479
|
+
other_summary="codex-primary-judge.summary.json",
|
|
2480
|
+
)
|
|
2481
|
+
assert summary["source_verdicts"]["pair_judge"] == "PASS", "N4"
|
|
2482
|
+
assert summary["verdict"] == "PASS", "N4"
|
|
2483
|
+
|
|
2484
|
+
_, summary = iter_0083_case(
|
|
2485
|
+
None,
|
|
2486
|
+
"INFO",
|
|
2487
|
+
other_summary="grok-judge.summary.json",
|
|
2488
|
+
)
|
|
2489
|
+
assert summary["source_verdicts"]["pair_judge"] == "PASS", "N5"
|
|
2490
|
+
assert summary["verdict"] == "PASS", "N5"
|
|
2491
|
+
|
|
2492
|
+
_, summary = iter_0083_case(
|
|
2493
|
+
{"verdict": "NEEDS_WORK"},
|
|
2494
|
+
"INFO",
|
|
2495
|
+
carrier="verify.pair-judge.findings.jsonl",
|
|
2496
|
+
)
|
|
2497
|
+
assert summary["source_verdicts"]["pair_judge"] == "NEEDS_WORK", "N6"
|
|
2498
|
+
assert summary["verdict"] == "NEEDS_WORK", "N6"
|
|
2499
|
+
|
|
2500
|
+
case_findings, summary = iter_0083_case({"verdict": "BLOCKED"}, None, timeout=True)
|
|
2501
|
+
assert case_findings == [], "VERIFY-JUDGE-001"
|
|
2502
|
+
assert summary["source_verdicts"]["pair_judge"] == "BLOCKED", "VERIFY-JUDGE-001"
|
|
2503
|
+
assert summary["verdict"] == "BLOCKED", "VERIFY-JUDGE-001"
|
|
1987
2504
|
return 0
|
|
1988
2505
|
|
|
1989
2506
|
|