---
name: Elenchus Protocol
slug: elenchus-protocol
category: AI Engineering
description: Elenchus Protocol vets working context before action by challenging claims, checking provenance, and surfacing counterarguments. Use it when a context may be suspect and needs to be resolved into a vetted context.
github: "https://github.com/jongwony/epistemic-protocols/tree/main/elenchus/skills/sublate"
language: JavaScript
stars: 161
forks: 15
install: "npx degit https://github.com/jongwony/epistemic-protocols/tree/main/elenchus/skills/sublate ~/.claude/skills/sublate"
installs_to: ~/.claude/skills/sublate
source_path: elenchus/skills/sublate/SKILL.md
collection_size: 25
category_size: 3670
collection_url: "https://dirskills.com/collections/jongwony/epistemic-protocols"
added: 2026-09-08T05:36:04.079Z
last_synced: 2026-09-08T05:36:04.079Z
canonical_url: "https://dirskills.com/skills/elenchus-protocol"
---

# Elenchus Protocol

Elenchus Protocol vets working context before action by challenging claims, checking provenance, and surfacing counterarguments. Use it when a context may be suspect and needs to be resolved into a vetted context.

**Install:**

```bash
npx degit https://github.com/jongwony/epistemic-protocols/tree/main/elenchus/skills/sublate ~/.claude/skills/sublate
```

## README

# Elenchus Protocol

Vet working context by dialectical antithesis before action through structured per-claim disposition judgment, resolving suspect context into vetted context. Type: `(ContextSuspect, User, VET, WorkingContext) → VettedContext`.

## Definition

**Elenchus** (ἔλεγχος): A dialogical act of cross-examination — from the Socratic refutation tradition meaning "testing by argument" — resolving suspect working context into vetted context through provenance challenge, counterfactual gap forecasting, cross-source consistency check, and inference-fallacy archetype scan before pre-execution sync. The protocol's lexical verb is `/sublate`. Each audit — a source under one claim it is read as authority for — undergoes the motion of stating that claim, surfacing what would shake it, and then deciding what to make of it in light of that challenge (the Hegelian *Aufhebung* — preserve + negate + lift up — supplies the source vocabulary).

