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.
- package/LICENSE +21 -0
- package/README.ko.md +73 -0
- package/README.md +67 -12
- package/dist/bin.d.ts +15 -0
- package/dist/bin.js +138 -0
- package/dist/claude-code-hook.d.ts +47 -0
- package/dist/claude-code-hook.js +233 -0
- package/dist/covenant-check.d.ts +52 -0
- package/dist/covenant-check.js +238 -0
- package/dist/docs/configuration.md +361 -0
- package/dist/docs/installation.md +208 -0
- package/dist/docs/reference/adapter-claude-code.md +82 -0
- package/dist/docs/reference/adapter-git.md +87 -0
- package/dist/docs/reference/core.md +111 -0
- package/dist/docs/reference/covenant.md +100 -0
- package/dist/docs/reference/polydeukes.md +210 -0
- package/dist/docs/troubleshooting.md +152 -0
- package/dist/docs-query.d.ts +46 -0
- package/dist/docs-query.js +138 -0
- package/dist/index.d.ts +23 -4
- package/dist/index.js +22 -4
- package/dist/init-claude-code.d.ts +39 -0
- package/dist/init-claude-code.js +255 -0
- package/dist/load-config.d.ts +42 -0
- package/dist/load-config.js +83 -0
- package/dist/scaffold-project.d.ts +37 -0
- package/dist/scaffold-project.js +123 -0
- package/package.json +29 -10
|
@@ -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.
|