polydeukes 0.0.1 → 0.3.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.
@@ -0,0 +1,208 @@
1
+ # Installing Polydeukes
2
+
3
+ **English** · [한국어](./installation.ko.md)
4
+
5
+ > Alpha. This guide covers the install paths that ship today, and everything here is the
6
+ > measured behaviour of the published packages.
7
+
8
+ One devDependency, one command per surface. The umbrella package `polydeukes` is the only
9
+ thing you install — it carries the core, the judge, and the adapters as its own
10
+ dependencies, and `pdks` is its CLI (an alias of `polydeukes`).
11
+
12
+ **Two surfaces ship, for two different situations — pick the one that matches how the
13
+ project is developed.** A project built alongside an AI partner in Claude Code wires the
14
+ **session surface**: a PreToolUse hook that judges every editing tool call and shell
15
+ command as it is declared. A project you develop yourself wires the **commit surface**: a
16
+ pre-commit hook that judges the staged diff, so the discipline you declared for yourself
17
+ is applied at the moment work becomes history. They enforce the same config vocabulary,
18
+ but they answer different situations — there is no general reason to wire both in one
19
+ project.
20
+
21
+ ## Prerequisites
22
+
23
+ - **Node.js ≥ 24** — the engines floor of every published package.
24
+ - **A package manager** — pnpm and npm both work; examples below use pnpm.
25
+ - **Claude Code** — only for the session surface. The commit surface needs no AI tool at
26
+ all: just git and a way to run a pre-commit hook.
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ pnpm add -D polydeukes
32
+ ```
33
+
34
+ (or `npm install --save-dev polydeukes`.)
35
+
36
+ This must be a real project dependency, not a one-off `npx` run — both surfaces load the
37
+ judge from your project's own installed package.
38
+
39
+ ## The session surface — developing with an AI partner
40
+
41
+ From the project root:
42
+
43
+ ```sh
44
+ pnpm exec pdks init claude-code
45
+ ```
46
+
47
+ The command installs into the directory it is invoked from, and it proves the `polydeukes`
48
+ package resolves there **before writing anything** — if it does not (say, the install step
49
+ was skipped), it prints the install command and exits 2 with zero files written, never a
50
+ half-wired tree.
51
+
52
+ Five artifacts, none ever overwritten. What exists is reported and kept — the hook, the
53
+ config, and the discipline file are left alone, the settings file is merged, and
54
+ `.gitignore` is only ever appended to — so re-running is always safe:
55
+
56
+ | Artifact | What it is |
57
+ |---|---|
58
+ | `.claude/hooks/covenant-pretooluse.mjs` | The hook — a thin delegator that loads the judge from the installed package. Upgrading the package upgrades the judge; this file never changes. |
59
+ | `.claude/settings.json` | The PreToolUse registration for editing tools and shell calls. **Merged, never replaced** — your other hooks and permissions stay. |
60
+ | `polydeukes.config.yaml` | The starter protection policy: a placeholder `languages` block, a minimum `protectedPaths` list, and the witness block. The comments in the file explain why each entry is there. |
61
+ | `.claude/rules/polydeukes.md` | A scoped discipline file telling your AI partner that `pdks docs` exists and which topic answers what. It carries `paths` frontmatter, so it loads when a Polydeukes path is in play rather than sitting in every session's context. |
62
+ | `.gitignore` | An appended ignore rule for `.polydeukes/`, with its comment line — telemetry is local observation data and never belongs in history. |
63
+
64
+ ## First edit — `languages`
65
+
66
+ The generated config ships a placeholder language profile, because the installer cannot
67
+ know your stack:
68
+
69
+ ```yaml
70
+ languages:
71
+ placeholder:
72
+ productionGlob: 'src/**'
73
+ testCmd: 'echo "set a verification command for {scope}"'
74
+ ```
75
+
76
+ Rename the key to your language, point `productionGlob` at your production sources, and put
77
+ your real verification command in `testCmd`. (On the commit-surface path you write this
78
+ block yourself as part of the config below.) The placeholder is valid as generated and no
79
+ judgment path reads these values yet, so it cannot produce a wrong verdict while it waits —
80
+ but `languages` is the schema's one required block, so *removing* it (or emptying it) makes
81
+ the config invalid, and an invalid config blocks every call. Edit it, don't delete it.
82
+
83
+ ## The commit surface — developing by yourself
84
+
85
+ This path is for applying your own discipline to your own commits — no AI tool involved.
86
+ It has no installer today; the wiring is two small manual steps.
87
+
88
+ **First, the config.** Create `polydeukes.config.yaml` at the project root (there is no
89
+ generator on this path — the file is yours from the first line):
90
+
91
+ ```yaml
92
+ languages:
93
+ typescript:
94
+ productionGlob: 'src/**'
95
+ testCmd: 'pnpm test'
96
+
97
+ # Judged at commit time: a staged change to these paths stops the commit
98
+ # until you answer the witness prompt in person.
99
+ protectedPaths:
100
+ - 'db/migrations'
101
+
102
+ witness:
103
+ token: 'pdks witness'
104
+ ttlMinutes: 10
105
+ ```
106
+
107
+ Add `.polydeukes/` to your `.gitignore` too — telemetry is local observation data.
108
+
109
+ **Then, the hook.** One command judges what is currently staged and exits 2 on a broken
110
+ covenant:
111
+
112
+ ```sh
113
+ pnpm exec pdks covenant check
114
+ ```
115
+
116
+ Register it as a pre-commit hook. With **lefthook**:
117
+
118
+ ```yaml
119
+ # lefthook.yml
120
+ pre-commit:
121
+ commands:
122
+ covenant:
123
+ priority: 1
124
+ interactive: true # keep the witness prompt visible — see below
125
+ run: ./node_modules/.bin/pdks covenant check
126
+ ```
127
+
128
+ With **husky**:
129
+
130
+ ```sh
131
+ # .husky/pre-commit
132
+ ./node_modules/.bin/pdks covenant check
133
+ ```
134
+
135
+ With plain **`.git/hooks`** (make it executable):
136
+
137
+ ```sh
138
+ #!/bin/sh
139
+ # .git/hooks/pre-commit
140
+ ./node_modules/.bin/pdks covenant check
141
+ ```
142
+
143
+ Three things to know about this surface:
144
+
145
+ - **The valve is a TTY prompt.** At the default `block` level, a commit that stages a
146
+ protected change stops at a prompt only a human at a terminal can answer. Configure your
147
+ hook runner so it does not swallow that prompt (lefthook needs `interactive: true`).
148
+ - **Two discipline families judge here.** A staged diff carries file changes and nothing
149
+ else, so protection lists and the delta and path families (`forbid`, `immutable`) judge
150
+ in full. A command-family entry (`forbidCommand`) has no command line to read in a
151
+ staged diff and is not assembled on this surface, and a context-family entry
152
+ (`requirePrecedent`) is recorded as `skipped` — declare those two where an AI partner's
153
+ session exists to be judged.
154
+ - **The commit surface has its own additive scope.** Paths that are fine to edit freely
155
+ but whose promotion into history deserves a judged checkpoint go under the adapter
156
+ namespace, judged on top of the shared list:
157
+
158
+ ```yaml
159
+ adapters:
160
+ git:
161
+ protectedPaths:
162
+ - 'src/policy'
163
+ ```
164
+
165
+ ## The witness valve
166
+
167
+ Both surfaces carry the same valve, spelled for their situation. It sits **after** the
168
+ verdict — only a judgment that actually blocked can be witnessed open — and every allowance
169
+ is recorded as `witnessed`, never silent.
170
+
171
+ ```yaml
172
+ witness:
173
+ token: 'pdks witness'
174
+ ttlMinutes: 10
175
+ ```
176
+
177
+ - **Session surface:** a human types the token so it stands alone on the first line of a
178
+ conversation message; the window holds for `ttlMinutes`, then blocking resumes. An agent
179
+ cannot open the valve for itself — only human-authored messages count.
180
+ - **Commit surface:** the blocked commit shows a TTY prompt, and typing the full token
181
+ there opens that one commit.
182
+
183
+ Change the token and window as you like — the token is not a secret; the defence is
184
+ provenance, not confidentiality. **Keep the block**: on the session surface the generated
185
+ protection list covers `.claude/hooks`, so without a valve the first blocked call would
186
+ freeze the project until a human edits the config from their own terminal.
187
+
188
+ ## Prove the gate is live
189
+
190
+ Prove it once on the surface you wired, then read the telemetry.
191
+
192
+ - **Session surface:** ask your agent to append a line to
193
+ `.claude/hooks/covenant-pretooluse.mjs` (a protected path). The call must come back
194
+ blocked.
195
+ - **Commit surface:** stage an edit to a path on your protection list and run
196
+ `git commit`. It must stop at the witness prompt (answer it, or abort with Ctrl-C).
197
+
198
+ ```sh
199
+ cat .polydeukes/roi.log
200
+ ```
201
+
202
+ Every judgment appends exactly one record — `passed`, `blocked`, `witnessed`, `advised`, or
203
+ `skipped` — so the block you just caused is the last line. A gate you have watched block
204
+ once is a gate you know is wired.
205
+
206
+ From here: [the configuration reference](./configuration.md) for every field and for
207
+ writing your own disciplines, and [troubleshooting](./troubleshooting.md) when something
208
+ blocks and you don't know why.
@@ -0,0 +1,82 @@
1
+ # `@polydeukes/adapter-claude-code`
2
+
3
+ **English** · [한국어](./adapter-claude-code.ko.md)
4
+
5
+ > **The session surface's translator** — PreToolUse payloads become the covenant input IR,
6
+ > with the file-change evidence and the transcript channel the judge reads.
7
+ >
8
+ > Alpha. A transitive dependency of the umbrella: you do not install it and you do not
9
+ > import it. The session surface reaches it through
10
+ > [`polydeukes/claude-code`](./polydeukes.md#subpaths).
11
+
12
+ ## What this package owns
13
+
14
+ The boundary where Claude Code's vocabulary is translated away. Agent and tool literals
15
+ live *here* by design, so that they never reach the core — which is what makes the core's
16
+ agent-neutrality a claim a test can check rather than a slogan.
17
+
18
+ | Unit | What it does |
19
+ |---|---|
20
+ | Payload up-translation | A raw PreToolUse payload becomes a `CovenantInput` |
21
+ | Virtual post-state | Computes what a file *would* contain after an edit applies, without touching disk |
22
+ | File-change evidence | Pairs the disk pre-state with the virtual post-state into union evidence |
23
+ | Transcript provider | Turns a session JSONL file into a `CanonicalTranscript` |
24
+ | Precedent evaluator | This adapter's own evidence vocabulary for the context family |
25
+ | Telemetry wiring | Drives the full funnel so exactly one row lands per call |
26
+
27
+ This package never imports the covenant package. The dispatch seam is *injected* by the
28
+ umbrella, which keeps dependencies one-way, through the core alone.
29
+
30
+ ## Payload translation and the three axes
31
+
32
+ **Three axes reach the judge**, and they differ in what evidence they can carry.
33
+
34
+ | Axis | Carries | Consequence |
35
+ |---|---|---|
36
+ | Tool | A proven `fileChange` — the mutation target computed before the tool runs | Only the proven target is judged. A protected path inside an edit's *content* is a mention and passes |
37
+ | Shell | A command line whose target is often not computable before execution | Computable writes are judged like an edit; the rest is recorded rather than guessed |
38
+ | Transcript | The session's own record | Judged by whole-path equality, never as a protected ancestor |
39
+
40
+ Translation is fail-closed at every step. A `Task` call carrying a subagent type maps to a
41
+ spawn; a payload that cannot be classified is a translation *failure* that logs one
42
+ `blocked` record and exits `2`, rather than degrading into a guess.
43
+
44
+ **Evidence is computed, never read back.** The virtual post-state applies `Edit`, `Write`,
45
+ and `MultiEdit` in memory — sequential multi-edit application included — so a content-aware
46
+ discipline judges the *proposed* result rather than the file as it currently is. An
47
+ unresolvable post-state yields no evidence at all, because the real tool would reject the
48
+ same edit, and evidence is never fabricated for a non-mutating call.
49
+
50
+ **The transcript admits only positively-identified human messages.** That is what makes the
51
+ witness valve human-only: an AI cannot synthesize its own witness. A read failure answers
52
+ `undefined` rather than an empty transcript — an empty session has said nothing yet and is
53
+ judged, an unreadable one is no evidence channel at all and is skipped. Either way the
54
+ valve turns off, never open.
55
+
56
+ **The precedent evaluator judges two keys.** `subagent` is exact spawn-kind equality, since
57
+ a kind is a value rather than a pattern; `tool` matches observed tool names as a regular
58
+ expression. Any key outside this vocabulary returns `undefined` — the handshake that tells
59
+ the compiler the evidence is unjudgeable, so the entry skips instead of judging on a guess.
60
+
61
+ ## Where the consumer touches it
62
+
63
+ - **The generated hook**, which loads this adapter through the umbrella's `claude-code`
64
+ subpath. Upgrading the package upgrades what runs; the hook file itself never changes.
65
+ - **`requirePrecedent` entries** using the `subagent` or `tool` evidence keys.
66
+
67
+ No import, and no configuration namespace of its own.
68
+
69
+ ## Declared limits
70
+
71
+ - **A child process's writes are outside observation.** This surface judges *declared tool
72
+ calls*. A command that spawns a process which then writes files — a test runner, a build
73
+ — is judged on the command, not on what the child did. The commit surface is the second
74
+ observation that covers the same ground for tracked files.
75
+ - **Evidence exists only where a post-state can be computed.** All four mutating tools
76
+ contribute one, notebooks included — a `NotebookEdit` yields cell-level `modify` evidence.
77
+ What yields nothing is a payload this adapter cannot resolve: an unreadable or unparseable
78
+ notebook, a cell it cannot name, an edit mode it does not know.
79
+ - **An evidence-free call falls back to the conservative judgment** — the call's arguments
80
+ are compared for a mention rather than a proven target.
81
+ - **Out-of-repository ancestors stay out of scope.** A path above the project root is not
82
+ observed here; the agent's own deny policy owns that ground.
@@ -0,0 +1,87 @@
1
+ # `@polydeukes/adapter-git`
2
+
3
+ **English** · [한국어](./adapter-git.ko.md)
4
+
5
+ > **The commit surface's translator** — a staged diff becomes the covenant input IR, and
6
+ > the `adapters.git` config namespace is defined here.
7
+ >
8
+ > Alpha. A transitive dependency of the umbrella: you do not install it and you do not
9
+ > import it. The commit surface reaches it through
10
+ > [`pdks covenant check`](./polydeukes.md#pdks-covenant-check).
11
+
12
+ ## What this package owns
13
+
14
+ The boundary where git's vocabulary is translated away. A staged diff becomes the same
15
+ agent-neutral input IR the session surface produces — the same judgment for every hand,
16
+ AI or human.
17
+
18
+ | Unit | What it does |
19
+ |---|---|
20
+ | Staged-change collection | Reads the staging area into a list of changes with their content baselines |
21
+ | Pure translation | Folds those changes into one `CovenantInput` |
22
+ | Settings vocabulary | Validates this adapter's own config namespace |
23
+
24
+ This is a pure library. It knows the staged-diff shape and nothing about installation, hook
25
+ runners, or valves — wiring it into a pre-commit hook is a deployment act that lives in the
26
+ umbrella.
27
+
28
+ ## Staged collection and the `adapters.git` namespace
29
+
30
+ **Collection is deliberately narrow about what it trusts.**
31
+
32
+ | Decision | Why |
33
+ |---|---|
34
+ | `--no-renames` forced on | A rename is judged as a deletion plus an addition. A `git mv` of a protected file must not slip through as one opaque rename entry |
35
+ | `pre` from the HEAD blob, `post` from the **staged** blob | Never the worktree, which may have diverged after `git add` |
36
+ | A binary blob yields null content | Rather than lossily decoded bytes |
37
+ | The unborn first commit narrows to all-added | Rather than throwing |
38
+
39
+ Translation produces one tool call per change, under the adapter-owned names `staged-write`
40
+ and `staged-delete`. A deletion always carries its evidence. A write carries it unless the
41
+ staged blob was binary — there is no text to compare, so the call arrives with no
42
+ `fileChange` at all and is judged on its path alone, the same as any unproven call.
43
+ **The session collections are honestly empty** — the commit surface has no session, and a
44
+ key is never fabricated to look like one.
45
+
46
+ **The namespace is this adapter's own vocabulary.** The core validates only the container
47
+ shape — one settings object per adapter — and passes the contents through verbatim, so the
48
+ vocabulary, its validator, and its defaults all live here.
49
+
50
+ | Key | Values | Default | Means |
51
+ |---|---|---|---|
52
+ | `adapters.git.enforce` | `block` \| `advise` | `block` | What a commit-surface verdict *does* |
53
+ | `adapters.git.protectedPaths` | string[] | `[]` | The commit surface's **additive** protection scope, judged on top of the common list |
54
+
55
+ An unknown key, an `enforce` outside the two values, or a `protectedPaths` that is not an
56
+ array of strings each fail fast with the full field path. The writing reference is
57
+ [configuration.md's `adapters` section](../configuration.md#adapters-optional).
58
+
59
+ **The additive scope is additive for a reason.** The level belongs to the observer, and so
60
+ does the scope: entries listed here are judged when work becomes history, and the session
61
+ surface never reads them. That is what lets a repository leave judge *sources* editable in
62
+ a session while still stopping the commit that promotes them.
63
+
64
+ Under `enforce: advise` the valve is structurally absent: a verdict is recorded as
65
+ `advised`, one advisory line lands on stderr, and the commit proceeds. Only the verdict is
66
+ relaxed — a run that cannot judge still fails closed at exit `2` at either level.
67
+
68
+ ## Where the consumer touches it
69
+
70
+ - **The `adapters.git` block** in your config.
71
+ - **The pre-commit hook** that runs `pdks covenant check`, wired by hand — the manual
72
+ procedure for three hook managers is in
73
+ [installation](../installation.md#the-commit-surface--developing-by-yourself).
74
+
75
+ No import.
76
+
77
+ ## Declared limits
78
+
79
+ - **The context family cannot be judged here.** `requirePrecedent` needs session history
80
+ and a commit has none, so a matching entry records `skipped`. A permanent condition of
81
+ this surface, not a fault in the entry.
82
+ - **A commit never shows a gitignored file.** Anything outside version control — a built
83
+ `dist`, a generated hook script — is invisible to this surface by nature. That is why the
84
+ session surface carries those paths on the common list instead.
85
+ - **The valve needs a human at a terminal.** No TTY means no prompt and no way through: a
86
+ CI run and an agent-spawned `git commit` reach the same closed door. Nothing is ever
87
+ persisted, so one answer never covers a later commit.
@@ -0,0 +1,111 @@
1
+ # `@polydeukes/core`
2
+
3
+ **English** · [한국어](./core.ko.md)
4
+
5
+ > **The protocol every covenant speaks** — the input IR, the verdict shape, the config
6
+ > schema, and the telemetry collector.
7
+ >
8
+ > Alpha. A transitive dependency of the umbrella: you do not install it and you do not import
9
+ > it. The consumer entry point is [`polydeukes`](./polydeukes.md).
10
+
11
+ ## What this package owns
12
+
13
+ The protocol every covenant speaks, and nothing that knows what a covenant is *about*.
14
+
15
+ | Area | What it is |
16
+ |---|---|
17
+ | Covenant protocol | The stdin-JSON input IR, the verdict shape, the exit-code contract |
18
+ | Config schema | `defineConfig()` validates parsed yaml/json data; the matching JSON Schema ships as a sibling artifact |
19
+ | ROI telemetry | One append-only line collector every package writes through |
20
+ | Fail policy | One table deciding fail-open against fail-closed per failure kind |
21
+ | Protected-path normalization | The declared list becomes the literal strings the dispatcher matches |
22
+ | Transcript seam | The query interface a covenant uses to ask about session history |
23
+
24
+ Two constraints hold this package's shape. **Zero runtime dependencies** — validation is
25
+ hand-rolled and the published JSON Schema is a sibling artifact the source never reads.
26
+ **No agent, tool, or language literals** — editor tool verbs and test-runner names are
27
+ *values* supplied by configs and adapters, so the core's agent-neutrality is a claim a grep
28
+ can check. Every other package depends on this one; this one depends on none of them.
29
+
30
+ ## The judged protocol
31
+
32
+ This is the contract the shipped judge bodies speak: a body reads a `CovenantInput` from
33
+ stdin and answers with an exit code. Every row in `.polydeukes/roi.log` traces back to one
34
+ of these verdicts, so this vocabulary is what a blocked row is written in.
35
+
36
+ ```ts
37
+ type CovenantInput = {
38
+ toolCalls: { name: string; args?: Record<string, unknown>; fileChange?: FileChange }[];
39
+ subagentSpawns: { kind: string }[];
40
+ userMessages: { text: string }[];
41
+ };
42
+
43
+ type FileChange =
44
+ | { kind: 'create'; path: string; post: string }
45
+ | { kind: 'modify'; path: string; pre: string; post: string }
46
+ | { kind: 'delete'; path: string; pre?: string };
47
+
48
+ type CovenantVerdict = { upheld: true } | { upheld: false; reason: string };
49
+ ```
50
+
51
+ The vocabulary carries no tool or agent names. A concrete tool name is a *value* an adapter
52
+ fills into `name`; `kind` on a spawn is likewise a value. `FileChange` is a discriminated
53
+ union so that a deletion is first-class evidence rather than an unrepresentable case, and
54
+ impossible states — a deletion carrying resulting content, a creation carrying a baseline —
55
+ cannot be written down. `delete.pre` is absent when the baseline was a binary blob, because
56
+ a deletion needs no content to be judged.
57
+
58
+ **Evidence has exactly one home: the call it belongs to.** `fileChange` absent means *this
59
+ call is unproven*, and no sibling call's evidence stands in for it.
60
+
61
+ ```ts
62
+ function parseInput(stdinJson: string):
63
+ | { ok: true; value: CovenantInput }
64
+ | { ok: false; exitCode: 2 };
65
+
66
+ function verdictToExitCode(verdict: CovenantVerdict): 0 | 1;
67
+
68
+ function allFileChanges(input: CovenantInput): FileChange[];
69
+ ```
70
+
71
+ `parseInput` never throws. Unparseable JSON, an empty payload, a non-object, a missing
72
+ required collection — each resolves to a blocking `{ ok: false, exitCode: 2 }`, so an
73
+ unjudgeable input can never be mistaken for a valid one.
74
+
75
+ `verdictToExitCode` returns `0` or `1` and never `2`. Translating a break into a block is
76
+ the wrapper's policy, not the body's — see [exit codes](./polydeukes.md#exit-codes).
77
+
78
+ `allFileChanges` flattens every call's evidence in call order for consumers that need no
79
+ attribution. Calls without evidence are skipped, never substituted for.
80
+
81
+ ## Where the consumer touches it
82
+
83
+ Three places, all of them indirect.
84
+
85
+ - **The config file.** Its schema is defined here. The vocabulary reference is
86
+ [configuration.md](../configuration.md).
87
+ - **The JSON Schema artifact** — `@polydeukes/core/schema.json`, an exports subpath. Under
88
+ a default pnpm layout it does not resolve from a consumer's top-level `node_modules`,
89
+ because the core arrives as a transitive dependency; the working spellings are in
90
+ [configuration.md's IDE section](../configuration.md#ide-support).
91
+ - **The protocol above** — reading a `blocked` row means reading the vocabulary a body
92
+ answered in.
93
+
94
+ Everything else here is reached through `polydeukes`.
95
+
96
+ ## Declared limits
97
+
98
+ - **Adapter namespaces are validated by shape, not by name.** `defineConfig()` checks that
99
+ `adapters` is a map of plain objects and that each namespace value is an object. It does
100
+ not check that a namespace *name* is one anybody implements, and it does not look inside
101
+ the namespace at all. Unknown vocabulary inside `adapters.git` is rejected by the git
102
+ adapter's own validator, at its own layer — not here.
103
+ - **`requirePrecedent` evidence is layered the same way.** The core fully validates the
104
+ `command` key, because a shell command is the surface where an agent crosses into the
105
+ system. Every other key is validated for container shape alone — a flat object holding
106
+ exactly one key — and its value passes through verbatim for the owning adapter to judge.
107
+ - **The default transcript is a noop.** A consumer that injects no real transcript
108
+ converges on "nothing happened", which is the safe direction for a valve: it never opens.
109
+ Real transcripts live behind adapters.
110
+ - **Telemetry is fail-open, alone.** A logging failure never changes a verdict. Every other
111
+ failure kind in the table resolves toward blocking.
@@ -0,0 +1,100 @@
1
+ # `@polydeukes/covenant`
2
+
3
+ **English** · [한국어](./covenant.ko.md)
4
+
5
+ > **The judge** — the dispatcher, the discipline library, the meta-covenants, and the TTL
6
+ > witness valve.
7
+ >
8
+ > Alpha. A transitive dependency of the umbrella: you do not install it and you do not
9
+ > import it. What you reach is its behaviour, through the `disciplines:` block in your config
10
+ > and the rows it writes to `.polydeukes/roi.log`.
11
+
12
+ ## What this package owns
13
+
14
+ The judge. Everything that turns a declared promise into a verdict lives here.
15
+
16
+ | Unit | What it does |
17
+ |---|---|
18
+ | `runCovenant` wrapper | Runs a judge body, translates its non-blocking `1` into the blocking `2`, and logs every call. No covenant runs unmeasured |
19
+ | Path-routing dispatcher | Registers covenants against protected paths and runs *every* matching one — no short-circuit, so the telemetry never under-counts |
20
+ | Meta-covenants | Three registrations that protect the judging chain itself |
21
+ | TTL witness | The time-boxed human valve, consulted only after a verdict blocked |
22
+ | Delta layer | New-violation-only judgment over a file's before/after pair |
23
+ | Discipline library | Config `disciplines:` entries become enforcement without a line of code |
24
+
25
+ ## Discipline families and meta-covenants
26
+
27
+ **A `disciplines:` entry belongs to exactly one family**, decided by which predicate key it
28
+ carries. The family determines what evidence the judgment needs — which is also what
29
+ determines whether it can be judged on a given surface.
30
+
31
+ | Family | Key | Judges | Evidence needed |
32
+ |---|---|---|---|
33
+ | delta | `forbid` | Added-direction content of a file change. Existing debt is forgiven; only new occurrences break | File change |
34
+ | path | `immutable` | Any change to a file that already exists — modification or deletion alike. Creation passes | File change |
35
+ | command | `forbidCommand` | The command line itself | None |
36
+ | context | `requirePrecedent` | Session history — was a qualifying call actually executed *before* this one | A transcript |
37
+
38
+ `when` is a trigger, not a family: it narrows a `requirePrecedent` entry and combines with
39
+ nothing else. The writing guide for these entries — the four predicate forms, the two
40
+ pitfalls — is [configuration.md's `disciplines` section](../configuration.md#disciplines-optional).
41
+
42
+ **Three meta-covenants** protect the judging chain. They are covenants like any other; the
43
+ vocabulary below applies to them unchanged.
44
+
45
+ | Registration | Axis | Judges |
46
+ |---|---|---|
47
+ | self-mod | Tool | Mutations to protected paths through editing tools. Only the call's proven mutation target is compared — a protected path inside an edit's *content* is a mention and passes |
48
+ | shell-mod | Shell | The same, through a command line. A command mentioning a protected path passes only if its leading word proves it read-only |
49
+ | transcript-mod | Transcript | Writes to the live session transcript, judged by whole-path **equality** — never as a protected ancestor |
50
+
51
+ **Five verdict words** are the telemetry contract. A row in `.polydeukes/roi.log` carries
52
+ exactly one of them, and the CLI, the docs, and the tests use the same word for the same
53
+ event. How to read a row is in
54
+ [troubleshooting](../troubleshooting.md#reading-a-verdict).
55
+
56
+ | Verdict | Means |
57
+ |---|---|
58
+ | `passed` | The call was judged and upheld the covenant |
59
+ | `blocked` | The call was judged and broke it |
60
+ | `witnessed` | A **blocked** verdict a human opened in person. Never silent, never a clean call |
61
+ | `advised` | The commit surface at `enforce: advise` recorded a break without stopping it |
62
+ | `skipped` | The call reached a registration that could not judge it. **Not a pass** — the recorded absence of a judgment |
63
+
64
+ ## Where the consumer touches it
65
+
66
+ - **The `disciplines:` block** in your config. One entry compiles into one registration,
67
+ carrying its own telemetry label.
68
+ - **`protectedPaths`**, which the path-routing dispatcher matches against.
69
+ - **The `witness` block**, which arms the TTL valve.
70
+ - **`.polydeukes/roi.log`**, where every judgment lands as one row.
71
+
72
+ No import. The umbrella assembles this package for both surfaces.
73
+
74
+ ## Declared limits
75
+
76
+ - **The shell axis leaves `skipped` rows, and that row is the contract.** Predicting a
77
+ shell command's target from its text is undecidable, so the invariant this axis holds is
78
+ not "nothing gets through" — it is that **no call passes unrecorded**. A new spelling
79
+ landing in `skipped` is the declared limit showing itself. A pass with no row at all, or
80
+ one recorded `passed` without a judgment, is the defect class.
81
+ - **The context family cannot be judged without a session.** On the commit surface there is
82
+ none, so a matching `requirePrecedent` entry always records `skipped`. That is a permanent
83
+ condition of that surface.
84
+ - **The command family is absent from the commit surface, and absent without a row.**
85
+ `forbidCommand` entries are filtered out before compilation there, since a staged diff
86
+ carries no command line to judge. Unlike the context family this leaves nothing in
87
+ `.polydeukes/roi.log`, so the log cannot separate a command discipline that never
88
+ triggered from one that was never registered on that surface.
89
+ - **An unjudgeable entry compiles to a skip registration** — routing intact, no body. A
90
+ pattern that does not compile skips the same way. Assembly therefore never throws: one
91
+ unresolvable entry cannot take down its siblings, the meta-covenants, and the valve,
92
+ which would leave no way to fix the config that caused it.
93
+ - **Complete containment is a non-goal.** There are no blocklists here — enumerating bypass
94
+ spellings is always one step behind, so the logic is inverted: a mention of a protected
95
+ path blocks unless proven safe. Residual vectors such as indirect path computation are
96
+ telemetry targets, not block targets. The two friction valves — the read-only allowlist
97
+ and the TTL witness — both leave a measurable trace.
98
+ - **The valve stands after the verdict.** Only a judgment that actually blocked can be
99
+ witnessed open, a mid-sentence mention of the token does not arm it, and an AI can never
100
+ open the valve for itself.