```
── FLOW ──
W → identify(W) → S_cand → ∀s ∈ S_cand: bind_kind(s) → k [split where non-atomic] → certify(k, local_claims) → [status(c) = ambiguous: Qa(a, claims_supported(a)) → Stop → Attribution → attribute(a, ·)] → keep(status(c) = pass) → bind_value_space(audit, W.action) → S_high →
  S_high = ∅ ∧ nothing yet posited: emit VettedContext (extension) → deactivate   -- trivial convergence, two causes reported apart: no audit-candidate source at all (Λ.formed = ∅, dispositions empty), or candidates every one of which this protocol handed elsewhere or the user left unattributable (dispositions = the records Phase 0 wrote, by the certificate or by the user's attribution, so NOT empty). Both are first-pass shapes; a loop return that empties S_high leaves through the loop, never here, or the antitheses already put to the user would go out with an empty trace
  Λ.pass = ∅ ∧ something already posited: → integrate(J, Λ.antitheses) → V → LOOP evaluation   -- a loop return this re-pass admitted nothing from: nothing to tag, posit against, or ask, but V is still re-assembled because this pass rewrote J
  Λ.pass ≠ ∅: tag(provenance, freshness, leverage) → S' → posit(antithesis) → A[] →   -- tagging and positing range over Λ.pass, not the standing S_high, so an audit judged in an earlier cycle is not worked again unless its Revisit returned it
  Q(per-claim disposition slots) → J → integrate(J, Λ.antitheses) → V →
  (loop if ∃ a ∈ S_high : J(a.ref).disposition = Judged(_, Revisit(c)) ∧ trigger_met(c))   -- the S_high membership is part of the condition, not a shorthand: a met Revisit is owed a fresh antithesis and a fresh question, and only a member can be given either

── MORPHISM ──
WorkingContext
  → identify(high_leverage_sources, S_cand)    -- silent scan for sources warranting audit
  → bind_kind(source) → [split_binding(binding, source) where atomicity = non-atomic] → certify(kind_binding, local_claims) → bind_value_space(audit, W.action) -- shared meta-backbone: bind each candidate to the CLAIM it is being read as authority for, SPLIT the binding where it bundles more than one claim so certify never sees a compound, certify deficit fit (fail-closed) against the claims inscribed in this SKILL.md, then derive the NARROWING its judgment will be made against — in that strict order, BEFORE any tagging, antithesis, or surfacing. ONLY status(certificate) = pass CONTINUES DOWN THIS CHAIN: route writes a certificate-assigned record straight into the disposition table emit reads; ambiguous is put to the user at Qa, whose attribution admits the candidate, hands it elsewhere, or records it Unattributable — so every step below ranges over the admitted audits alone
  → tag(provenance, freshness, leverage)        -- attach metadata triple per audit
  → posit(antithesis per audit)                 -- Pattern A ∪ Pattern B ∪ Pattern C ∪ Pattern D ∪ Emergent(Pattern)
  → present(antitheses as text, then the reach of a judgment, then the question)  -- per-claim material and a per-batch reach note precede the gate, which carries the question and the instruction list
  → judge(disposition per audit)                 -- over the admitted audits: an open verdict plus an optional instruction, whose members are only what this protocol can itself discharge. The records Phase 0 wrote — certificate- or attribution-assigned — are already in the table and are reported rather than judged
  → emit(VettedContext with disposition table)
  → VettedContext
requires: working_context_pre_execution_committed   -- runtime checkpoint (Phase 0)
deficit:  ContextSuspect                            -- activation precondition (Layer 1); the certificate's own_claim deficit for in-scope audits
preserves: source_chain                              -- W.sources is read-only; binding, antithesis and disposition annotate, never mutate. A split produces several AUDITS over one source, never several sources
invariant: Dialectical Vetting over Silent Trust
invariant: certificate-before-surfacing              -- status(certificate) = pass strictly precedes entry into S_high, and therefore precedes tagging, antithesis positing, and the Phase 2 disposition gate (shared meta-backbone order)

── TYPES ──
W              = WorkingContext { sources: List(Source), action: Prospect }
Prospect       = { intent: String, leans_on: List(Reference) }   -- the pre-execution action the working context was committed against: what is about to be done, and the source references it rests on. READ by bind_value_space, which turns it into the Stake each judgment is made against. Without that read a user is asked what they make of a claim with no statement of what the pending action would do with it, and a claim's standing gets judged in the abstract — which is the one condition under which the judgment cannot be theirs to make
Source         = { content: String, origin: Origin, observed_at: Timestamp, downstream: List(Reference) }
Origin         ∈ {UserStatement, DocumentRead, ToolOutput, AIInference, ExternalAPI, PastSession} ∪ Emergent(Origin)
identify       = WorkingContext → Set(Source)            -- silent selection per Source Identification Criteria
S_cand         = Set(Source) from identify(W)            -- audit CANDIDATES: sources the criteria selected, before any of them is bound to a claim, certified, or admitted
AuditRef       = { source: Source, claim_at_admission: ClaimRef }      -- the audit's STABLE IDENTITY, and the only thing any map is keyed by
               -- both components are fixed when the audit is FORMED and never rewritten, which is what lets a re-triggered audit be re-bound and re-certified without its key moving. Every map below — S', J, V.trace, and the Λ maps — is keyed by this ref; two audits are the same audit exactly when their refs agree, and two siblings from an atomicity split get distinct refs because they were admitted under distinct claims
               -- claim_at_admission IS NOT THE LIVE CLAIM. The claim under test is kind_binding.label, which a loop re-binding may replace when the conditions that fired the trigger moved the claim with them. Keeping the admission claim in the ref is what stops the identity from drifting while the challenge still aims at the claim as it now stands; where the two have come apart, the trace shows both
ClaimAudit     = { ref: AuditRef, kind_binding: KindBinding, certificate: DeficitFitCertificate }
               -- object_ref: the per-audit anchor the certificate evaluates and the value space binds over (elenchus-local instantiation of the shared backbone's object_ref)
               -- IDENTITY VERSUS CONTENTS: ref is the identity, and the two fields after it are contents a loop re-binding and re-certification may replace. Keying anything by the whole record would make re-certifying a re-triggered audit silently change its key and orphan its entry, so the split is load-bearing rather than presentational
               -- THE UNIT OF JUDGMENT IS THE SOURCE UNDER ONE CLAIM, not the source. One source read as authority for two distinct claims is TWO audits, which is what the atomicity split produces and what `Claim-relative provenance` has always required be surfaced rather than carried over silently. A source carrying one claim yields one audit, which is the ordinary case
               -- THE NARROWING IS NOT A FIELD HERE: bind_value_space takes this record as an argument and writes its result to Λ.narrowing, keyed by ref, so this type carries none of it. That keeps the type inhabitable at every point the pipeline reaches it — including for a candidate the certificate routes or leaves unattributable, which never earns a narrowing at all, and for the record as it stands between certify and the Phase 2 slot. A field would demand a value the contract forbids producing until the certificate passes; the narrowing lives in Λ.narrowing instead, keyed by the audit's ref
               -- ASSEMBLED ACROSS THE PHASE 0 PIPELINE: identify selects the source, bind_kind binds the claim it is read as authority for, and certify fits that binding against the local claims — in the order `Admission-time certificate` fixes
               -- The REF is fixed before the record completes, at the point the binding is settled atomic, and that is what Λ.formed holds. So the convergence domain is a set of identities and never a set of records: a candidate can be answered for from the moment its identity exists, which is earlier than the moment it becomes a ClaimAudit this protocol tags, posits against, or surfaces
S_high         = Map(AuditRef, ClaimAudit)                -- the audit-candidate set, KEYED BY THE STABLE IDENTITY like every other collection over audits, with key = value.ref. A set of records would have to remove BY VALUE, and the value moves: a loop re-binding and re-certification replaces the record, so the member a removal clause is looking for is no longer the member that is there. Read a ∈ S_high as membership in its range and r ∈ S_high as membership in its domain — key = value.ref holds by construction, so the two never disagree. Atomic and certificate-passing BY CONSTRUCTION, since nothing else is admitted to it. Cardinality 0 yields trivial convergence ON THE FIRST PASS — which is a SATISFIED terminal rather than a vacuous one, since every candidate that never reached S_high carries a Phase 0 record in J, certificate- or attribution-assigned. On a LOOP RETURN cardinality 0 means something different and takes a different exit: a re-certification can route the last admitted audit out after the user has already judged it, and that run's terminal is the loop's, whose account ranges over dom(Λ.antitheses). Reading cardinality 0 as one condition would send that run to a terminal emitting an empty trace
ClaimRef       = { referent: String, claim_kind: String, scope: String, text: String }
               -- claim_kind = the SEMANTIC CATEGORY of claim the source is tested as authority for (a distinct axis from an evidence-channel kind); values are protocol-local (self-contained, no shared cross-protocol enum). text = claim verbatim, used by Pattern A's cite-claim-verbatim test
               -- carried as KindBinding.label (below), which is what makes the axis a BOUND FIELD rather than something the antithesis inherits from how it happened to be worded. All four components are load-bearing for that: referent and claim_kind fix what the claim is about and in what category, scope fixes how far it reaches, and text pins the wording the challenge must confront
ProvenanceTag  = { claim: ClaimRef, evidence: VerificationPath, confidence: Float }
               -- claim-relative tag: the source's authority is recorded for the claim it authorizes, not for the source in general
VerificationPath ∈ {DirectObserved, InferredFromN, ExternalCited, ProvisionalAssumption}
FreshnessTag   = { age: Duration, horizon: Duration }
               -- currency only: a source can be fresh yet still not track the behavior its claim asserts (it documents intent with nothing enforcing the match). Freshness is necessary but not sufficient for trusting a source; the support-integrity challenge is posited per audit in Pattern A (Source Provenance Audit).
LeverageTag    = { branches: Set(Reference) }
S'             = Map(AuditRef, ProvenanceTag × FreshnessTag × LeverageTag)
               -- keyed by the AUDIT, not the source: ProvenanceTag is claim-relative already (`Claim-relative provenance`), so two audits over one source carry two provenance verdicts. FreshnessTag and LeverageTag are properties of the source and are therefore identical across sibling audits — carried per audit so one read gives the whole triple, not because they differ

-- Shared meta-backbone (KIND dispatch, admission-time). One canonical schema; elenchus-local instantiation ONLY for object_ref (= ClaimAudit), local_value_space (= the per-audit narrowing the judgment is made against, never a set of answers), the label field's type (ClaimRef), the own claim, and the local route claims.
KindBinding    = { label: ClaimRef, positive_predicate: String, evidence: Set(Evidence), atomicity ∈ {atomic, non-atomic} }
                 -- binds the candidate to the CLAIM it is being read as authority for, read off the source's content together with its downstream references. positive_predicate states what makes that claim suspect
                 -- if atomicity = non-atomic (the binding bundles two distinct claims) → split BEFORE certify, and the split is given the SOURCE alongside this binding, since label holds one claim and a further one is readable only there. No compound is admitted to S_high, tagged, posited against, or surfaced
                 -- atomicity IS THE BACKBONE'S QUESTION AND CARRIES THE BACKBONE'S MEANING: how many distinct claims this binding bundles — the same question the sibling protocols sharing this schema ask of it. THE FLAG AND THE FIELD DISAGREE ABOUT WHAT A BINDING CAN HOLD, AND THAT IS A DECLARED RESIDUAL: this flag can say "more than one" while label holds exactly one, so the assertion has nothing here to point at. It costs no admitted claim — split_binding takes the source and each distinct claim gets its own binding, its own ref and its own place in Λ.formed — and what it leaves is the flag naming a multiplicity the record cannot represent. Changing that means changing what atomicity refers to, which is this backbone's own question and therefore a change across every protocol sharing the schema, so it is named here and not made here. It never asks how far a disposition would reach: one claim standing on evidence that lies in several places is ONE audit, because what becomes of the source is what the disposition answers and not what admission decides
Evidence       = { source: String, content: String }   -- observable indicator from W supporting the binding and the certificate's deficit-fit basis
OwnClaim       = { deficit: ContextSuspect, resolution: VettedContext, in_scope_if: String }
                 -- the claim elenchus makes, stated as the WHOLE local morphism: the deficit it takes AND the resolution it produces. An audit is claimed here when its positive_predicate instantiates ContextSuspect AND the local value space can carry it to VettedContext — the bare deficit label is a name, the morphism is the predicate
DeficitFitCertificate = { own_claim: OwnClaim, route_claims: List<RouteClaim>, claimed_by: Set(Deficit), evidence: Set(Evidence), attribution_by ∈ {certificate, user} }
                 -- attribution_by records WHO SETTLED claimed_by: `certificate` where the fit alone settled it, `user` where the fit was ambiguous and the user's Attribution at Qa rewrote claimed_by. Written by attribute and nowhere else; read by DispositionRecord.assigned_by and the closing account
status(c)        = pass iff c.claimed_by = {ContextSuspect}; route iff c.claimed_by = {d} with d a routed_deficit of one of c.route_claims; ambiguous otherwise
                 -- read off claimed_by rather than stored beside it, so no field can disagree with the set that determines it
                 -- fail-closed: status(c) ≠ pass BLOCKS entry into S_high, and therefore blocks tagging, antithesis positing, and the Phase 2 disposition gate. Generated at Phase 0 by fitting KindBinding.positive_predicate against own_claim and every route_claim inscribed below — the certificate reads nothing outside this SKILL.md
                 -- claimed_by collects every claim the evidence supports; a SET, so "no claim holds" is the value ∅ rather than a hole in the type
                 -- status(c) = pass: claimed_by = {ContextSuspect} — the own claim holds alone → admitted to S_high
                 -- status(c) = route: claimed_by = {d} for a single route_claim's routed_deficit d → emit d as the typed handoff, record Handed(d) against the audit, and drop it from admission; it never enters S_high, so it never reaches a disposition slot — but it IS in Λ.formed, so vetted(V) quantifies over it and that record is what discharges it
                 -- status(c) = ambiguous: |claimed_by| ≠ 1 — several claims hold, or none holds on the evidence at hand → Qa: the candidate is put to the user with the claims its evidence supports, and their Attribution settles it — Own (admitted to S_high on their word), Route(d) (Handed(d), attribution-assigned), or Unattributable (recorded against the audit with what they saw as unresolved in basis, reported, and dropped from admission — never admitted, but discharged in the ledger like every other formed audit). ONE gate and no AI re-assessment, because W.sources is read-only (preserves: source_chain) and the detection state cannot have moved: nothing the certificate could re-read would move the fit, and a step whose answer can move with no new evidence behind it would admit or dismiss a candidate arbitrarily. The user's utterance is the one new ground that can arrive, so ambiguity goes to them and not to a second pass — asked once per candidate, and Unattributable reached only on their word
                 -- what a pass certifies is LOCAL ADMISSIBILITY: elenchus's own gate governing elenchus's own activation, not the absence of a claim anywhere in the wider protocol set. Where two protocols' scopes both reach a situation, each protocol's own gate governs
RouteClaim     = (route_if_predicate: String, routed_deficit: Deficit)
                 -- elenchus-local route claims — what a candidate is handed to when it is not a suspect claim in the working context. routed_deficit is the BINDING field; the command in parentheses is a non-binding hint for the user, not the relation this guard composes on:
                 --   a missing pre-execution fact — nothing to vet, something to acquire        → ContextInsufficient (hint: /inquire)
                 --   the claim is not open at all: a convention or ownership question settles it → BoundaryUndefined   (hint: /bound)
Deficit        = a deficit label — a name for the kind of interaction deficit a claim's resolution belongs to. THE TYPE IS OPEN: what may inhabit it is not bounded by this file, because a user's HandOff names where they judge the question belongs and that judgment needs no local inscription to be well-formed. What IS bounded is the CERTIFICATE's range — every label the certificate can assign is inscribed in THIS SKILL.md (elenchus's own ContextSuspect, or one of the sibling deficits named in the route claims above) and nothing outside this file supplies one, the certificate being a check this protocol performs and so one it must be able to run from this file alone
               -- the bound named above reaches the certificate and stops there. A user's HandOff at Qs, or their Route(d) attribution at Qa, names where THEY judge the question belongs, and it travels in the typed field whether or not this file mentions that deficit — a judgment is not a check and owes no local inscription, so nothing coerces it toward an inscribed name. What the bound must not be read as is the range of thi
