agent-bios 0.12.1 → 0.13.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.
|
@@ -161,10 +161,19 @@ Ranked by how much the channel refuses for you:
|
|
|
161
161
|
Stated honestly: on a prose route this is steering, not control — the fold is
|
|
162
162
|
performed by an agent following this guide, and nothing structural refuses a
|
|
163
163
|
class-less row for it. That is the known weaker result of a request-only rule.
|
|
164
|
-
3.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
3. **The launcher's criterion discipline** (agent-launch): a preset declaring
|
|
165
|
+
`criterion = true` renders one core-owned discipline clause into every review
|
|
166
|
+
method row — the criterion's content never enters the per-launch config; it rides
|
|
167
|
+
the packet as a `ReviewCriterion/v1:` record line beside the prose above.
|
|
168
|
+
`--compile-criterion` refuses a document failing the schema's decidable subset and
|
|
169
|
+
emits the canonical findings schema; where the host CLI's structured-output flag
|
|
170
|
+
probes present (`--check-schema-flag`), the dispatch passes that schema, and
|
|
171
|
+
receipt emission under `REVIEW_CRITERION_SCHEMA` refuses a class-less or
|
|
172
|
+
out-of-enum result — no receipt, and an unproven dispatch is already in the stop
|
|
173
|
+
class. `--verify-receipts --packet` recompiles the packet's record and refuses a
|
|
174
|
+
receipt whose schema digest was compiled from any other criterion. A route whose
|
|
175
|
+
flag probes absent runs rank 2 and is disclosed as prose discipline — never
|
|
176
|
+
credited as schema-enforced.
|
|
168
177
|
|
|
169
178
|
## Golden lifecycle
|
|
170
179
|
|
|
@@ -161,10 +161,19 @@ Ranked by how much the channel refuses for you:
|
|
|
161
161
|
Stated honestly: on a prose route this is steering, not control — the fold is
|
|
162
162
|
performed by an agent following this guide, and nothing structural refuses a
|
|
163
163
|
class-less row for it. That is the known weaker result of a request-only rule.
|
|
164
|
-
3.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
3. **The launcher's criterion discipline** (agent-launch): a preset declaring
|
|
165
|
+
`criterion = true` renders one core-owned discipline clause into every review
|
|
166
|
+
method row — the criterion's content never enters the per-launch config; it rides
|
|
167
|
+
the packet as a `ReviewCriterion/v1:` record line beside the prose above.
|
|
168
|
+
`--compile-criterion` refuses a document failing the schema's decidable subset and
|
|
169
|
+
emits the canonical findings schema; where the host CLI's structured-output flag
|
|
170
|
+
probes present (`--check-schema-flag`), the dispatch passes that schema, and
|
|
171
|
+
receipt emission under `REVIEW_CRITERION_SCHEMA` refuses a class-less or
|
|
172
|
+
out-of-enum result — no receipt, and an unproven dispatch is already in the stop
|
|
173
|
+
class. `--verify-receipts --packet` recompiles the packet's record and refuses a
|
|
174
|
+
receipt whose schema digest was compiled from any other criterion. A route whose
|
|
175
|
+
flag probes absent runs rank 2 and is disclosed as prose discipline — never
|
|
176
|
+
credited as schema-enforced.
|
|
168
177
|
|
|
169
178
|
## Golden lifecycle
|
|
170
179
|
|
package/launch/agent-launch.py
CHANGED
|
@@ -937,6 +937,19 @@ SEVERITY_NAME_RULE = (
|
|
|
937
937
|
# forbidding the labels that trigger it, which is why `<`, `-` and `=` stay registrable.
|
|
938
938
|
SEVERITY_ARROW = " => "
|
|
939
939
|
SEVERITY_CLAUSE_MARKER = "; severities "
|
|
940
|
+
# The criterion discipline clause. A CONSTANT, not a slot: the criterion's content is
|
|
941
|
+
# per-review and lives in the packet (which packet_sha256 binds), while the contract is
|
|
942
|
+
# per-launch and golden-pinned — so what renders here is only the discipline, identical
|
|
943
|
+
# for every method and every criterion. Placed BEFORE the severity clause in the row,
|
|
944
|
+
# because the severity clause is parsed as everything after its marker.
|
|
945
|
+
CRITERION_CLAUSE_MARKER = "; criterion "
|
|
946
|
+
CRITERION_CLAUSE = (
|
|
947
|
+
f"{CRITERION_CLAUSE_MARKER}declared in the packet: compile it with "
|
|
948
|
+
f"--compile-criterion, prefer a dispatch whose host schema flag probed present "
|
|
949
|
+
f"(--check-schema-flag), emit receipts under REVIEW_CRITERION_SCHEMA so emission "
|
|
950
|
+
f"refuses unclassified findings, and fold returned rows against the declared enum "
|
|
951
|
+
f"without guessing a class"
|
|
952
|
+
)
|
|
940
953
|
# The panel that carries corpus/registration text, and the scrolling body that holds it.
|
|
941
954
|
# Named once so the screen, its CSS, its key bindings and the check that drives them cannot
|
|
942
955
|
# drift apart.
|
|
@@ -1127,6 +1140,17 @@ class ReviewMechanism:
|
|
|
1127
1140
|
def parse_review_method(method_id: str, raw: Any, context: str) -> ReviewMethod:
|
|
1128
1141
|
if not isinstance(raw, dict):
|
|
1129
1142
|
raise LaunchError(f"{context} must be a table")
|
|
1143
|
+
# The method ID is the one authored value core PREFIXES to the rendered row, so the
|
|
1144
|
+
# formatted-body marker checks never see it — a registry key carrying a clause
|
|
1145
|
+
# marker forged a second decodable clause beside core's (criterion round, #1; the
|
|
1146
|
+
# same door round 20 #5 opened for the plan marker, closed there on the whole row).
|
|
1147
|
+
for marker in (SEVERITY_CLAUSE_MARKER, CONTROLS_CLAUSE_MARKER, CRITERION_CLAUSE_MARKER):
|
|
1148
|
+
if marker in method_id:
|
|
1149
|
+
raise LaunchError(
|
|
1150
|
+
f"{context}: the method id contains {marker!r}, which core owns for a "
|
|
1151
|
+
f"rendered clause; an id carrying it would forge a second clause in "
|
|
1152
|
+
f"every row it prefixes"
|
|
1153
|
+
)
|
|
1130
1154
|
unknown = sorted(set(raw) - METHOD_KEYS)
|
|
1131
1155
|
if unknown:
|
|
1132
1156
|
raise LaunchError(f"{context}: unknown method key(s): {', '.join(unknown)}")
|
|
@@ -1307,6 +1331,12 @@ def validate_instruction_slots(template: str, context: str) -> set:
|
|
|
1307
1331
|
f"{context}: contains {CONTROLS_CLAUSE_MARKER!r}, which core appends from the "
|
|
1308
1332
|
f"method's declared controls; a second clause would contradict it"
|
|
1309
1333
|
)
|
|
1334
|
+
if CRITERION_CLAUSE_MARKER in template:
|
|
1335
|
+
raise LaunchError(
|
|
1336
|
+
f"{context}: contains {CRITERION_CLAUSE_MARKER!r}, which core appends when the "
|
|
1337
|
+
f"plan declares a criterion-disciplined review; a second clause would "
|
|
1338
|
+
f"contradict the plan's toggle"
|
|
1339
|
+
)
|
|
1310
1340
|
refuse_plan_marker(template, context)
|
|
1311
1341
|
return used
|
|
1312
1342
|
|
|
@@ -1513,9 +1543,16 @@ def derive_review_mechanism(
|
|
|
1513
1543
|
)
|
|
1514
1544
|
|
|
1515
1545
|
|
|
1516
|
-
def render_review_method(
|
|
1546
|
+
def render_review_method(
|
|
1547
|
+
method: ReviewMethod, mechanism: ReviewMechanism, criterion: bool = False
|
|
1548
|
+
) -> str:
|
|
1517
1549
|
"""The generic renderer. Every method — shipped or third-party — reaches the
|
|
1518
|
-
contract through this one function, with the exact seat already resolved.
|
|
1550
|
+
contract through this one function, with the exact seat already resolved.
|
|
1551
|
+
|
|
1552
|
+
`criterion` is the plan's toggle, threaded rather than read from anywhere global:
|
|
1553
|
+
when the preset declares a criterion-disciplined review, core appends the one
|
|
1554
|
+
discipline clause to every row, method-blind, the way the severity translation is
|
|
1555
|
+
appended — an author never writes it and a slot never carries it."""
|
|
1519
1556
|
binding = mechanism.binding
|
|
1520
1557
|
slots = {
|
|
1521
1558
|
"command": mechanism.command or "",
|
|
@@ -1561,7 +1598,8 @@ def render_review_method(method: ReviewMethod, mechanism: ReviewMechanism) -> st
|
|
|
1561
1598
|
# them as literals, and a perspective label carrying `; controls trials=1` rendered a
|
|
1562
1599
|
# second controls clause beside core's (round 19, #9). Every substitution passes here.
|
|
1563
1600
|
for marker, owner in ((CONTROLS_CLAUSE_MARKER, "the method's declared controls"),
|
|
1564
|
-
(REVIEW_PLAN_MARKER, "the contract's canonical review record")
|
|
1601
|
+
(REVIEW_PLAN_MARKER, "the contract's canonical review record"),
|
|
1602
|
+
(CRITERION_CLAUSE_MARKER, "the plan's criterion discipline")):
|
|
1565
1603
|
if marker in body:
|
|
1566
1604
|
raise LaunchError(
|
|
1567
1605
|
f"review method {method.method_id!r} renders {marker!r} in its body, which "
|
|
@@ -1570,7 +1608,7 @@ def render_review_method(method: ReviewMethod, mechanism: ReviewMechanism) -> st
|
|
|
1570
1608
|
rendered = (
|
|
1571
1609
|
f"{method.method_id}: {body} "
|
|
1572
1610
|
f"[{mechanism.shape}; {binding.model}/{binding.effort}"
|
|
1573
|
-
f"{controls_clause(method)}{severity_translation(method)}]"
|
|
1611
|
+
f"{controls_clause(method)}{criterion_clause(criterion)}{severity_translation(method)}]"
|
|
1574
1612
|
)
|
|
1575
1613
|
# The assembled ROW, after the body. The body check above catches a SLOT value and says
|
|
1576
1614
|
# so; what it cannot see is the part core itself prefixes — the METHOD ID, a user-chosen
|
|
@@ -1623,6 +1661,16 @@ def severity_vocabulary(method: ReviewMethod) -> str:
|
|
|
1623
1661
|
return ", ".join(method.severity_emits)
|
|
1624
1662
|
|
|
1625
1663
|
|
|
1664
|
+
def criterion_clause(active: bool) -> str:
|
|
1665
|
+
"""The discipline clause, or "" when the plan declares no criterion.
|
|
1666
|
+
|
|
1667
|
+
Core-owned and constant for the reason severity_translation is core-appended: a
|
|
1668
|
+
third-party method carries it without its author remembering to, and the gate can
|
|
1669
|
+
subtract the exact substring. Inside the seat bracket, BEFORE the severity clause,
|
|
1670
|
+
which is parsed as everything after its own marker."""
|
|
1671
|
+
return CRITERION_CLAUSE if active else ""
|
|
1672
|
+
|
|
1673
|
+
|
|
1626
1674
|
def severity_translation(method: ReviewMethod) -> str:
|
|
1627
1675
|
"""The clause telling the reader how to move this reviewer's severities onto the
|
|
1628
1676
|
canonical ladder, or "" when it reports on the ladder already.
|
|
@@ -1743,7 +1791,8 @@ def independence_grade(reviewer: ReviewBinding, main: ReviewBinding) -> str:
|
|
|
1743
1791
|
|
|
1744
1792
|
|
|
1745
1793
|
def _resolve_one(
|
|
1746
|
-
method: ReviewMethod, binding: ReviewBinding, main: ReviewBinding,
|
|
1794
|
+
method: ReviewMethod, binding: ReviewBinding, main: ReviewBinding,
|
|
1795
|
+
config: dict[str, Any], criterion: bool = False,
|
|
1747
1796
|
) -> ReviewMethodReport:
|
|
1748
1797
|
"""One method against the main seat. An optional method is NEVER silently rebound
|
|
1749
1798
|
to a different seat: if its mechanism cannot be derived it is dropped, and the
|
|
@@ -1823,7 +1872,7 @@ def _resolve_one(
|
|
|
1823
1872
|
return ReviewMethodReport(
|
|
1824
1873
|
method.method_id, status, grade, "; ".join(notes),
|
|
1825
1874
|
binding.model, binding.effort, binding.provider, mechanism.adapter,
|
|
1826
|
-
render_review_method(method, mechanism), method_controls(method),
|
|
1875
|
+
render_review_method(method, mechanism, criterion), method_controls(method),
|
|
1827
1876
|
tuple(mechanism.evidence),
|
|
1828
1877
|
)
|
|
1829
1878
|
|
|
@@ -1844,7 +1893,7 @@ def best_review_grade(rows) -> str:
|
|
|
1844
1893
|
|
|
1845
1894
|
def resolve_composable_review(
|
|
1846
1895
|
review: ReviewPlan, main: ReviewBinding, config: dict[str, Any],
|
|
1847
|
-
methods: dict[str, ReviewMethod],
|
|
1896
|
+
methods: dict[str, ReviewMethod], criterion: bool = False,
|
|
1848
1897
|
) -> ReviewReport:
|
|
1849
1898
|
"""Base panel plus the method map, each graded against the main seat.
|
|
1850
1899
|
|
|
@@ -1858,7 +1907,7 @@ def resolve_composable_review(
|
|
|
1858
1907
|
if panel is None:
|
|
1859
1908
|
raise LaunchError(f"the {PANEL_METHOD} method is required and is not registered")
|
|
1860
1909
|
base_binding = review.base_binding or main
|
|
1861
|
-
base = _resolve_one(panel, base_binding, main, config)
|
|
1910
|
+
base = _resolve_one(panel, base_binding, main, config, criterion)
|
|
1862
1911
|
# Authored-but-unsatisfiable is a fallback too, not just an undeliverable mechanism.
|
|
1863
1912
|
# Without this the §1 rule below was unreachable for the commonest case — a preset
|
|
1864
1913
|
# naming a provider the profile has no host for never got here at all, because the
|
|
@@ -1866,7 +1915,7 @@ def resolve_composable_review(
|
|
|
1866
1915
|
fell_back = review.base_unseated is not None
|
|
1867
1916
|
if base.status == STATUS_DROPPED and review.base_binding is not None:
|
|
1868
1917
|
# Try the same floor on the main's own seat before giving up on review.
|
|
1869
|
-
base = _resolve_one(panel, main, main, config)
|
|
1918
|
+
base = _resolve_one(panel, main, main, config, criterion)
|
|
1870
1919
|
fell_back = True
|
|
1871
1920
|
if fell_back and base.status != STATUS_DROPPED:
|
|
1872
1921
|
detail = (
|
|
@@ -1905,7 +1954,7 @@ def resolve_composable_review(
|
|
|
1905
1954
|
if unseated
|
|
1906
1955
|
else "no binding: the method names no verifier seat"))
|
|
1907
1956
|
continue
|
|
1908
|
-
rows.append(_resolve_one(method, binding, main, config))
|
|
1957
|
+
rows.append(_resolve_one(method, binding, main, config, criterion))
|
|
1909
1958
|
return ReviewReport(base, tuple(rows), best_review_grade((base, *rows)))
|
|
1910
1959
|
|
|
1911
1960
|
|
|
@@ -2375,6 +2424,12 @@ RECEIPT_KEYS = frozenset({
|
|
|
2375
2424
|
"provider", "model", "effort", "exit_status", "passes", "ordering_seed", "swap_group",
|
|
2376
2425
|
# What the tool reported back, keyed by the field names its offer declared.
|
|
2377
2426
|
"evidence",
|
|
2427
|
+
# The digest of the compiled criterion schema the emission validated this result
|
|
2428
|
+
# against — present ONLY on a schema-route dispatch (REVIEW_CRITERION_SCHEMA was
|
|
2429
|
+
# set), read by --verify-receipts, which recompiles the packet's criterion and
|
|
2430
|
+
# refuses a mismatch. Absent on a prose-route dispatch, which is disclosed, never
|
|
2431
|
+
# refused: host schema capability is per-machine and not retroactively provable.
|
|
2432
|
+
"criterion_schema_sha256",
|
|
2378
2433
|
})
|
|
2379
2434
|
# The BUNDLE's grammar, closed for the reason the receipt's is. The reader took its four
|
|
2380
2435
|
# anchors and ignored everything else, so a key it does not know rode along unjudged and
|
|
@@ -2460,6 +2515,16 @@ def _receipt_structure_reason(receipt: Any) -> str:
|
|
|
2460
2515
|
f"records exit_status as {type(status).__name__}, not an integer — a boolean is "
|
|
2461
2516
|
f"not an exit status, and JSON false is not exit 0"
|
|
2462
2517
|
)
|
|
2518
|
+
# Form here, in the door the adjudicator AND the fold share, so a folded pair of
|
|
2519
|
+
# matching non-hashes cannot agree its way past the per-receipt check.
|
|
2520
|
+
if "criterion_schema_sha256" in receipt and not is_sha256_hex(
|
|
2521
|
+
receipt["criterion_schema_sha256"]
|
|
2522
|
+
):
|
|
2523
|
+
return (
|
|
2524
|
+
f"records criterion_schema_sha256={receipt['criterion_schema_sha256']!r}, "
|
|
2525
|
+
f"which is not a SHA-256 hash — nothing was hashed, so no schema validated "
|
|
2526
|
+
f"this result"
|
|
2527
|
+
)
|
|
2463
2528
|
return ""
|
|
2464
2529
|
|
|
2465
2530
|
|
|
@@ -2496,7 +2561,7 @@ def _raw_receipt_reason(receipt: Any) -> str:
|
|
|
2496
2561
|
|
|
2497
2562
|
def _receipt_reason(
|
|
2498
2563
|
receipt: Any, row: "ReviewMethodReport | None", bundle: dict, seen: set,
|
|
2499
|
-
controls: dict, required_evidence: tuple = (),
|
|
2564
|
+
controls: dict, required_evidence: tuple = (), criterion_digest: "str | None" = None,
|
|
2500
2565
|
) -> str:
|
|
2501
2566
|
"""Why this receipt cannot be credited, or "" when it can.
|
|
2502
2567
|
|
|
@@ -2545,6 +2610,22 @@ def _receipt_reason(
|
|
|
2545
2610
|
)
|
|
2546
2611
|
if packet != bundle.get("packet_sha256"):
|
|
2547
2612
|
return "hashes a different packet than the one the plan declared"
|
|
2613
|
+
# Only when the packet's criterion is in hand: the digest is recompiled from the
|
|
2614
|
+
# packet's own record, so a mismatch means the schema the host was handed was
|
|
2615
|
+
# compiled from some OTHER criterion — a stricter or looser enum than the one this
|
|
2616
|
+
# packet declares. With no packet supplied there is nothing to recompile, and the
|
|
2617
|
+
# rendering says the claim is unbound rather than treating it as proven.
|
|
2618
|
+
if (
|
|
2619
|
+
criterion_digest is not None
|
|
2620
|
+
and "criterion_schema_sha256" in receipt
|
|
2621
|
+
and receipt["criterion_schema_sha256"] != criterion_digest
|
|
2622
|
+
):
|
|
2623
|
+
return (
|
|
2624
|
+
f"was validated against a schema hashing "
|
|
2625
|
+
f"{receipt['criterion_schema_sha256']}, and recompiling this packet's "
|
|
2626
|
+
f"declared criterion yields {criterion_digest} — the schema enforced was "
|
|
2627
|
+
f"not compiled from the criterion this packet declares"
|
|
2628
|
+
)
|
|
2548
2629
|
for field in ("provider", "model", "effort"):
|
|
2549
2630
|
if receipt.get(field) != getattr(row, field):
|
|
2550
2631
|
return (
|
|
@@ -2639,7 +2720,7 @@ def _receipt_reason(
|
|
|
2639
2720
|
|
|
2640
2721
|
def verify_review_receipts(
|
|
2641
2722
|
report: ReviewReport, bundle: Any, required_controls: dict[str, dict],
|
|
2642
|
-
required_evidence: dict[str, tuple],
|
|
2723
|
+
required_evidence: dict[str, tuple], criterion_digest: "str | None" = None,
|
|
2643
2724
|
) -> tuple[ReviewReport, tuple[ReceiptVerdict, ...]]:
|
|
2644
2725
|
"""Adjudicate a review against its receipts.
|
|
2645
2726
|
|
|
@@ -2860,6 +2941,7 @@ def verify_review_receipts(
|
|
|
2860
2941
|
# The row's own snapshot, never the caller's recomputation — see the drift
|
|
2861
2942
|
# refusal above.
|
|
2862
2943
|
row.evidence if row else (),
|
|
2944
|
+
criterion_digest,
|
|
2863
2945
|
)
|
|
2864
2946
|
if not reason and isinstance(receipt.get("dispatch_id"), str):
|
|
2865
2947
|
seen.add(receipt["dispatch_id"])
|
|
@@ -2908,7 +2990,8 @@ def verify_review_receipts(
|
|
|
2908
2990
|
|
|
2909
2991
|
|
|
2910
2992
|
def render_receipt_verdicts(
|
|
2911
|
-
report: ReviewReport, verdicts: tuple[ReceiptVerdict, ...],
|
|
2993
|
+
report: ReviewReport, verdicts: tuple[ReceiptVerdict, ...],
|
|
2994
|
+
packet_bound: bool = False, criterion_note: str = "",
|
|
2912
2995
|
) -> str:
|
|
2913
2996
|
# Coverage is printed as a count, not only as a word: `partial` tells a reader the set
|
|
2914
2997
|
# was incomplete and `1/3` tells them how incomplete, which is the difference between
|
|
@@ -2952,6 +3035,10 @@ def render_receipt_verdicts(
|
|
|
2952
3035
|
" grade_derivation=claimed — the plan does not serialize the main seat, so a row's "
|
|
2953
3036
|
"independence grade is the launch's own claim and is not recomputed here"
|
|
2954
3037
|
)
|
|
3038
|
+
if criterion_note:
|
|
3039
|
+
# Empty on every non-criterion adjudication, so the rendering is byte-identical
|
|
3040
|
+
# to today's whenever the plan declared no discipline.
|
|
3041
|
+
lines.append(criterion_note)
|
|
2955
3042
|
for verdict in verdicts:
|
|
2956
3043
|
state = "ACHIEVED" if verdict.accepted else "PROPOSED"
|
|
2957
3044
|
lines.append(
|
|
@@ -2962,6 +3049,426 @@ def render_receipt_verdicts(
|
|
|
2962
3049
|
return "\n".join(lines)
|
|
2963
3050
|
|
|
2964
3051
|
|
|
3052
|
+
# ── criterion discipline (defect-criterion stage 6) ─────────────────────────
|
|
3053
|
+
# The criterion is PER-REVIEW: its content rides the packet, never the contract or the
|
|
3054
|
+
# config. What core owns is the deterministic subset — the document grammar, the
|
|
3055
|
+
# compiled findings schema, the packet record line, and the findings check that receipt
|
|
3056
|
+
# emission runs. Whether a golden is persuasive or a stop condition genuinely reachable
|
|
3057
|
+
# stays semantic and is never adjudicated here.
|
|
3058
|
+
CRITERION_RECORD_SCHEMA = "ReviewCriterion/v1"
|
|
3059
|
+
CRITERION_RECORD_MARKER = f"{CRITERION_RECORD_SCHEMA}: "
|
|
3060
|
+
# The guide's eight fields plus the two the machine subset needs named apart: the enum
|
|
3061
|
+
# and its one stop-relevant member. Closed, and every key required — the guide's own
|
|
3062
|
+
# rule is that an empty cell makes a hunch, not a criterion.
|
|
3063
|
+
CRITERION_KEYS = frozenset({
|
|
3064
|
+
"name", "observer", "defect", "classes", "stop_class", "evidence",
|
|
3065
|
+
"non_defects", "stop_condition", "misclassification_cost", "goldens",
|
|
3066
|
+
})
|
|
3067
|
+
CRITERION_GOLDEN_KEYS = frozenset({"kind", "case", "why", "date", "provenance"})
|
|
3068
|
+
CRITERION_GOLDEN_KINDS = ("positive", "negative", "boundary")
|
|
3069
|
+
# The guide's admission bar: ≥2 positive, ≥2 negative, ≥1 boundary.
|
|
3070
|
+
CRITERION_GOLDEN_MINIMUMS = {"positive": 2, "negative": 2, "boundary": 1}
|
|
3071
|
+
CRITERION_PROVENANCE = frozenset({"measured", "constructed"})
|
|
3072
|
+
|
|
3073
|
+
|
|
3074
|
+
def _utf8_encodable(value: str) -> bool:
|
|
3075
|
+
"""False for a string no UTF-8 artifact can carry — a JSON escape can smuggle a
|
|
3076
|
+
lone surrogate through json.loads, and every encode after it crashes."""
|
|
3077
|
+
try:
|
|
3078
|
+
value.encode("utf-8")
|
|
3079
|
+
except UnicodeEncodeError:
|
|
3080
|
+
return False
|
|
3081
|
+
return True
|
|
3082
|
+
|
|
3083
|
+
|
|
3084
|
+
def validate_criterion(document: Any, context: str) -> None:
|
|
3085
|
+
"""Refuse a criterion document that fails the DECIDABLE subset of the guide schema,
|
|
3086
|
+
by field name. Nothing downstream receives a half-criterion: there is no fallback
|
|
3087
|
+
to "no criterion" and no default for any cell."""
|
|
3088
|
+
if not isinstance(document, dict):
|
|
3089
|
+
raise LaunchError(f"{context} must be a JSON object")
|
|
3090
|
+
# The whole document must be UTF-8-encodable BEFORE any field check: JSON escapes
|
|
3091
|
+
# admit lone surrogates that json.loads accepts and every later encode —
|
|
3092
|
+
# the canonical record line, the compiled schema bytes — crashes on with a
|
|
3093
|
+
# traceback instead of a named refusal (criterion round 2, #0).
|
|
3094
|
+
try:
|
|
3095
|
+
json.dumps(document, ensure_ascii=False).encode("utf-8")
|
|
3096
|
+
except UnicodeEncodeError as exc:
|
|
3097
|
+
raise LaunchError(
|
|
3098
|
+
f"{context} is not UTF-8-encodable ({exc}); a lone surrogate cannot ride "
|
|
3099
|
+
f"the record line or the compiled schema"
|
|
3100
|
+
) from exc
|
|
3101
|
+
missing = sorted(CRITERION_KEYS - set(document))
|
|
3102
|
+
if missing:
|
|
3103
|
+
raise LaunchError(
|
|
3104
|
+
f"{context} is missing {', '.join(missing)}; an empty cell makes a hunch, "
|
|
3105
|
+
f"not a criterion"
|
|
3106
|
+
)
|
|
3107
|
+
unknown = sorted(set(document) - CRITERION_KEYS)
|
|
3108
|
+
if unknown:
|
|
3109
|
+
raise LaunchError(f"{context} carries unknown key(s): {', '.join(unknown)}")
|
|
3110
|
+
for field in ("name", "observer", "defect", "evidence", "stop_condition",
|
|
3111
|
+
"misclassification_cost"):
|
|
3112
|
+
value = document[field]
|
|
3113
|
+
if not isinstance(value, str) or not value.strip():
|
|
3114
|
+
raise LaunchError(f"{context}.{field} must be a non-empty string")
|
|
3115
|
+
classes = document["classes"]
|
|
3116
|
+
if (
|
|
3117
|
+
not isinstance(classes, list) or len(classes) < 2
|
|
3118
|
+
or not all(isinstance(value, str) and value for value in classes)
|
|
3119
|
+
):
|
|
3120
|
+
raise LaunchError(
|
|
3121
|
+
f"{context}.classes must list at least two non-empty class names — the "
|
|
3122
|
+
f"stop-relevant class and the relief valves that keep it honest"
|
|
3123
|
+
)
|
|
3124
|
+
if len(set(classes)) != len(classes):
|
|
3125
|
+
raise LaunchError(f"{context}.classes repeats a value")
|
|
3126
|
+
for value in classes:
|
|
3127
|
+
if value != value.strip():
|
|
3128
|
+
raise LaunchError(
|
|
3129
|
+
f"{context}.classes[{value!r}] may not carry leading or trailing "
|
|
3130
|
+
f"whitespace, which no reader can see"
|
|
3131
|
+
)
|
|
3132
|
+
stop = document["stop_class"]
|
|
3133
|
+
if not isinstance(stop, str) or stop not in classes:
|
|
3134
|
+
raise LaunchError(
|
|
3135
|
+
f"{context}.stop_class must name exactly one member of classes; "
|
|
3136
|
+
f"got {stop!r}"
|
|
3137
|
+
)
|
|
3138
|
+
non_defects = document["non_defects"]
|
|
3139
|
+
if (
|
|
3140
|
+
not isinstance(non_defects, list) or not non_defects
|
|
3141
|
+
or not all(isinstance(value, str) and value.strip() for value in non_defects)
|
|
3142
|
+
):
|
|
3143
|
+
raise LaunchError(
|
|
3144
|
+
f"{context}.non_defects must be a non-empty list of the defect-lookalikes "
|
|
3145
|
+
f"this criterion excludes"
|
|
3146
|
+
)
|
|
3147
|
+
goldens = document["goldens"]
|
|
3148
|
+
if not isinstance(goldens, list):
|
|
3149
|
+
raise LaunchError(f"{context}.goldens must be a list")
|
|
3150
|
+
counts = {kind: 0 for kind in CRITERION_GOLDEN_KINDS}
|
|
3151
|
+
for index, golden in enumerate(goldens):
|
|
3152
|
+
slot = f"{context}.goldens[{index}]"
|
|
3153
|
+
if not isinstance(golden, dict):
|
|
3154
|
+
raise LaunchError(f"{slot} must be an object")
|
|
3155
|
+
missing = sorted(CRITERION_GOLDEN_KEYS - set(golden))
|
|
3156
|
+
if missing:
|
|
3157
|
+
raise LaunchError(f"{slot} is missing {', '.join(missing)}")
|
|
3158
|
+
unknown = sorted(set(golden) - CRITERION_GOLDEN_KEYS)
|
|
3159
|
+
if unknown:
|
|
3160
|
+
raise LaunchError(f"{slot} carries unknown key(s): {', '.join(unknown)}")
|
|
3161
|
+
kind = golden["kind"]
|
|
3162
|
+
if kind not in CRITERION_GOLDEN_KINDS:
|
|
3163
|
+
raise LaunchError(
|
|
3164
|
+
f"{slot}.kind must be one of {', '.join(CRITERION_GOLDEN_KINDS)}; "
|
|
3165
|
+
f"got {kind!r}"
|
|
3166
|
+
)
|
|
3167
|
+
for field in ("case", "why"):
|
|
3168
|
+
if not isinstance(golden[field], str) or not golden[field].strip():
|
|
3169
|
+
raise LaunchError(f"{slot}.{field} must be a non-empty string")
|
|
3170
|
+
date = golden["date"]
|
|
3171
|
+
if not isinstance(date, str):
|
|
3172
|
+
raise LaunchError(f"{slot}.date must be a string")
|
|
3173
|
+
try:
|
|
3174
|
+
datetime.date.fromisoformat(date)
|
|
3175
|
+
except ValueError as exc:
|
|
3176
|
+
raise LaunchError(f"{slot}.date must be an ISO date: {exc}") from exc
|
|
3177
|
+
if golden["provenance"] not in CRITERION_PROVENANCE:
|
|
3178
|
+
raise LaunchError(
|
|
3179
|
+
f"{slot}.provenance must be measured or constructed; "
|
|
3180
|
+
f"got {golden['provenance']!r}"
|
|
3181
|
+
)
|
|
3182
|
+
counts[kind] += 1
|
|
3183
|
+
short = [
|
|
3184
|
+
f"{kind} {counts[kind]}/{minimum}"
|
|
3185
|
+
for kind, minimum in CRITERION_GOLDEN_MINIMUMS.items()
|
|
3186
|
+
if counts[kind] < minimum
|
|
3187
|
+
]
|
|
3188
|
+
if short:
|
|
3189
|
+
raise LaunchError(
|
|
3190
|
+
f"{context}.goldens falls short of the admission bar ({', '.join(short)}); "
|
|
3191
|
+
f"the bar is never weakened to admit an entry"
|
|
3192
|
+
)
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def criterion_schema(classes: list) -> dict:
|
|
3196
|
+
"""The compiled findings schema — a pure function of the enum, which is what makes
|
|
3197
|
+
`--verify-receipts` able to recompile it from the packet's record and compare
|
|
3198
|
+
digests. Both object levels reject unknown fields; `findings` may be empty."""
|
|
3199
|
+
return {
|
|
3200
|
+
"type": "object",
|
|
3201
|
+
"additionalProperties": False,
|
|
3202
|
+
"required": ["findings"],
|
|
3203
|
+
"properties": {
|
|
3204
|
+
"findings": {
|
|
3205
|
+
"type": "array",
|
|
3206
|
+
"items": {
|
|
3207
|
+
"type": "object",
|
|
3208
|
+
"additionalProperties": False,
|
|
3209
|
+
"required": ["class", "finding"],
|
|
3210
|
+
"properties": {
|
|
3211
|
+
"class": {"enum": list(classes)},
|
|
3212
|
+
"finding": {"type": "string", "minLength": 1},
|
|
3213
|
+
},
|
|
3214
|
+
},
|
|
3215
|
+
},
|
|
3216
|
+
},
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
|
|
3220
|
+
def criterion_schema_bytes(document: dict) -> bytes:
|
|
3221
|
+
"""Canonical bytes for the compiled schema: sorted keys, no whitespace, one
|
|
3222
|
+
trailing newline. Byte-identical on every compile of the same document — proven by
|
|
3223
|
+
the double-compile control — because the receipt digest is compared against a
|
|
3224
|
+
recompilation."""
|
|
3225
|
+
schema = criterion_schema(document["classes"])
|
|
3226
|
+
return (
|
|
3227
|
+
json.dumps(schema, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
3228
|
+
+ "\n"
|
|
3229
|
+
).encode("utf-8")
|
|
3230
|
+
|
|
3231
|
+
|
|
3232
|
+
def criterion_record_line(document: dict) -> str:
|
|
3233
|
+
"""The one line the packet carries: the canonical serialization of the document
|
|
3234
|
+
behind the record marker, the same shape the ReviewPlan/v1 record rides in."""
|
|
3235
|
+
return CRITERION_RECORD_MARKER + json.dumps(
|
|
3236
|
+
document, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
3237
|
+
)
|
|
3238
|
+
|
|
3239
|
+
|
|
3240
|
+
def extract_criterion(text: str, context: str) -> dict:
|
|
3241
|
+
"""The criterion the packet declares, from its record line — exactly one.
|
|
3242
|
+
|
|
3243
|
+
Zero records is a refusal rather than None on purpose: this is only called when
|
|
3244
|
+
the plan's rows carry the discipline clause, and a launch that declared the
|
|
3245
|
+
discipline cannot verify without the criterion."""
|
|
3246
|
+
# LINE-anchored, because the packet is free text: an unanchored find took a prose
|
|
3247
|
+
# mention — "for example, ReviewCriterion/v1: {...}" mid-sentence — as the declared
|
|
3248
|
+
# record (criterion round, #5). The record is a line the compiler printed: marker at
|
|
3249
|
+
# line start, one JSON value, nothing else before the newline.
|
|
3250
|
+
starts = []
|
|
3251
|
+
index = text.find(CRITERION_RECORD_MARKER)
|
|
3252
|
+
while index >= 0:
|
|
3253
|
+
if index == 0 or text[index - 1] == "\n":
|
|
3254
|
+
starts.append(index)
|
|
3255
|
+
index = text.find(CRITERION_RECORD_MARKER, index + 1)
|
|
3256
|
+
if not starts:
|
|
3257
|
+
raise LaunchError(
|
|
3258
|
+
f"{context} carries no {CRITERION_RECORD_SCHEMA} record line, so the "
|
|
3259
|
+
f"criterion the plan's discipline clause declares is nowhere in the bytes "
|
|
3260
|
+
f"the reviewers consumed"
|
|
3261
|
+
)
|
|
3262
|
+
if len(starts) > 1:
|
|
3263
|
+
raise LaunchError(
|
|
3264
|
+
f"{context} carries more than one {CRITERION_RECORD_SCHEMA} record; a mixed "
|
|
3265
|
+
f"packet is split into one packet per criterion, so exactly one is the claim "
|
|
3266
|
+
f"this can adjudicate"
|
|
3267
|
+
)
|
|
3268
|
+
start = starts[0] + len(CRITERION_RECORD_MARKER)
|
|
3269
|
+
try:
|
|
3270
|
+
document, consumed = json.JSONDecoder().raw_decode(text[start:])
|
|
3271
|
+
except json.JSONDecodeError as exc:
|
|
3272
|
+
raise LaunchError(
|
|
3273
|
+
f"{context} carries a {CRITERION_RECORD_SCHEMA} record that does not parse: "
|
|
3274
|
+
f"{exc}"
|
|
3275
|
+
) from exc
|
|
3276
|
+
trailing = text[start + consumed:].split("\n", 1)[0]
|
|
3277
|
+
if trailing.strip():
|
|
3278
|
+
raise LaunchError(
|
|
3279
|
+
f"{context} carries a {CRITERION_RECORD_SCHEMA} record line with trailing "
|
|
3280
|
+
f"content ({trailing.strip()[:40]!r}); the record is the whole line, so "
|
|
3281
|
+
f"extra text on it is a different claim than the compiler printed"
|
|
3282
|
+
)
|
|
3283
|
+
validate_criterion(document, f"{context}'s {CRITERION_RECORD_SCHEMA} record")
|
|
3284
|
+
return document
|
|
3285
|
+
|
|
3286
|
+
|
|
3287
|
+
def criterion_classes_of_schema(schema: Any, context: str) -> list:
|
|
3288
|
+
"""The enum a compiled schema file carries — accepted only when the whole schema
|
|
3289
|
+
equals what compiling that enum produces, so a hand-edited or foreign schema is
|
|
3290
|
+
refused rather than partially read."""
|
|
3291
|
+
try:
|
|
3292
|
+
classes = schema["properties"]["findings"]["items"]["properties"]["class"]["enum"]
|
|
3293
|
+
except (TypeError, KeyError):
|
|
3294
|
+
raise LaunchError(
|
|
3295
|
+
f"{context} is not a compiled criterion schema; compile one with "
|
|
3296
|
+
f"--compile-criterion"
|
|
3297
|
+
) from None
|
|
3298
|
+
# Self-consistency alone lets the schema supply its own authority: an exact
|
|
3299
|
+
# criterion_schema(['anything']) equals recompiling its own enum although the
|
|
3300
|
+
# compiler can never produce it — validate_criterion refuses every one-class
|
|
3301
|
+
# document (criterion round, #2). The enum must also be one the compiler admits.
|
|
3302
|
+
if (
|
|
3303
|
+
not isinstance(classes, list) or len(classes) < 2
|
|
3304
|
+
or len(set(classes)) != len(classes)
|
|
3305
|
+
or not all(
|
|
3306
|
+
isinstance(value, str) and value and value == value.strip()
|
|
3307
|
+
and _utf8_encodable(value)
|
|
3308
|
+
for value in classes
|
|
3309
|
+
)
|
|
3310
|
+
):
|
|
3311
|
+
raise LaunchError(
|
|
3312
|
+
f"{context} carries an enum --compile-criterion can never produce (fewer "
|
|
3313
|
+
f"than two classes, or a duplicate, empty, whitespace-padded, or "
|
|
3314
|
+
f"non-UTF-8-encodable name); it was not written by the compiler — "
|
|
3315
|
+
f"recompile rather than editing the artifact"
|
|
3316
|
+
)
|
|
3317
|
+
if schema != criterion_schema(classes):
|
|
3318
|
+
raise LaunchError(
|
|
3319
|
+
f"{context} differs from what compiling its own enum produces, so it was "
|
|
3320
|
+
f"not written by --compile-criterion; recompile rather than editing the "
|
|
3321
|
+
f"artifact"
|
|
3322
|
+
)
|
|
3323
|
+
return list(classes)
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def findings_violations(result: Any, classes: list) -> list:
|
|
3327
|
+
"""Why this result is not a findings record under the declared enum — every
|
|
3328
|
+
violation named, empty when it conforms. Deterministic and total: no salvage, no
|
|
3329
|
+
guessed class, no partial admission."""
|
|
3330
|
+
violations = []
|
|
3331
|
+
if not isinstance(result, dict):
|
|
3332
|
+
return [f"the result is {type(result).__name__}, not an object"]
|
|
3333
|
+
unknown = sorted(set(result) - {"findings"})
|
|
3334
|
+
if unknown:
|
|
3335
|
+
violations.append(f"unknown result key(s): {', '.join(unknown)}")
|
|
3336
|
+
findings = result.get("findings")
|
|
3337
|
+
if not isinstance(findings, list):
|
|
3338
|
+
violations.append("the result carries no findings array")
|
|
3339
|
+
return violations
|
|
3340
|
+
for index, finding in enumerate(findings):
|
|
3341
|
+
slot = f"findings[{index}]"
|
|
3342
|
+
if not isinstance(finding, dict):
|
|
3343
|
+
violations.append(f"{slot} is {type(finding).__name__}, not an object")
|
|
3344
|
+
continue
|
|
3345
|
+
unknown = sorted(set(finding) - {"class", "finding"})
|
|
3346
|
+
if unknown:
|
|
3347
|
+
violations.append(f"{slot} carries unknown key(s): {', '.join(unknown)}")
|
|
3348
|
+
if "class" not in finding:
|
|
3349
|
+
violations.append(f"{slot} carries no class; a row is never admitted unclassified")
|
|
3350
|
+
elif finding["class"] not in classes:
|
|
3351
|
+
violations.append(
|
|
3352
|
+
f"{slot} carries class {finding['class']!r}, which is not in the "
|
|
3353
|
+
f"declared enum ({', '.join(classes)})"
|
|
3354
|
+
)
|
|
3355
|
+
prose = finding.get("finding")
|
|
3356
|
+
if not isinstance(prose, str) or not prose.strip():
|
|
3357
|
+
violations.append(f"{slot}.finding must be a non-empty string")
|
|
3358
|
+
return violations
|
|
3359
|
+
|
|
3360
|
+
|
|
3361
|
+
def compile_criterion_command(criterion_file: str, schema_out: str) -> int:
|
|
3362
|
+
"""Validate a criterion document and publish its compiled findings schema.
|
|
3363
|
+
|
|
3364
|
+
Prints the packet record line the dispatching agent pastes verbatim, and the
|
|
3365
|
+
digest receipt emission will stamp — so the three artifacts (packet record,
|
|
3366
|
+
schema file, receipt digest) cannot drift from one authoring."""
|
|
3367
|
+
path = pathlib.Path(criterion_file).expanduser()
|
|
3368
|
+
try:
|
|
3369
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
3370
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3371
|
+
raise LaunchError(f"cannot read criterion document {path}: {exc}") from exc
|
|
3372
|
+
validate_criterion(document, str(path))
|
|
3373
|
+
payload = criterion_schema_bytes(document)
|
|
3374
|
+
target = pathlib.Path(schema_out).expanduser()
|
|
3375
|
+
try:
|
|
3376
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
3377
|
+
publish_atomically(target, payload)
|
|
3378
|
+
except OSError as exc:
|
|
3379
|
+
raise LaunchError(f"cannot write compiled schema into {target}: {exc}") from exc
|
|
3380
|
+
print(criterion_record_line(document))
|
|
3381
|
+
print(f"criterion_schema_sha256={hashlib.sha256(payload).hexdigest()} {target}")
|
|
3382
|
+
return 0
|
|
3383
|
+
|
|
3384
|
+
|
|
3385
|
+
def check_findings_command(result_file: str, schema_file: str) -> int:
|
|
3386
|
+
"""The structural accepting channel, standalone: refuse a findings result that
|
|
3387
|
+
fails the compiled schema, each violation by name. The same check runs inside
|
|
3388
|
+
--emit-receipt when REVIEW_CRITERION_SCHEMA is set, so bypassing this one only
|
|
3389
|
+
forfeits the receipt."""
|
|
3390
|
+
schema_path = pathlib.Path(schema_file).expanduser()
|
|
3391
|
+
try:
|
|
3392
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
3393
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3394
|
+
raise LaunchError(f"cannot read compiled schema {schema_path}: {exc}") from exc
|
|
3395
|
+
classes = criterion_classes_of_schema(schema, str(schema_path))
|
|
3396
|
+
result_path = pathlib.Path(result_file).expanduser()
|
|
3397
|
+
try:
|
|
3398
|
+
result = json.loads(result_path.read_text(encoding="utf-8"))
|
|
3399
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
3400
|
+
raise LaunchError(f"cannot read result {result_path}: {exc}") from exc
|
|
3401
|
+
except json.JSONDecodeError as exc:
|
|
3402
|
+
print(f"REFUSED: {result_path} is not JSON, so no finding in it carries a class: {exc}")
|
|
3403
|
+
return 1
|
|
3404
|
+
violations = findings_violations(result, classes)
|
|
3405
|
+
if violations:
|
|
3406
|
+
for violation in violations:
|
|
3407
|
+
print(f"REFUSED: {violation}")
|
|
3408
|
+
return 1
|
|
3409
|
+
count = len(result["findings"])
|
|
3410
|
+
print(f"OK: {count} finding{'s' if count != 1 else ''} conform to the declared enum")
|
|
3411
|
+
return 0
|
|
3412
|
+
|
|
3413
|
+
|
|
3414
|
+
# The host CLIs' structured-output flags, and how each registers them. A TABLE plus a
|
|
3415
|
+
# PROBE rather than an offer key: a declared capability is documentation, and the rule
|
|
3416
|
+
# is probe-over-docs — the flag either appears in the installed binary's registered
|
|
3417
|
+
# options or the route renders prose discipline.
|
|
3418
|
+
HOST_SCHEMA_FLAGS = {
|
|
3419
|
+
"codex": (("exec", "--help"), "--output-schema"),
|
|
3420
|
+
"claude": (("--help",), "--json-schema"),
|
|
3421
|
+
}
|
|
3422
|
+
SCHEMA_FLAG_ABSENT_EXIT = 4
|
|
3423
|
+
|
|
3424
|
+
|
|
3425
|
+
def check_schema_flag_command(host: str, config_path: pathlib.Path) -> int:
|
|
3426
|
+
"""Probe the resolved backend binary for its structured-output flag.
|
|
3427
|
+
|
|
3428
|
+
Exit 0 when the flag is registered, SCHEMA_FLAG_ABSENT_EXIT when the binary runs
|
|
3429
|
+
and does not register it — absence is a finding, not an error — and a LaunchError
|
|
3430
|
+
when nothing could be probed at all."""
|
|
3431
|
+
if host not in HOST_SCHEMA_FLAGS:
|
|
3432
|
+
raise LaunchError(
|
|
3433
|
+
f"no schema flag is known for host {host!r}; known hosts: "
|
|
3434
|
+
f"{', '.join(sorted(HOST_SCHEMA_FLAGS))}"
|
|
3435
|
+
)
|
|
3436
|
+
config = load_config(config_path)
|
|
3437
|
+
command = config.get("backends", {}).get(host, {}).get("command")
|
|
3438
|
+
if not isinstance(command, str) or not command:
|
|
3439
|
+
raise LaunchError(f"config names no backend command for host {host!r}")
|
|
3440
|
+
help_args, flag = HOST_SCHEMA_FLAGS[host]
|
|
3441
|
+
try:
|
|
3442
|
+
probe = subprocess.run(
|
|
3443
|
+
[command, *help_args], capture_output=True, text=True, timeout=60,
|
|
3444
|
+
)
|
|
3445
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
3446
|
+
raise LaunchError(f"cannot probe {command!r} for {flag}: {exc}") from exc
|
|
3447
|
+
# A failed help run proves nothing about the flag — and its ERROR text can carry
|
|
3448
|
+
# the very token being searched for ("error: unknown option --output-schema"), so
|
|
3449
|
+
# scanning it reported present on the known-opposite input (criterion round, #7).
|
|
3450
|
+
if probe.returncode != 0:
|
|
3451
|
+
detail = (probe.stderr or probe.stdout).strip()[:160]
|
|
3452
|
+
raise LaunchError(
|
|
3453
|
+
f"cannot probe {command!r} for {flag}: help exited {probe.returncode}"
|
|
3454
|
+
+ (f" ({detail})" if detail else "")
|
|
3455
|
+
+ " — presence cannot be read from a failed probe"
|
|
3456
|
+
)
|
|
3457
|
+
text = (probe.stdout + probe.stderr).replace(",", " ")
|
|
3458
|
+
registered = any(
|
|
3459
|
+
token == flag or token.startswith(f"{flag}=")
|
|
3460
|
+
for token in text.split()
|
|
3461
|
+
)
|
|
3462
|
+
if registered:
|
|
3463
|
+
print(f"schema flag {flag} for host {host}: present ({command})")
|
|
3464
|
+
return 0
|
|
3465
|
+
print(
|
|
3466
|
+
f"schema flag {flag} for host {host}: absent ({command} registers no such "
|
|
3467
|
+
f"option; dispatch this route as prose discipline)"
|
|
3468
|
+
)
|
|
3469
|
+
return SCHEMA_FLAG_ABSENT_EXIT
|
|
3470
|
+
|
|
3471
|
+
|
|
2965
3472
|
# ── receipt production (the adapter side of the same contract) ──────────────
|
|
2966
3473
|
# Everything above adjudicates receipts; nothing produced one. That gap is the whole
|
|
2967
3474
|
# defect: the only party able to write a receipt was whoever ran the review, so the
|
|
@@ -2983,6 +3490,12 @@ RECEIPT_METHOD_ENV = "REVIEW_METHOD_ID"
|
|
|
2983
3490
|
# multi-pass method, and a third-party one gets them right by not participating.
|
|
2984
3491
|
RECEIPT_SEED_ENV = "REVIEW_ORDERING_SEED"
|
|
2985
3492
|
RECEIPT_SWAP_ENV = "REVIEW_SWAP_GROUP"
|
|
3493
|
+
# The compiled criterion schema for THIS dispatch, set by the dispatch wrapper only
|
|
3494
|
+
# when it passed the schema to the host. Set, emission validates the result against it
|
|
3495
|
+
# and refuses to write a receipt for a class-less or out-of-enum finding — so the only
|
|
3496
|
+
# path to a receipt IS the structural accepting channel, and a bypassed check is an
|
|
3497
|
+
# unproven dispatch. Unset — every prose route — emission behaves exactly as today.
|
|
3498
|
+
RECEIPT_CRITERION_ENV = "REVIEW_CRITERION_SCHEMA"
|
|
2986
3499
|
# The probe adjudicates a one-row plan, and every plan's one required row is the base
|
|
2987
3500
|
# panel wearing its reserved id (`_review_identity_reason`). It had a name of its own —
|
|
2988
3501
|
# `adapter-conformance-probe` — which made the probe the only plan in the system whose
|
|
@@ -3083,18 +3596,79 @@ def emit_receipt_command(
|
|
|
3083
3596
|
if not key or not separator:
|
|
3084
3597
|
raise LaunchError(f"--evidence takes key=value, got {item!r}")
|
|
3085
3598
|
fields[key] = value
|
|
3599
|
+
# Present-but-empty is a configuration error, never a silent prose downgrade: an
|
|
3600
|
+
# exported empty value made the whole validation vacuous while the caller believed
|
|
3601
|
+
# it armed (criterion round, #3 — the empty-variable class the corpus already
|
|
3602
|
+
# names for shell checks).
|
|
3603
|
+
criterion_env = os.environ.get(RECEIPT_CRITERION_ENV)
|
|
3604
|
+
if criterion_env is not None and not criterion_env.strip():
|
|
3605
|
+
raise LaunchError(
|
|
3606
|
+
f"{RECEIPT_CRITERION_ENV} is set but empty; unset it for a prose-route "
|
|
3607
|
+
f"dispatch or point it at a compiled schema"
|
|
3608
|
+
)
|
|
3609
|
+
# Only a SUCCESSFUL dispatch is validated and stamped: a failed dispatch's receipt
|
|
3610
|
+
# records the failure and is already refused credit for exit != 0, and refusing to
|
|
3611
|
+
# write it would erase the failure record — while stamping it would claim a
|
|
3612
|
+
# validation that never ran.
|
|
3613
|
+
criterion_schema_path = criterion_env if status == 0 else None
|
|
3614
|
+
# ONE byte snapshot each for the schema and the result: validating one read and
|
|
3615
|
+
# hashing a second let the digest bind bytes the validation never saw — the same
|
|
3616
|
+
# false PASS whether the swap is a race or an adversary (criterion round, #4).
|
|
3617
|
+
criterion_digest = None
|
|
3618
|
+
result_bytes = None
|
|
3619
|
+
if criterion_schema_path:
|
|
3620
|
+
schema_file = pathlib.Path(criterion_schema_path).expanduser()
|
|
3621
|
+
try:
|
|
3622
|
+
schema_bytes = schema_file.read_bytes()
|
|
3623
|
+
except OSError as exc:
|
|
3624
|
+
raise LaunchError(
|
|
3625
|
+
f"{RECEIPT_CRITERION_ENV} names {schema_file}, which is not a readable "
|
|
3626
|
+
f"compiled schema: {exc}"
|
|
3627
|
+
) from exc
|
|
3628
|
+
try:
|
|
3629
|
+
schema = json.loads(schema_bytes)
|
|
3630
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3631
|
+
raise LaunchError(
|
|
3632
|
+
f"{RECEIPT_CRITERION_ENV} names {schema_file}, which is not a readable "
|
|
3633
|
+
f"compiled schema: {exc}"
|
|
3634
|
+
) from exc
|
|
3635
|
+
classes = criterion_classes_of_schema(schema, str(schema_file))
|
|
3636
|
+
try:
|
|
3637
|
+
result_bytes = pathlib.Path(result_file).expanduser().read_bytes()
|
|
3638
|
+
except OSError as exc:
|
|
3639
|
+
raise LaunchError(f"cannot read result {result_file}: {exc}") from exc
|
|
3640
|
+
try:
|
|
3641
|
+
result = json.loads(result_bytes)
|
|
3642
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3643
|
+
raise LaunchError(
|
|
3644
|
+
f"no receipt: the result is not JSON, so no finding in it carries a "
|
|
3645
|
+
f"class from the declared enum ({exc})"
|
|
3646
|
+
) from exc
|
|
3647
|
+
violations = findings_violations(result, classes)
|
|
3648
|
+
if violations:
|
|
3649
|
+
raise LaunchError(
|
|
3650
|
+
"no receipt: " + "; ".join(violations) + " — a result the schema "
|
|
3651
|
+
"refuses is not published, and runtime never guesses a class"
|
|
3652
|
+
)
|
|
3653
|
+
criterion_digest = hashlib.sha256(schema_bytes).hexdigest()
|
|
3086
3654
|
dispatch_id = uuid.uuid4().hex
|
|
3087
3655
|
receipt = {
|
|
3088
3656
|
"schema": RECEIPT_SCHEMA,
|
|
3089
3657
|
"method_id": method_id,
|
|
3090
3658
|
"dispatch_id": dispatch_id,
|
|
3091
3659
|
"packet_sha256": _sha256_file(packet_file),
|
|
3092
|
-
|
|
3660
|
+
# The bytes the validation saw, when it ran; the file, when it did not.
|
|
3661
|
+
"result_sha256": (
|
|
3662
|
+
hashlib.sha256(result_bytes).hexdigest()
|
|
3663
|
+
if result_bytes is not None else _sha256_file(result_file)
|
|
3664
|
+
),
|
|
3093
3665
|
"provider": provider,
|
|
3094
3666
|
"model": model,
|
|
3095
3667
|
"effort": effort,
|
|
3096
3668
|
"exit_status": status,
|
|
3097
3669
|
}
|
|
3670
|
+
if criterion_digest:
|
|
3671
|
+
receipt["criterion_schema_sha256"] = criterion_digest
|
|
3098
3672
|
if fields:
|
|
3099
3673
|
receipt["evidence"] = fields
|
|
3100
3674
|
for key, name in (("ordering_seed", RECEIPT_SEED_ENV), ("swap_group", RECEIPT_SWAP_ENV)):
|
|
@@ -3222,7 +3796,12 @@ def _merge_method_passes(method_id: str, group: list[dict], main_dispatch_id: st
|
|
|
3222
3796
|
# `passes`, and for a lone receipt it is naturally the one-element set. Round 4 made
|
|
3223
3797
|
# the multi-pass record faithful and this is the same sentence applied to the twin
|
|
3224
3798
|
# F1 already names: asked of EACH receipt, singleton included.
|
|
3225
|
-
for field_name in (
|
|
3799
|
+
for field_name in (
|
|
3800
|
+
"provider", "model", "effort", "packet_sha256", "criterion_schema_sha256",
|
|
3801
|
+
):
|
|
3802
|
+
# criterion_schema_sha256 included with PRESENCE counted as a value: passes
|
|
3803
|
+
# validated against different schemas — or one validated and one not — ran
|
|
3804
|
+
# under different criteria and cannot be one method's passes.
|
|
3226
3805
|
values = {json.dumps(receipt.get(field_name), sort_keys=True) for receipt in group}
|
|
3227
3806
|
if len(values) > 1:
|
|
3228
3807
|
raise LaunchError(
|
|
@@ -3586,7 +4165,8 @@ def routed_preset_names(presets: dict[str, Any]) -> set[str]:
|
|
|
3586
4165
|
"""
|
|
3587
4166
|
return {
|
|
3588
4167
|
name for name, preset in presets.items()
|
|
3589
|
-
if isinstance(preset, dict)
|
|
4168
|
+
if isinstance(preset, dict)
|
|
4169
|
+
and ("mission" in preset or "trigger" in preset or "criterion" in preset)
|
|
3590
4170
|
}
|
|
3591
4171
|
|
|
3592
4172
|
|
|
@@ -4527,7 +5107,7 @@ def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> lis
|
|
|
4527
5107
|
|
|
4528
5108
|
def resolve_review_for_plan(
|
|
4529
5109
|
review: ReviewPlan, config: dict[str, Any], host: str, main_model: str,
|
|
4530
|
-
main_effort: str, context: str,
|
|
5110
|
+
main_effort: str, context: str, criterion: bool = False,
|
|
4531
5111
|
) -> tuple[dict, "ReviewReport | None"]:
|
|
4532
5112
|
"""(method registry, resolved report) for a review against the main seat.
|
|
4533
5113
|
|
|
@@ -4535,6 +5115,16 @@ def resolve_review_for_plan(
|
|
|
4535
5115
|
what a fresh launch would; two copies of this would let the Custom hub show a report
|
|
4536
5116
|
the next launch disagrees with."""
|
|
4537
5117
|
if review.source != "composable":
|
|
5118
|
+
if criterion:
|
|
5119
|
+
# The clause renders only through composable review rows, so on a legacy
|
|
5120
|
+
# plan the toggle is accepted and then does nothing — a false success the
|
|
5121
|
+
# operator reads as discipline in force (criterion round, #0).
|
|
5122
|
+
raise LaunchError(
|
|
5123
|
+
f"{context} declares criterion = true, but its review uses the legacy "
|
|
5124
|
+
f"schema; the discipline clause renders only through composable review "
|
|
5125
|
+
f"rows, so the toggle would be silently inert — author a composable "
|
|
5126
|
+
f"[review] block or drop the toggle"
|
|
5127
|
+
)
|
|
4538
5128
|
return {}, None
|
|
4539
5129
|
provider = config["hosts"][host].get("provider")
|
|
4540
5130
|
if not isinstance(provider, str) or not provider:
|
|
@@ -4544,7 +5134,7 @@ def resolve_review_for_plan(
|
|
|
4544
5134
|
)
|
|
4545
5135
|
methods = load_review_methods(config)
|
|
4546
5136
|
main_seat = ReviewBinding(provider, host, main_model, main_effort)
|
|
4547
|
-
return methods, resolve_composable_review(review, main_seat, config, methods)
|
|
5137
|
+
return methods, resolve_composable_review(review, main_seat, config, methods, criterion)
|
|
4548
5138
|
|
|
4549
5139
|
|
|
4550
5140
|
def effective_review_family(
|
|
@@ -4586,6 +5176,7 @@ def apply_review_plan(plan: dict[str, Any], config: dict[str, Any], review: Revi
|
|
|
4586
5176
|
methods, report = resolve_review_for_plan(
|
|
4587
5177
|
review, config, plan["host"], plan["tiers"][plan["main_tier"]]["model"],
|
|
4588
5178
|
tier_effort(plan, plan["main_tier"]), f"custom.{plan['preset']}",
|
|
5179
|
+
plan.get("criterion", False),
|
|
4589
5180
|
)
|
|
4590
5181
|
plan["review_plan"] = review
|
|
4591
5182
|
# Editing on one host replaces that host's arm and leaves the others exactly as
|
|
@@ -4800,8 +5391,18 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
4800
5391
|
# isolated mechanism is an invalid launch, and that has to fail while the plan is
|
|
4801
5392
|
# being built rather than halfway through printing a contract.
|
|
4802
5393
|
main_effort = frontier_effort if main_tier == "frontier" else tiers[main_tier]["effort"]
|
|
5394
|
+
# The criterion-discipline toggle. A BOOLEAN, deliberately: the criterion itself is
|
|
5395
|
+
# per-review and rides the packet, which packet_sha256 binds — a preset carrying its
|
|
5396
|
+
# content would put per-review text into the per-launch contract the golden pins.
|
|
5397
|
+
criterion = preset.get("criterion", False)
|
|
5398
|
+
if not isinstance(criterion, bool):
|
|
5399
|
+
raise LaunchError(
|
|
5400
|
+
f"presets.{preset_name}.criterion must be a boolean toggle; the criterion "
|
|
5401
|
+
f"itself is declared in the review packet, never in the preset"
|
|
5402
|
+
)
|
|
4803
5403
|
review_methods, review_report = resolve_review_for_plan(
|
|
4804
|
-
review, config, host, tiers[main_tier]["model"], main_effort,
|
|
5404
|
+
review, config, host, tiers[main_tier]["model"], main_effort,
|
|
5405
|
+
f"presets.{preset_name}", criterion,
|
|
4805
5406
|
)
|
|
4806
5407
|
codex_policy = preset.get("codex_execution_policy")
|
|
4807
5408
|
claude_policy = preset.get("claude_permission_mode")
|
|
@@ -4894,6 +5495,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
4894
5495
|
},
|
|
4895
5496
|
"mission": mission,
|
|
4896
5497
|
"trigger": trigger,
|
|
5498
|
+
"criterion": criterion,
|
|
4897
5499
|
}
|
|
4898
5500
|
|
|
4899
5501
|
|
|
@@ -6061,6 +6663,13 @@ def preset_from_plan(
|
|
|
6061
6663
|
"codex_execution_policy": plan["codex_execution_policy"],
|
|
6062
6664
|
"claude_permission_mode": plan["claude_permission_mode"],
|
|
6063
6665
|
}
|
|
6666
|
+
if plan.get("criterion"):
|
|
6667
|
+
# Save As from a criterion-toggled plan silently dropped the discipline: the
|
|
6668
|
+
# routed-name guard covers same-name shadowing, not a fresh name, and the
|
|
6669
|
+
# reload defaulted the absent field to false while Save reported success
|
|
6670
|
+
# (criterion round, #8). Written only when true, so every existing saved
|
|
6671
|
+
# preset stays byte-identical.
|
|
6672
|
+
fields["criterion"] = True
|
|
6064
6673
|
if review_block is None:
|
|
6065
6674
|
# Carrying both schemas on one preset is a validation error, so the legacy
|
|
6066
6675
|
# names appear only when there is no authored block to write.
|
|
@@ -8941,6 +9550,28 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
8941
9550
|
"--fold-receipts", nargs=3, metavar=("DIR", "PACKET_FILE", "MAIN_DISPATCH_ID"),
|
|
8942
9551
|
help=f"fold a directory of receipts into one {RECEIPT_BUNDLE_SCHEMA} bundle on stdout",
|
|
8943
9552
|
)
|
|
9553
|
+
# The criterion side of the same per-review file surface. Compile turns a criterion
|
|
9554
|
+
# document into the canonical findings schema and prints the packet record line;
|
|
9555
|
+
# check-findings is the structural accepting channel standalone; check-schema-flag
|
|
9556
|
+
# probes the installed backend for its structured-output flag, because a declared
|
|
9557
|
+
# capability is docs and the rule is probe-over-docs.
|
|
9558
|
+
parser.add_argument(
|
|
9559
|
+
"--compile-criterion", nargs=2, metavar=("CRITERION_FILE", "SCHEMA_OUT"),
|
|
9560
|
+
help=f"validate a criterion document, write its compiled findings schema, and "
|
|
9561
|
+
f"print the {CRITERION_RECORD_SCHEMA} record line the packet carries",
|
|
9562
|
+
)
|
|
9563
|
+
parser.add_argument(
|
|
9564
|
+
"--check-findings", nargs=2, metavar=("RESULT_FILE", "SCHEMA_FILE"),
|
|
9565
|
+
help="refuse a findings result that fails the compiled criterion schema, each "
|
|
9566
|
+
"violation by name; the same check runs inside --emit-receipt when "
|
|
9567
|
+
f"${RECEIPT_CRITERION_ENV} is set",
|
|
9568
|
+
)
|
|
9569
|
+
parser.add_argument(
|
|
9570
|
+
"--check-schema-flag", metavar="PROBE_HOST",
|
|
9571
|
+
help="probe the resolved backend binary for its structured-output flag; exit 0 "
|
|
9572
|
+
f"when registered, {SCHEMA_FLAG_ABSENT_EXIT} when the binary runs and does "
|
|
9573
|
+
f"not register it",
|
|
9574
|
+
)
|
|
8944
9575
|
# REMAINDER and not "+": the adapter's own command line carries flags, and "+" stops
|
|
8945
9576
|
# at the first token starting with a dash — which handed `--model` to the host
|
|
8946
9577
|
# positional and rejected the run before the adapter was ever dispatched.
|
|
@@ -8956,7 +9587,8 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
8956
9587
|
args = parser.parse_args(argv)
|
|
8957
9588
|
hostless = (
|
|
8958
9589
|
args.verify_receipts or args.emit_receipt or args.fold_receipts
|
|
8959
|
-
or args.check_adapter
|
|
9590
|
+
or args.check_adapter or args.compile_criterion or args.check_findings
|
|
9591
|
+
or args.check_schema_flag
|
|
8960
9592
|
)
|
|
8961
9593
|
if args.host is None and not hostless:
|
|
8962
9594
|
parser.error("the following arguments are required: host")
|
|
@@ -9031,9 +9663,25 @@ def verify_receipts_command(
|
|
|
9031
9663
|
# under audit. Both halves, because either alone is satisfiable by the wrong thing —
|
|
9032
9664
|
# a digest agreeing with the anchor says these bytes were hashed, and the plan the
|
|
9033
9665
|
# bytes carry says WHICH review they were hashed for.
|
|
9666
|
+
# Whether the plan declared the criterion discipline: read from the rows' rendered
|
|
9667
|
+
# instructions, where the launch snapshotted it — a ReviewPlan/v1 field would be a
|
|
9668
|
+
# schema change restating what the instruction already carries.
|
|
9669
|
+
criterion_active = any(
|
|
9670
|
+
CRITERION_CLAUSE_MARKER in row.instruction
|
|
9671
|
+
for row in (report.base, *report.methods)
|
|
9672
|
+
if row.status != STATUS_DROPPED
|
|
9673
|
+
)
|
|
9674
|
+
criterion_digest: "str | None" = None
|
|
9034
9675
|
if packet_path is not None:
|
|
9035
9676
|
packet_file = pathlib.Path(packet_path).expanduser()
|
|
9036
|
-
|
|
9677
|
+
# ONE byte snapshot: hashing the path and then reopening it as text let the
|
|
9678
|
+
# equality speak for one file state and the plan/criterion extraction for
|
|
9679
|
+
# another (criterion round, #6 — the emit-side twin is #4).
|
|
9680
|
+
try:
|
|
9681
|
+
packet_bytes = packet_file.read_bytes()
|
|
9682
|
+
except OSError as exc:
|
|
9683
|
+
raise LaunchError(f"cannot read the packet {packet_file}: {exc}") from exc
|
|
9684
|
+
digest = hashlib.sha256(packet_bytes).hexdigest()
|
|
9037
9685
|
anchor = bundle.get("packet_sha256") if isinstance(bundle, dict) else None
|
|
9038
9686
|
if digest != anchor:
|
|
9039
9687
|
raise LaunchError(
|
|
@@ -9042,8 +9690,8 @@ def verify_receipts_command(
|
|
|
9042
9690
|
f"these bytes"
|
|
9043
9691
|
)
|
|
9044
9692
|
try:
|
|
9045
|
-
packet_text =
|
|
9046
|
-
except
|
|
9693
|
+
packet_text = packet_bytes.decode("utf-8")
|
|
9694
|
+
except UnicodeDecodeError as exc:
|
|
9047
9695
|
raise LaunchError(f"cannot read the packet {packet_file}: {exc}") from exc
|
|
9048
9696
|
carried = extract_review_plan_v1(packet_text)
|
|
9049
9697
|
if carried is None:
|
|
@@ -9056,6 +9704,16 @@ def verify_receipts_command(
|
|
|
9056
9704
|
f"the supplied packet carries a different {REVIEW_PLAN_SCHEMA} record than "
|
|
9057
9705
|
f"{plan_path}; these receipts evidence a review of another plan"
|
|
9058
9706
|
)
|
|
9707
|
+
if criterion_active:
|
|
9708
|
+
# A plan whose rows carry the discipline clause declared a criterion, and a
|
|
9709
|
+
# packet that omits the record cannot verify: extract_criterion refuses the
|
|
9710
|
+
# absence, the malformed record and the second record each by name. The
|
|
9711
|
+
# digest is a RECOMPILATION from the packet's own bytes — never read off
|
|
9712
|
+
# the receipts, which are the artifacts under audit.
|
|
9713
|
+
document = extract_criterion(packet_text, "the supplied packet")
|
|
9714
|
+
criterion_digest = hashlib.sha256(
|
|
9715
|
+
criterion_schema_bytes(document)
|
|
9716
|
+
).hexdigest()
|
|
9059
9717
|
# The offer that SERVES THE ROW'S HOST, selected by `select_capability_offer` — the
|
|
9060
9718
|
# same function `derive_review_mechanism` seats a method with, rather than a second
|
|
9061
9719
|
# loop written to the same description. The first offer for the operation was taken
|
|
@@ -9110,9 +9768,26 @@ def verify_receipts_command(
|
|
|
9110
9768
|
method, capability, row_host
|
|
9111
9769
|
)["evidence"]
|
|
9112
9770
|
verified, verdicts = verify_review_receipts(
|
|
9113
|
-
report, bundle, required_controls, required_evidence
|
|
9771
|
+
report, bundle, required_controls, required_evidence, criterion_digest
|
|
9114
9772
|
)
|
|
9115
|
-
|
|
9773
|
+
if not criterion_active:
|
|
9774
|
+
criterion_note = ""
|
|
9775
|
+
elif criterion_digest is not None:
|
|
9776
|
+
criterion_note = (
|
|
9777
|
+
" criterion=declared and bound: receipts carrying criterion_schema_sha256 "
|
|
9778
|
+
"were held to the schema recompiled from the packet's record; a receipt "
|
|
9779
|
+
"without it ran prose discipline — steering, not structural enforcement"
|
|
9780
|
+
)
|
|
9781
|
+
else:
|
|
9782
|
+
criterion_note = (
|
|
9783
|
+
" criterion=declared but UNBOUND: no packet was supplied, so the declared "
|
|
9784
|
+
"criterion was recompiled against nothing and no schema digest above was "
|
|
9785
|
+
"adjudicated"
|
|
9786
|
+
)
|
|
9787
|
+
print(render_receipt_verdicts(
|
|
9788
|
+
verified, verdicts, packet_bound=packet_path is not None,
|
|
9789
|
+
criterion_note=criterion_note,
|
|
9790
|
+
))
|
|
9116
9791
|
return 0 if verified.achievement == ACHIEVEMENT_COMPLETE else 1
|
|
9117
9792
|
|
|
9118
9793
|
|
|
@@ -9162,6 +9837,14 @@ def main(argv: list[str]) -> int:
|
|
|
9162
9837
|
return emit_receipt_command(*args.emit_receipt, evidence=args.evidence)
|
|
9163
9838
|
if args.fold_receipts:
|
|
9164
9839
|
return fold_receipts_command(*args.fold_receipts)
|
|
9840
|
+
if args.compile_criterion:
|
|
9841
|
+
return compile_criterion_command(*args.compile_criterion)
|
|
9842
|
+
if args.check_findings:
|
|
9843
|
+
return check_findings_command(*args.check_findings)
|
|
9844
|
+
if args.check_schema_flag:
|
|
9845
|
+
return check_schema_flag_command(
|
|
9846
|
+
args.check_schema_flag, args.config.expanduser()
|
|
9847
|
+
)
|
|
9165
9848
|
if args.check_adapter:
|
|
9166
9849
|
if len(args.check_adapter) < 2:
|
|
9167
9850
|
raise LaunchError("--check-adapter takes a seat and then the adapter command")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"releaseDate": "2026-08-19",
|
|
5
5
|
"description": "A thin, low-level instruction layer for LLM CLI agents: one set of principles and behavior whichever model you run. Deploys into $HOME by copy via an explicit `agent-bios install`.",
|
|
6
6
|
"bin": {
|
package/provenance.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"7d37b87e38e892108e7cd7bd0a397fd30316065c","committedAt":"2026-08-19T11:53:21+09:00","dirty":false}
|