devlyn-cli 2.9.2 → 2.10.0
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 +4 -4
- package/README.md +6 -6
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +1 -1
- package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +1 -0
- package/bin/devlyn.js +68 -7
- package/config/skills/_shared/adapters/README.md +2 -2
- package/config/skills/_shared/adapters/grok.md +77 -0
- package/config/skills/_shared/archive_run.py +35 -1
- package/config/skills/_shared/collect-codex-findings.py +15 -10
- package/config/skills/_shared/engine-doctor.sh +6 -3
- package/config/skills/_shared/engine-preflight.md +12 -5
- package/config/skills/_shared/resolve-bootstrap.py +630 -0
- package/config/skills/_shared/resolve-stop-hook.py +325 -0
- package/config/skills/_shared/state-phase-write.py +2284 -41
- package/config/skills/_shared/terminal-claim-check.py +537 -0
- package/config/skills/_shared/verify-merge-findings.py +363 -16
- package/config/skills/devlyn:engines/SKILL.md +1 -1
- package/config/skills/devlyn:queue/SKILL.md +12 -0
- package/config/skills/devlyn:resolve/SKILL.md +55 -73
- package/config/skills/devlyn:resolve/references/free-form-mode.md +3 -2
- package/config/skills/devlyn:resolve/references/phases/surface-close.md +34 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +65 -96
- package/config/skills/devlyn:resolve/references/state-schema.md +29 -22
- 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-skills.sh +131 -67
|
@@ -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"}:
|
|
@@ -338,6 +343,63 @@ def risk_profile_contract_violation(devlyn: pathlib.Path) -> dict[str, Any] | No
|
|
|
338
343
|
return None
|
|
339
344
|
|
|
340
345
|
|
|
346
|
+
def verify_state_contract_violation(devlyn: pathlib.Path) -> dict[str, Any] | None:
|
|
347
|
+
state_path = devlyn / "pipeline.state.json"
|
|
348
|
+
if not state_path.is_file():
|
|
349
|
+
return None
|
|
350
|
+
try:
|
|
351
|
+
state = loads_strict_json(state_path.read_text(encoding="utf-8"))
|
|
352
|
+
except ValueError:
|
|
353
|
+
return None
|
|
354
|
+
if not isinstance(state, dict) or not state_uses_default_pair_contract(state):
|
|
355
|
+
return None
|
|
356
|
+
source = state.get("source")
|
|
357
|
+
if not isinstance(source, dict) or source.get("type") != "generated":
|
|
358
|
+
return None
|
|
359
|
+
goal_path = source.get("goal_path")
|
|
360
|
+
goal_sha256 = source.get("goal_sha256")
|
|
361
|
+
if (
|
|
362
|
+
not isinstance(goal_path, str)
|
|
363
|
+
or not goal_path
|
|
364
|
+
or not isinstance(goal_sha256, str)
|
|
365
|
+
or re.fullmatch(r"[0-9a-f]{64}", goal_sha256) is None
|
|
366
|
+
):
|
|
367
|
+
rule = "verify.state.goal-persistence-missing"
|
|
368
|
+
return {
|
|
369
|
+
"id": rule,
|
|
370
|
+
"rule_id": rule,
|
|
371
|
+
"message": (
|
|
372
|
+
"schema-v3 generated runs require source.goal_path as a non-empty string "
|
|
373
|
+
"and source.goal_sha256 as 64 lowercase hexadecimal characters."
|
|
374
|
+
),
|
|
375
|
+
"file": "pipeline.state.json",
|
|
376
|
+
}
|
|
377
|
+
if state.get("complexity") in {"trivial", "medium"}:
|
|
378
|
+
phases = state.get("phases")
|
|
379
|
+
surface_close = phases.get("surface_close") if isinstance(phases, dict) else None
|
|
380
|
+
skipped = (
|
|
381
|
+
isinstance(surface_close, dict)
|
|
382
|
+
and surface_close.get("verdict") is None
|
|
383
|
+
and surface_close.get("started_at") is None
|
|
384
|
+
and surface_close.get("skipped_reason") == "auto_surface_close_claude_unavailable"
|
|
385
|
+
)
|
|
386
|
+
if (
|
|
387
|
+
not isinstance(surface_close, dict)
|
|
388
|
+
or (surface_close.get("verdict") is None and not skipped)
|
|
389
|
+
):
|
|
390
|
+
rule = "verify.state.surface-close-skipped"
|
|
391
|
+
return {
|
|
392
|
+
"id": rule,
|
|
393
|
+
"rule_id": rule,
|
|
394
|
+
"message": (
|
|
395
|
+
"schema-v3 generated trivial/medium runs require completed SURFACE_CLOSE "
|
|
396
|
+
"or its canonical automatic Claude-unavailable skip before VERIFY merge."
|
|
397
|
+
),
|
|
398
|
+
"file": "pipeline.state.json",
|
|
399
|
+
}
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
341
403
|
def source_spec_text(state: dict[str, Any]) -> str | None:
|
|
342
404
|
source = state.get("source") if isinstance(state.get("source"), dict) else {}
|
|
343
405
|
for key in ("spec_path", "criteria_path"):
|
|
@@ -424,6 +486,11 @@ def state_pair_trigger_reasons(
|
|
|
424
486
|
verify_state = state.get("verify") if isinstance(state.get("verify"), dict) else {}
|
|
425
487
|
risk_profile = state.get("risk_profile") if isinstance(state.get("risk_profile"), dict) else {}
|
|
426
488
|
reasons: list[str] = []
|
|
489
|
+
if (
|
|
490
|
+
state_uses_default_pair_contract(state)
|
|
491
|
+
and risk_profile.get("pair_default_enabled") is not False
|
|
492
|
+
):
|
|
493
|
+
reasons.append("pair.default")
|
|
427
494
|
if state.get("mode") == "verify-only":
|
|
428
495
|
reasons.append("mode.verify-only")
|
|
429
496
|
if state.get("pair_verify") is True:
|
|
@@ -455,11 +522,19 @@ def state_pair_trigger_reasons(
|
|
|
455
522
|
return reasons
|
|
456
523
|
|
|
457
524
|
|
|
525
|
+
def outcome_independent_reasons(devlyn: pathlib.Path) -> list[str]:
|
|
526
|
+
return [
|
|
527
|
+
reason
|
|
528
|
+
for reason in state_pair_trigger_reasons(devlyn, {})
|
|
529
|
+
if reason not in ("coverage.failed", "mechanical.warning", "judge.warning")
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
|
|
458
533
|
def pair_trigger_missing_contract_violation(
|
|
459
534
|
devlyn: pathlib.Path,
|
|
460
535
|
source_verdicts: dict[str, str | None],
|
|
461
536
|
) -> dict[str, Any] | None:
|
|
462
|
-
if rank(source_verdicts.get("mechanical")) >= 2
|
|
537
|
+
if rank(source_verdicts.get("mechanical")) >= 2:
|
|
463
538
|
return None
|
|
464
539
|
reasons = state_pair_trigger_reasons(devlyn, source_verdicts)
|
|
465
540
|
if not reasons:
|
|
@@ -558,15 +633,36 @@ def pair_trigger_skip_contract_violation(
|
|
|
558
633
|
),
|
|
559
634
|
"file": "pipeline.state.json",
|
|
560
635
|
}
|
|
561
|
-
if skipped_reason == "primary_judge_blocker"
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
636
|
+
if skipped_reason == "primary_judge_blocker":
|
|
637
|
+
if state_uses_default_pair_contract(state):
|
|
638
|
+
return {
|
|
639
|
+
"id": "verify-pair-trigger-primary-judge-blocker-retired",
|
|
640
|
+
"message": (
|
|
641
|
+
"pair_trigger skipped_reason primary_judge_blocker is archived-v2.0 "
|
|
642
|
+
"state only; schema-v3 runs must dispatch the pair-JUDGE."
|
|
643
|
+
),
|
|
644
|
+
"file": "pipeline.state.json",
|
|
645
|
+
}
|
|
646
|
+
if rank(source_verdicts.get("judge")) < 2:
|
|
647
|
+
return {
|
|
648
|
+
"id": "verify-pair-trigger-primary-judge-blocker-unsupported",
|
|
649
|
+
"message": (
|
|
650
|
+
"pair_trigger skipped_reason primary_judge_blocker requires a "
|
|
651
|
+
"verdict-binding primary JUDGE finding."
|
|
652
|
+
),
|
|
653
|
+
"file": "pipeline.state.json",
|
|
654
|
+
}
|
|
655
|
+
preknown_reasons = outcome_independent_reasons(devlyn)
|
|
656
|
+
if preknown_reasons:
|
|
657
|
+
return {
|
|
658
|
+
"id": "verify-pair-trigger-primary-judge-blocker-preknown",
|
|
659
|
+
"message": (
|
|
660
|
+
"pair_trigger cannot skip the pair-JUDGE for a primary JUDGE blocker "
|
|
661
|
+
"when outcome-independent reasons applied at spawn: "
|
|
662
|
+
+ ", ".join(preknown_reasons)
|
|
663
|
+
),
|
|
664
|
+
"file": "pipeline.state.json",
|
|
665
|
+
}
|
|
570
666
|
return None
|
|
571
667
|
|
|
572
668
|
|
|
@@ -574,7 +670,7 @@ def pair_trigger_reason_completeness_violation(
|
|
|
574
670
|
devlyn: pathlib.Path,
|
|
575
671
|
source_verdicts: dict[str, str | None],
|
|
576
672
|
) -> dict[str, Any] | None:
|
|
577
|
-
if rank(source_verdicts.get("mechanical")) >= 2
|
|
673
|
+
if rank(source_verdicts.get("mechanical")) >= 2:
|
|
578
674
|
return None
|
|
579
675
|
state_path = devlyn / "pipeline.state.json"
|
|
580
676
|
if not state_path.is_file():
|
|
@@ -614,10 +710,15 @@ def pair_trigger_reason_completeness_violation(
|
|
|
614
710
|
}
|
|
615
711
|
|
|
616
712
|
|
|
617
|
-
def pair_blocker(
|
|
713
|
+
def pair_blocker(
|
|
714
|
+
id_: str,
|
|
715
|
+
message: str,
|
|
716
|
+
file_: str | None = None,
|
|
717
|
+
rule_id: str = "verify.pair.emission-contract",
|
|
718
|
+
) -> dict[str, Any]:
|
|
618
719
|
return {
|
|
619
720
|
"id": id_,
|
|
620
|
-
"rule_id":
|
|
721
|
+
"rule_id": rule_id,
|
|
621
722
|
"severity": "CRITICAL",
|
|
622
723
|
"confidence": "high",
|
|
623
724
|
"file": file_,
|
|
@@ -715,6 +816,17 @@ def detect_pair_stdout_contract_violations(
|
|
|
715
816
|
risk_profile_violation["file"],
|
|
716
817
|
)
|
|
717
818
|
]
|
|
819
|
+
state_violation = verify_state_contract_violation(devlyn)
|
|
820
|
+
if state_violation is not None:
|
|
821
|
+
source_verdicts["pair_judge"] = "BLOCKED"
|
|
822
|
+
return [
|
|
823
|
+
pair_blocker(
|
|
824
|
+
state_violation["id"],
|
|
825
|
+
state_violation["message"],
|
|
826
|
+
state_violation["file"],
|
|
827
|
+
state_violation["rule_id"],
|
|
828
|
+
)
|
|
829
|
+
]
|
|
718
830
|
if not required and not pair_trigger_present(devlyn):
|
|
719
831
|
missing_violation = pair_trigger_missing_contract_violation(devlyn, source_verdicts)
|
|
720
832
|
if missing_violation is not None:
|
|
@@ -892,7 +1004,15 @@ def self_test() -> int:
|
|
|
892
1004
|
# the real shape write_state() sees on every VERIFY completion, not
|
|
893
1005
|
# the pre-populated {} the other scenarios below seed for brevity.
|
|
894
1006
|
(devlyn / "pipeline.state.json").write_text(
|
|
895
|
-
json.dumps({
|
|
1007
|
+
json.dumps({
|
|
1008
|
+
"phases": {
|
|
1009
|
+
"verify": {
|
|
1010
|
+
"verdict": None,
|
|
1011
|
+
"sub_verdicts": None,
|
|
1012
|
+
"judge_durations_ms": {"judge": 23, "pair_judge": 31},
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}),
|
|
896
1016
|
encoding="utf-8",
|
|
897
1017
|
)
|
|
898
1018
|
(devlyn / "verify.findings.jsonl").write_text("", encoding="utf-8")
|
|
@@ -904,13 +1024,130 @@ def self_test() -> int:
|
|
|
904
1024
|
assert state["phases"]["verify"]["sub_verdicts"] == {
|
|
905
1025
|
"mechanical": "PASS", "judge": "PASS", "pair_judge": None,
|
|
906
1026
|
}, state
|
|
1027
|
+
assert state["phases"]["verify"]["judge_durations_ms"] == {
|
|
1028
|
+
"judge": 23, "pair_judge": 31,
|
|
1029
|
+
}, state
|
|
1030
|
+
|
|
1031
|
+
# iter-0072 Amendment 3: generated schema-v3 runs mechanically prove
|
|
1032
|
+
# raw-goal persistence and required SURFACE_CLOSE dispatch.
|
|
1033
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1034
|
+
json.dumps({
|
|
1035
|
+
"version": "3.0",
|
|
1036
|
+
"complexity": "large",
|
|
1037
|
+
"source": {"type": "generated"},
|
|
1038
|
+
"phases": {"verify": {"verdict": None, "sub_verdicts": None}},
|
|
1039
|
+
}),
|
|
1040
|
+
encoding="utf-8",
|
|
1041
|
+
)
|
|
1042
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
1043
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
1044
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
1045
|
+
assert summary["source_verdicts"]["pair_judge"] == "BLOCKED", summary
|
|
1046
|
+
assert any(
|
|
1047
|
+
finding.get("rule_id") == "verify.state.goal-persistence-missing"
|
|
1048
|
+
for finding in findings
|
|
1049
|
+
), findings
|
|
1050
|
+
|
|
1051
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1052
|
+
json.dumps({
|
|
1053
|
+
"version": "3.0",
|
|
1054
|
+
"complexity": "large",
|
|
1055
|
+
"source": {"type": "generated", "goal_path": "", "goal_sha256": "A" * 64},
|
|
1056
|
+
}),
|
|
1057
|
+
encoding="utf-8",
|
|
1058
|
+
)
|
|
1059
|
+
violation = verify_state_contract_violation(devlyn)
|
|
1060
|
+
assert violation is not None, violation
|
|
1061
|
+
assert violation["rule_id"] == "verify.state.goal-persistence-missing", violation
|
|
1062
|
+
|
|
1063
|
+
generated_source = {
|
|
1064
|
+
"type": "generated",
|
|
1065
|
+
"goal_path": ".devlyn/goal.raw.txt",
|
|
1066
|
+
"goal_sha256": "a" * 64,
|
|
1067
|
+
}
|
|
1068
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1069
|
+
json.dumps({
|
|
1070
|
+
"version": "3.0",
|
|
1071
|
+
"complexity": "medium",
|
|
1072
|
+
"source": generated_source,
|
|
1073
|
+
"phases": {
|
|
1074
|
+
"surface_close": None,
|
|
1075
|
+
"verify": {"verdict": None, "sub_verdicts": None},
|
|
1076
|
+
},
|
|
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 any(
|
|
1084
|
+
finding.get("rule_id") == "verify.state.surface-close-skipped"
|
|
1085
|
+
for finding in findings
|
|
1086
|
+
), findings
|
|
1087
|
+
|
|
1088
|
+
for surface_entry in (
|
|
1089
|
+
{"verdict": "PASS"},
|
|
1090
|
+
{
|
|
1091
|
+
"verdict": None,
|
|
1092
|
+
"started_at": None,
|
|
1093
|
+
"skipped_reason": "auto_surface_close_claude_unavailable",
|
|
1094
|
+
},
|
|
1095
|
+
):
|
|
1096
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1097
|
+
json.dumps({
|
|
1098
|
+
"version": "3.0",
|
|
1099
|
+
"complexity": "medium",
|
|
1100
|
+
"source": generated_source,
|
|
1101
|
+
"phases": {"surface_close": surface_entry},
|
|
1102
|
+
}),
|
|
1103
|
+
encoding="utf-8",
|
|
1104
|
+
)
|
|
1105
|
+
assert verify_state_contract_violation(devlyn) is None
|
|
1106
|
+
|
|
1107
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1108
|
+
json.dumps({
|
|
1109
|
+
"version": "3.0",
|
|
1110
|
+
"complexity": "medium",
|
|
1111
|
+
"source": generated_source,
|
|
1112
|
+
"phases": {
|
|
1113
|
+
"surface_close": {
|
|
1114
|
+
"verdict": None,
|
|
1115
|
+
"started_at": None,
|
|
1116
|
+
"skipped_reason": "claude-unavailable",
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
}),
|
|
1120
|
+
encoding="utf-8",
|
|
1121
|
+
)
|
|
1122
|
+
assert verify_state_contract_violation(devlyn)["rule_id"] == "verify.state.surface-close-skipped"
|
|
1123
|
+
|
|
1124
|
+
for state_shape in (
|
|
1125
|
+
{
|
|
1126
|
+
"version": "3.0", "complexity": "medium",
|
|
1127
|
+
"source": {"type": "spec"}, "phases": {"surface_close": None},
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
"version": "3.0", "complexity": "large",
|
|
1131
|
+
"source": generated_source, "phases": {"surface_close": None},
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
"version": "2.0", "complexity": "medium",
|
|
1135
|
+
"source": {"type": "generated"}, "phases": {"surface_close": None},
|
|
1136
|
+
},
|
|
1137
|
+
):
|
|
1138
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
1139
|
+
json.dumps(state_shape), encoding="utf-8",
|
|
1140
|
+
)
|
|
1141
|
+
assert verify_state_contract_violation(devlyn) is None
|
|
907
1142
|
|
|
908
1143
|
# 2026-07-04 field bug (iter-0060 G1): an AUTO pair trigger skipped on
|
|
909
1144
|
# OTHER-engine unavailability spawns no second judge — pair_judge must
|
|
910
1145
|
# be recorded null, never a synthesized PASS.
|
|
911
1146
|
(devlyn / "pipeline.state.json").write_text(
|
|
912
1147
|
json.dumps({
|
|
1148
|
+
"version": "3.0",
|
|
913
1149
|
"mode": "spec",
|
|
1150
|
+
"risk_profile": {"pair_default_enabled": True},
|
|
914
1151
|
"phases": {
|
|
915
1152
|
"verify": {
|
|
916
1153
|
"verdict": None,
|
|
@@ -1936,18 +2173,128 @@ def self_test() -> int:
|
|
|
1936
2173
|
for finding in findings
|
|
1937
2174
|
), findings
|
|
1938
2175
|
|
|
2176
|
+
# Self-test: preknown_primary_blocker_requires_pair.
|
|
2177
|
+
(devlyn / "verify.pair.findings.jsonl").unlink(missing_ok=True)
|
|
1939
2178
|
(devlyn / "verify.findings.jsonl").write_text(
|
|
1940
|
-
json.dumps({"id": "
|
|
2179
|
+
json.dumps({"id": "j-preknown", "severity": "HIGH"}) + "\n",
|
|
2180
|
+
encoding="utf-8",
|
|
2181
|
+
)
|
|
2182
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2183
|
+
json.dumps({
|
|
2184
|
+
"version": "2.0",
|
|
2185
|
+
"mode": "spec",
|
|
2186
|
+
"pair_verify": True,
|
|
2187
|
+
"phases": {
|
|
2188
|
+
"verify": {
|
|
2189
|
+
"verdict": "PASS",
|
|
2190
|
+
"sub_verdicts": {},
|
|
2191
|
+
"pair_trigger": {
|
|
2192
|
+
"eligible": False,
|
|
2193
|
+
"reasons": [],
|
|
2194
|
+
"skipped_reason": "primary_judge_blocker",
|
|
2195
|
+
},
|
|
2196
|
+
}
|
|
2197
|
+
},
|
|
2198
|
+
}),
|
|
2199
|
+
encoding="utf-8",
|
|
2200
|
+
)
|
|
2201
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2202
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2203
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2204
|
+
assert any(
|
|
2205
|
+
finding.get("id") == "verify-pair-trigger-primary-judge-blocker-preknown"
|
|
2206
|
+
and "mode.pair-verify" in str(finding.get("message"))
|
|
2207
|
+
for finding in findings
|
|
2208
|
+
), findings
|
|
2209
|
+
|
|
2210
|
+
# Self-test: schema-v3 default pair reason is required before merge.
|
|
2211
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2212
|
+
json.dumps({
|
|
2213
|
+
"version": "3.0",
|
|
2214
|
+
"mode": "spec",
|
|
2215
|
+
"pair_verify": True,
|
|
2216
|
+
"phases": {
|
|
2217
|
+
"verify": {
|
|
2218
|
+
"verdict": "PASS",
|
|
2219
|
+
"sub_verdicts": {},
|
|
2220
|
+
"pair_trigger": {
|
|
2221
|
+
"eligible": True,
|
|
2222
|
+
"reasons": ["mode.pair-verify"],
|
|
2223
|
+
"skipped_reason": None,
|
|
2224
|
+
},
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
}),
|
|
2228
|
+
encoding="utf-8",
|
|
2229
|
+
)
|
|
2230
|
+
(devlyn / "verify.pair.findings.jsonl").write_text(
|
|
2231
|
+
json.dumps({"id": "p-preknown", "severity": "LOW"}) + "\n",
|
|
2232
|
+
encoding="utf-8",
|
|
2233
|
+
)
|
|
2234
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2235
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2236
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2237
|
+
assert any(
|
|
2238
|
+
finding.get("id") == "verify-pair-trigger-reasons-incomplete"
|
|
2239
|
+
and "pair.default" in str(finding.get("message"))
|
|
2240
|
+
for finding in findings
|
|
2241
|
+
), findings
|
|
2242
|
+
state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
2243
|
+
state["phases"]["verify"]["pair_trigger"]["reasons"].insert(0, "pair.default")
|
|
2244
|
+
(devlyn / "pipeline.state.json").write_text(json.dumps(state), encoding="utf-8")
|
|
2245
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2246
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2247
|
+
assert summary["verdict"] == "NEEDS_WORK", summary
|
|
2248
|
+
assert summary["source_verdicts"]["judge"] == "NEEDS_WORK", summary
|
|
2249
|
+
assert summary["source_verdicts"]["pair_judge"] == "PASS_WITH_ISSUES", summary
|
|
2250
|
+
assert '"id":"p-preknown"' in (
|
|
2251
|
+
devlyn / "verify-merged.findings.jsonl"
|
|
2252
|
+
).read_text(encoding="utf-8"), findings
|
|
2253
|
+
|
|
2254
|
+
# Self-test: archived-v2 sequential primary blocker remains legal.
|
|
2255
|
+
(devlyn / "verify.pair.findings.jsonl").unlink()
|
|
2256
|
+
(devlyn / "pipeline.state.json").write_text(
|
|
2257
|
+
json.dumps({
|
|
2258
|
+
"version": "2.0",
|
|
2259
|
+
"mode": "spec",
|
|
2260
|
+
"phases": {
|
|
2261
|
+
"verify": {
|
|
2262
|
+
"verdict": "PASS",
|
|
2263
|
+
"sub_verdicts": {},
|
|
2264
|
+
"pair_trigger": {
|
|
2265
|
+
"eligible": False,
|
|
2266
|
+
"reasons": [],
|
|
2267
|
+
"skipped_reason": "primary_judge_blocker",
|
|
2268
|
+
},
|
|
2269
|
+
}
|
|
2270
|
+
},
|
|
2271
|
+
}),
|
|
1941
2272
|
encoding="utf-8",
|
|
1942
2273
|
)
|
|
1943
2274
|
findings, source_verdicts = read_findings(devlyn)
|
|
1944
2275
|
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
1945
2276
|
assert summary["verdict"] == "NEEDS_WORK", summary
|
|
2277
|
+
assert summary["source_verdicts"]["pair_judge"] is None, summary
|
|
1946
2278
|
assert not any(
|
|
1947
|
-
finding.get("id")
|
|
2279
|
+
finding.get("id") in {
|
|
2280
|
+
"verify-pair-trigger-primary-judge-blocker-unsupported",
|
|
2281
|
+
"verify-pair-trigger-primary-judge-blocker-preknown",
|
|
2282
|
+
}
|
|
2283
|
+
for finding in findings
|
|
2284
|
+
), findings
|
|
2285
|
+
# Replay the same state as schema v3 and require the retired-skip blocker.
|
|
2286
|
+
state = loads_strict_json((devlyn / "pipeline.state.json").read_text(encoding="utf-8"))
|
|
2287
|
+
state["version"] = "3.0"
|
|
2288
|
+
(devlyn / "pipeline.state.json").write_text(json.dumps(state), encoding="utf-8")
|
|
2289
|
+
findings, source_verdicts = read_findings(devlyn)
|
|
2290
|
+
summary = write_outputs(devlyn, findings, source_verdicts)
|
|
2291
|
+
assert summary["verdict"] == "BLOCKED", summary
|
|
2292
|
+
assert any(
|
|
2293
|
+
finding.get("id") == "verify-pair-trigger-primary-judge-blocker-retired"
|
|
1948
2294
|
for finding in findings
|
|
1949
2295
|
), findings
|
|
1950
2296
|
(devlyn / "verify.findings.jsonl").write_text("", encoding="utf-8")
|
|
2297
|
+
(devlyn / "verify.pair.findings.jsonl").write_text("", encoding="utf-8")
|
|
1951
2298
|
|
|
1952
2299
|
(devlyn / "pipeline.state.json").write_text(
|
|
1953
2300
|
json.dumps({
|
|
@@ -25,7 +25,7 @@ DEVLYN_SHARED_DIR="$(cd "$DEVLYN_SKILL_DIR/../_shared" && pwd)"
|
|
|
25
25
|
## No args — status + how to choose
|
|
26
26
|
|
|
27
27
|
1. Read `cwd/.devlyn/engines.json`. Absent → report "no pins — auto-detection active".
|
|
28
|
-
2. Run `bash "$DEVLYN_SHARED_DIR/engine-doctor.sh"` — read-only; never installs anything or writes `.devlyn/engines.json`. It detects, per target
|
|
28
|
+
2. Run `bash "$DEVLYN_SHARED_DIR/engine-doctor.sh"` — read-only; never installs anything or writes `.devlyn/engines.json`. It detects, per target in the doctor's catalog, whether its binary is present, whether `_shared/adapters/<name>.md` exists, and whether that makes it `pin_eligible` today. Its trailing line is either a pair-judge-diversity confirmation or a recommendation to add a second adapter-valid engine (`autoresearch/iterations/0045-model-arm-drift.md`: different model tiers hit different failure-mode blind spots, so VERIFY pair-judge/risk-probes need ≥2 to ever fire).
|
|
29
29
|
3. Print the role table. Executor's "Available engines" = rows where `pin_eligible: yes` and `role` includes `executor` (i.e. not `judge-only`). Pair judge's "Available engines" = rows where `pin_eligible: yes` and `role` includes `judge` (`executor+judge` or `judge-only`):
|
|
30
30
|
|
|
31
31
|
```
|
|
@@ -9,6 +9,17 @@ Utility front-end for the intent-queue contract in the project instructions ("In
|
|
|
9
9
|
$ARGUMENTS
|
|
10
10
|
</args>
|
|
11
11
|
|
|
12
|
+
For `drain`, resolve the installed shared scripts before the first item:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
DEVLYN_SKILL_DIR="${CLAUDE_SKILL_DIR:-__DEVLYN_SKILL_DIR__}"
|
|
16
|
+
if [ "$DEVLYN_SKILL_DIR" = "__DEVLYN_SKILL_DIR__" ] || [ ! -d "$DEVLYN_SKILL_DIR/../_shared" ]; then
|
|
17
|
+
echo "BLOCKED:shared-dir-unresolved: $DEVLYN_SKILL_DIR/../_shared" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
DEVLYN_SHARED_DIR="$(cd "$DEVLYN_SKILL_DIR/../_shared" && pwd)"
|
|
21
|
+
```
|
|
22
|
+
|
|
12
23
|
## No args — status
|
|
13
24
|
|
|
14
25
|
Read `docs/specs/queue.md` (absent → report "queue empty — nothing staged" and how to add). Print pending `[ ]`, done `[x]`, and blocked `[F]` counts, the next item up, and one usage line per subcommand.
|
|
@@ -19,6 +30,7 @@ Read `docs/specs/queue.md` (absent → report "queue empty — nothing staged" a
|
|
|
19
30
|
- `drain` — serial drain per the project-instructions contract. For each pending item, in order:
|
|
20
31
|
1. Spec it if unspecced (the queue entry is the user's go-ahead). Unattended assumptions may only take scope-narrowing, reversible, non-user-visible defaults; material ambiguity (user-visible behavior, data/state semantics, new files/scripts/flags, implementation surface) → mark `[F] needs-review: <question>` and continue to the next item.
|
|
21
32
|
2. Run `/devlyn:resolve --spec <path>` hands-free.
|
|
33
|
+
After every resolve invocation, run `python3 "$DEVLYN_SHARED_DIR/terminal-claim-check.py" .`; exit 79 marks `[F] FAILED-INCOMPLETE` from the predicate, never from the session self-report.
|
|
22
34
|
3. Outer loop on the terminal verdict: PASS → mark `[x]`. Findings-backed verdicts (NEEDS_WORK, verify/build-gate exhaustion) → amend the spec (recorded in the spec file), re-run — at most 3 outer iterations. Infrastructure / invalid-input / engine-availability / implement-empty BLOCKED verdicts are not spec-amendable → mark `[F] <verdict>` immediately.
|
|
23
35
|
4. A blocked item never halts the queue; continue.
|
|
24
36
|
5. When the queue is drained (or the session must stop), emit the drain report: per-item verdict, every logged assumption, and the commit range produced.
|