Skip to content

@polydeukes/core

The protocol every covenant speaks — the input IR, the verdict shape, the config schema, and the telemetry collector.

Beta. A transitive dependency of the umbrella: you do not install it and you do not import it. The consumer entry point is polydeukes.

The protocol every covenant speaks, and nothing that knows what a covenant is about.

Area What it is
Covenant protocol The stdin-JSON input IR, the verdict shape, the exit-code contract
Config schema defineConfig() validates parsed yaml/json data; the matching JSON Schema ships as a sibling artifact
ROI telemetry One append-only line collector every package writes through
Fail policy One table deciding fail-open against fail-closed per failure kind
Protected-path normalization The declared list becomes the literal strings the dispatcher matches
Transcript seam The query interface a covenant uses to ask about session history

Two constraints hold this package’s shape. Zero runtime dependencies — validation is hand-rolled and the published JSON Schema is a sibling artifact the source never reads. No agent, tool, or language literals — editor tool verbs and test-runner names are values supplied by configs and adapters, so the core’s agent-neutrality is a claim a grep can check. Every other package depends on this one; this one depends on none of them.

The session adapter names it as a peerDependency, not a dependency of its own. It shares the vocabulary with the umbrella rather than installing a copy — SOURCE_KINDS and parseInput have to be one set of values for the validator and the engine to agree, and two copies would disagree silently instead of failing at install time. The umbrella carries the ordinary dependency that satisfies that peer, which is why a consumer still installs one package and gets core transitively.

This is the contract the shipped judges speak: a judge receives a CovenantInput — parsed once from the stdin-JSON payload the surface hands the dispatcher — and answers with a verdict the wrapper translates into an exit code. Blocked rows in .polydeukes/roi.log use this judgment vocabulary.

world is supplied by the surface (files, changes, channels). The judge does not read it from disk. tools and session are supplied by the host: the roster is values the adapter fills in, and the session is the evidence a live agent session proves that a bare IR cannot — message freshness and call outcome. transcriptFromSession wraps that key as the transcript a witness or a precedent consumer reads, the way transcriptFromInput wraps the bare IR without those two facts.

type CovenantInput = {
toolCalls: { name: string; args?: Record<string, unknown>; fileChange?: FileChange }[];
subagentSpawns: { kind: string }[];
userMessages: { text: string }[];
actor?: { agentType?: string };
world?: {
files?: Record<string, string>;
changes?: string[];
channels?: { sidecar?: string };
};
tools?: { mutating: string[]; shell: string[]; commandArgs: string[] };
session?: {
evidencePath?: string;
userMessages: { text: string; timestampMs?: number }[];
toolCalls: { name: string; args?: Record<string, unknown>; succeeded?: boolean }[];
channels?: { sidecar?: string };
};
};
type FileChange =
| { kind: 'create'; path: string; post: string }
| { kind: 'modify'; path: string; pre: string; post: string }
| { kind: 'delete'; path: string; pre?: string };
type CovenantVerdict = { upheld: true } | { upheld: false; reason: string };

The vocabulary carries no tool or agent names. A concrete tool name is a value an adapter fills into name; kind on a spawn is likewise a value. FileChange is a discriminated union so that a deletion is first-class evidence rather than an unrepresentable case, and impossible states — a deletion carrying resulting content, a creation carrying a baseline — cannot be written down. delete.pre is absent when the baseline was a binary blob, because a deletion needs no content to be judged. actor is the observation’s actor as the host envelope proves it — agentType inside a subagent, {} in the main session — and is absent when the surface proves none; the judge never defaults it.

Evidence has exactly one home: the call it belongs to. fileChange absent means this call is unproven, and no sibling call’s evidence stands in for it.

function parseInput(stdinJson: string):
| { ok: true; value: CovenantInput }
| { ok: false; exitCode: 2 };
function verdictToExitCode(verdict: CovenantVerdict): 0 | 1;
function allFileChanges(input: CovenantInput): FileChange[];

parseInput never throws. Unparseable JSON, an empty payload, a non-object, a missing required collection — each resolves to a blocking { ok: false, exitCode: 2 }, so an unjudgeable input can never be mistaken for a valid one.

verdictToExitCode returns 0 or 1 and never 2. Translating a break into a block is the wrapper’s policy, not the body’s — see exit codes. The exported constants are EXIT_UPHOLD (0), EXIT_BREAK_NON_BLOCKING (1), and EXIT_BREAK_BLOCKING (2).

allFileChanges flattens every call’s evidence in call order for consumers that need no attribution. Calls without evidence are skipped, never substituted for.

The three discipline lists and the channel derivation

Section titled “The three discipline lists and the channel derivation”

defineConfig() accepts three discipline lists and decides, for each entry, whether it is written in the right one.

Key Type Judged on
disciplines (DisciplineEntry | DisciplineDraft)[] both surfaces
sessionDisciplines DisciplineEntry[] the session surface only
changeSetDisciplines DisciplineEntry[] the change-set surface only

ResolvedConfig carries the three under the same names, holding judged entries only, with the drafts split out into drafts. A list the input did not declare stays absent rather than becoming an empty array.

The derivation both the validator and the umbrella use is one exported function:

type DeclarationChannel = 'transcript' | 'channel' | 'command' | 'actor' | 'changes';
function declarationChannels(body: Omit<AlgebraDeclaration, 'discipline'>): DeclarationChannel[];

It is pure and syntactic — the declaration is never run. transcript comes from a { transcript: true } binding, channel from a { sidecar: true } binding, command from scope.source === 'command' or a { op: 'source', of: 'command' } step, actor from a { op: 'source', of: 'actor' } step, and changes from a { op: 'source', of: 'changes' } step. A witness block’s own extract is walked with the body’s. The returned list is in the fixed order above, so two callers comparing channel sets compare the same list.

A body naming none of the five reads the changed file and repository files alone, which both surfaces supply, so it belongs in disciplines. Anything else is a ConfigValidationError naming the entry, its channels, and the list it belongs in; the messages are in the configuration reference. The umbrella uses this function to select the applicable discipline list.

Three places, all of them indirect.

  • The config file. Its schema is defined here. The vocabulary reference is the configuration reference.
  • The JSON Schema artifact@polydeukes/core/schema.json, an exports subpath, for a project that installs this package directly. A consumer of the umbrella names the copy bundled there instead; both spellings are in configuration.md’s IDE section.
  • The protocol above — reading a blocked row means reading the vocabulary a body answered in.

Everything else here is reached through polydeukes.

  • Adapter namespaces are validated by shape, not by name. defineConfig() checks that adapters is a map of plain objects and that each namespace value is an object. It does not check that a namespace name is one anybody implements, and it does not look inside the namespace at all. Unknown vocabulary inside a namespace is rejected by that adapter’s own validator, at its own layer — not here.
  • The default transcript is a noop. A consumer that injects no real transcript converges on “nothing happened”, which is the safe direction for a valve: it never opens. Real transcripts live behind adapters.
  • Telemetry is fail-open, alone. A logging failure never changes a verdict. Every other failure kind in the table resolves toward blocking.