polydeukes 0.0.1 → 0.4.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 +56 -0
- package/dist/claude-code-hook.js +360 -0
- package/dist/covenant-check.d.ts +58 -0
- package/dist/covenant-check.js +262 -0
- package/dist/docs/configuration.md +95 -0
- package/dist/docs/installation.md +211 -0
- package/dist/docs/reference/adapter-claude-code.md +82 -0
- package/dist/docs/reference/adapter-git.md +87 -0
- package/dist/docs/reference/configuration.md +294 -0
- package/dist/docs/reference/core.md +111 -0
- package/dist/docs/reference/covenant.md +108 -0
- package/dist/docs/reference/polydeukes.md +215 -0
- package/dist/docs/troubleshooting.md +160 -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 +43 -0
- package/dist/load-config.js +90 -0
- package/dist/scaffold-project.d.ts +28 -0
- package/dist/scaffold-project.js +137 -0
- package/dist/schema/polydeukes.schema.json +203 -0
- package/package.json +31 -11
|
@@ -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
|
+
[the configuration reference's `adapters` section](./configuration.md#adapters).
|
|
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,294 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
**English** · [한국어](./configuration.ko.md)
|
|
4
|
+
|
|
5
|
+
Every key of `polydeukes.config.yaml`, one section per key. The guide — what the file is,
|
|
6
|
+
how discovery fails, and the IDE wiring — is
|
|
7
|
+
[Configuring Polydeukes](../configuration.md), and what a verdict looks like when a
|
|
8
|
+
discipline fires is its
|
|
9
|
+
[What enforcement looks like](../configuration.md#what-enforcement-looks-like) section.
|
|
10
|
+
|
|
11
|
+
## `languages`
|
|
12
|
+
|
|
13
|
+
Required. The language axis, first-class. Keys are your values (`typescript`, `python`, …) —
|
|
14
|
+
the core ships no language names and never interprets the command string.
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
languages:
|
|
18
|
+
typescript:
|
|
19
|
+
productionGlob: 'packages/*/src/**/*.ts' # what counts as production source
|
|
20
|
+
testCmd: 'pnpm --filter {scope} test' # {scope} is substituted at resolve time
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`testCmd` is a template string, not a function. Every `{scope}` occurrence is replaced;
|
|
24
|
+
all other braces (`${VAR}`, `{a,b}`, `awk '{print}'`) pass through untouched. A command
|
|
25
|
+
that ignores scope (`pnpm test`) is equally valid.
|
|
26
|
+
|
|
27
|
+
## `protectedPaths`
|
|
28
|
+
|
|
29
|
+
Optional. Raw path patterns whose files the covenants protect from modification — by
|
|
30
|
+
editor tools and by shell commands alike (`sed -i`, `tee`, redirects, heredocs,
|
|
31
|
+
parent-directory moves). Entries are normalized (trimmed, deduplicated) at resolve time.
|
|
32
|
+
An empty-string entry is rejected at load time — it carries no path meaning.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
protectedPaths:
|
|
36
|
+
- 'packages/core/src'
|
|
37
|
+
- '.claude/hooks'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**The config file protects itself.** The discovered config file is automatically appended
|
|
41
|
+
to `protectedPaths` — an edit that would lower your own gates goes through the same judge
|
|
42
|
+
as everything else. If the file that declares the disciplines were not itself under the
|
|
43
|
+
disciplines, the whole chain would be decoration.
|
|
44
|
+
|
|
45
|
+
## `adapters`
|
|
46
|
+
|
|
47
|
+
Optional. Adapter namespaces. One config file, one namespace per adapter: each key names an
|
|
48
|
+
adapter, and its value is that adapter's own settings object. The core validates the
|
|
49
|
+
container shape only — the keys and the contents belong to each adapter, which ships
|
|
50
|
+
its own validator for its own vocabulary. An unknown key *inside* a namespace is
|
|
51
|
+
rejected by that adapter's validator, with the full field path in the error.
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
adapters:
|
|
55
|
+
git:
|
|
56
|
+
enforce: advise
|
|
57
|
+
protectedPaths:
|
|
58
|
+
- 'packages/core/src'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `adapters.git` — the git commit adapter
|
|
62
|
+
|
|
63
|
+
| Key | Values | Default | Meaning |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `enforce` | `block` \| `advise` | `block` | Enforcement level of the commit surface |
|
|
66
|
+
| `protectedPaths` | string array | `[]` | Additive protection scope judged by the commit surface only |
|
|
67
|
+
|
|
68
|
+
- **`block`** — a staged change that breaks a covenant blocks the commit (exit 2). The
|
|
69
|
+
only way through is the witness valve: a human answering the TTY prompt with the full
|
|
70
|
+
token. The prompt names what it asks the human to witness — the broken registration,
|
|
71
|
+
the matched entry, and the fact that the one answer covers the whole commit. An absent
|
|
72
|
+
namespace, an absent `adapters` map, or an absent `enforce` key all mean `block` — not
|
|
73
|
+
writing the key selects the strictest level.
|
|
74
|
+
- **`advise`** — the commit surface becomes a backstop without a block: a verdict on a
|
|
75
|
+
staged change is recorded as an `advised` telemetry event and the commit proceeds
|
|
76
|
+
(exit 0) with one advisory line on stderr. No TTY prompt fires. Only the verdict is
|
|
77
|
+
relaxed — a run that cannot judge (missing or invalid config, an unresolvable judge
|
|
78
|
+
body) still fails closed at exit 2, at either level.
|
|
79
|
+
|
|
80
|
+
**`protectedPaths` here is an additive scope.** The commit surface judges the union of the
|
|
81
|
+
top-level `protectedPaths` and this list — concatenated (common first) and normalized as one,
|
|
82
|
+
so spelling and dedupe rules are identical for both. The session surface never reads it: the
|
|
83
|
+
list exists for paths whose edit is legitimate work during a session but must pass a judged
|
|
84
|
+
checkpoint when it is promoted into repository history — a judgment chain's own sources are
|
|
85
|
+
the canonical tenant. As the enforcement level is the observer's setting, so is the
|
|
86
|
+
additional scope. There is no subtractive vocabulary: a config line can widen a surface's
|
|
87
|
+
scope, never quietly strip one.
|
|
88
|
+
|
|
89
|
+
The session surface (the editor-time hook) has no level setting here; it always blocks.
|
|
90
|
+
|
|
91
|
+
**Context-family disciplines skip on the commit surface.** A commit has no session to look
|
|
92
|
+
at, so a `requirePrecedent` entry cannot be judged there — demanding evidence a commit
|
|
93
|
+
cannot carry would block every matching commit with no legitimate way through.
|
|
94
|
+
|
|
95
|
+
They are not filtered out, though. They assemble like any other discipline and become
|
|
96
|
+
*skip registrations*: routing intact, no judge body. When one matches a staged change it
|
|
97
|
+
records a `skipped` telemetry event and lets the commit proceed. The record carries the
|
|
98
|
+
entry's `id` and the change it would have judged, so a gate that did nothing says so in
|
|
99
|
+
the data — and it appears **only when the entry's scope actually matched**, so a commit
|
|
100
|
+
touching nothing the entry cares about records nothing at all.
|
|
101
|
+
|
|
102
|
+
This is the same disposition the session surface uses whenever it has no transcript to
|
|
103
|
+
read. One rule, both surfaces: evidence that cannot be evaluated is skipped and measured,
|
|
104
|
+
never blocked and never silent.
|
|
105
|
+
|
|
106
|
+
## `telemetry`
|
|
107
|
+
|
|
108
|
+
Optional.
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
telemetry:
|
|
112
|
+
logPath: '.polydeukes/roi.log' # default when omitted; keep it gitignored
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Every judgment — passed, blocked, witnessed, advised, or skipped — appends one record.
|
|
116
|
+
Telemetry is fail-open by design: a logging failure never changes a verdict. The path
|
|
117
|
+
itself is still validated at load time — an empty or whitespace-only `logPath` is
|
|
118
|
+
rejected.
|
|
119
|
+
|
|
120
|
+
## `witness`
|
|
121
|
+
|
|
122
|
+
Optional.
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
witness:
|
|
126
|
+
token: 'covenant witness' # the phrase a human types in the conversation
|
|
127
|
+
ttlMinutes: 10 # validity window, in minutes, from that message
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The values of the time-boxed human valve, consumed where the covenants are assembled.
|
|
131
|
+
The valve is sudo, not an exemption: the one property a deterministic gate can compute
|
|
132
|
+
about a judgment chain is "is an accountable human present, right now", and the witness
|
|
133
|
+
is that human supplying the pass condition in person. When a covenant blocks a
|
|
134
|
+
legitimate edit, a human types the agreed token into the conversation; blocked judgments
|
|
135
|
+
can be witnessed open for `ttlMinutes` from that message's timestamp, then blocking
|
|
136
|
+
resumes automatically. Both keys are required when the section is present: the token
|
|
137
|
+
must be non-empty after trimming, the window a finite number greater than zero.
|
|
138
|
+
|
|
139
|
+
**The valve stands after the verdict, never instead of it.** The judge body always runs.
|
|
140
|
+
A call that would have passed anyway never consults the valve, so an open window changes
|
|
141
|
+
nothing about clean work — and a `witnessed` telemetry row therefore always names a real
|
|
142
|
+
block a human answered for, never a ritual. Only a judgment that actually blocked can be
|
|
143
|
+
witnessed open.
|
|
144
|
+
|
|
145
|
+
**The token must stand alone on the message's first line.** Invoking the witness is
|
|
146
|
+
distinct from talking about it: a message that quotes, questions, or explains the token
|
|
147
|
+
mid-sentence — or wraps it in backticks — does not open the valve, while a first line
|
|
148
|
+
carrying the token alone does, with any following lines free for the work itself.
|
|
149
|
+
|
|
150
|
+
A message that invokes — the token alone on the first line, the rest free:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
covenant witness
|
|
154
|
+
|
|
155
|
+
now fix the hook file
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A message that merely mentions — the valve stays shut:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
so when does `covenant witness` expire?
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The token's value is free — any phrase works, and it is never checked for a prefix or a
|
|
165
|
+
command shape. Only its placement is constrained.
|
|
166
|
+
|
|
167
|
+
The token is not a secret — the defense is provenance, not secrecy. A witness counts only
|
|
168
|
+
when the token arrives in a message positively identified as human-typed in the session
|
|
169
|
+
transcript, so an AI that knows the token still cannot forge one. Witnessed judgments are
|
|
170
|
+
recorded as `witnessed`, never silent.
|
|
171
|
+
|
|
172
|
+
## `disciplines`
|
|
173
|
+
|
|
174
|
+
Optional. Each entry is one discipline: a practice the team imposes on itself, declared as
|
|
175
|
+
data. An entry carries exactly **one** predicate (zero or two is rejected), an `id` (the
|
|
176
|
+
telemetry label), and optionally a `why` (the reason, kept next to the rule) plus, on a
|
|
177
|
+
`forbid` or `requirePrecedent` entry, `in` (the file globs it judges) and `except` (globs
|
|
178
|
+
carved out of that scope).
|
|
179
|
+
|
|
180
|
+
**`forbid` — content delta.** Blocks an edit that *adds* a new match of the pattern.
|
|
181
|
+
Existing occurrences are forgiven: adopting a discipline never blocks a legacy codebase,
|
|
182
|
+
because the judgment direction is "what did this edit add", not "what does the file
|
|
183
|
+
contain".
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
disciplines:
|
|
187
|
+
- id: 'covenant-vocabulary'
|
|
188
|
+
why: 'control-framing vocabulary is banned in package sources.'
|
|
189
|
+
in:
|
|
190
|
+
- 'packages/*/src/**'
|
|
191
|
+
forbid: '\b(guard|harness|kb)\b'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**`immutable` — path family.** Blocks modification of existing files that match; creating
|
|
195
|
+
new files is allowed.
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
- id: 'archived-records-stay-frozen'
|
|
199
|
+
why: 'an archive that can be edited is not an archive.'
|
|
200
|
+
immutable: 'records/archive/**'
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**`forbidCommand` — command family.** Blocks shell commands matching the pattern, even
|
|
204
|
+
when the command mentions no protected path. This is how gate-disarming commands are
|
|
205
|
+
caught. A multi-line command is judged twice over — the pattern is tested against each
|
|
206
|
+
line and against the whole string, so `^` means the start of a line while a pattern
|
|
207
|
+
spanning a line boundary still matches (the whole-content caution further down applies
|
|
208
|
+
to the delta and context families). An empty pattern is rejected at load time, here and
|
|
209
|
+
on `forbid` alike — it would match every command.
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
- id: 'hooks-stay-armed'
|
|
213
|
+
why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
|
|
214
|
+
forbidCommand: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**`requirePrecedent` — context family.** Blocks a change that arrives without a required
|
|
218
|
+
step having happened earlier in the session. The other three families all ask "is this
|
|
219
|
+
change itself bad"; this one asks something else. The change is legitimate — what is
|
|
220
|
+
missing is the procedure in front of it, so what gets judged is not the mutation but the
|
|
221
|
+
session history.
|
|
222
|
+
|
|
223
|
+
Evidence means an **execution**, not a request. A call the covenant blocked, one a human
|
|
224
|
+
refused, and one that simply failed all leave the same trace in a session, and none of
|
|
225
|
+
them is precedent — the transcript is read for what actually ran and reported success.
|
|
226
|
+
That is what keeps the cheapest way through the gate being the thing the discipline
|
|
227
|
+
asks for.
|
|
228
|
+
|
|
229
|
+
Two consequences are worth knowing before you write one. The outcome is read per command
|
|
230
|
+
LINE, so a chain where the required command ran but a later step failed does not count.
|
|
231
|
+
And the pattern is matched at the start of a simple command, so the same words in an
|
|
232
|
+
argument or a comment do not count either. **In both cases running the command on its own
|
|
233
|
+
opens the gate** — the block message says so.
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
- id: 'dependency-needs-npm-view'
|
|
237
|
+
why: 'a dependency version must be measured before it is written.'
|
|
238
|
+
in:
|
|
239
|
+
- 'package.json'
|
|
240
|
+
- 'packages/*/package.json'
|
|
241
|
+
when: '(^|\n)\s*"[^"]+"\s*:\s*"[~^]?\d[^"]*"'
|
|
242
|
+
requirePrecedent:
|
|
243
|
+
command: 'npm view '
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The evidence vocabulary is layered. `command` is the core's own key — a shell call is a surface
|
|
247
|
+
every agent shares — and the core validates it fully, rejecting an empty string or a pattern that
|
|
248
|
+
does not compile. It is matched **at the start of a simple command**, not anywhere in the command
|
|
249
|
+
line, so `echo "npm view yaml"` and a mention parked behind a `#` are not evidence while `cd pkg &&
|
|
250
|
+
npm view yaml` is. Every other key belongs to an adapter: the core checks the container only (a flat
|
|
251
|
+
object carrying exactly one evidence key) and passes the value through verbatim, and the adapter
|
|
252
|
+
that owns the word validates and judges it. The Claude Code adapter brings two: `subagent` (exact
|
|
253
|
+
match on a spawn kind) and `tool` (a regex over tool names) — so "query the docs tool before
|
|
254
|
+
touching this" is expressible today. Both follow the same execution rule as `command`. An evidence
|
|
255
|
+
key no assembled adapter recognizes cannot be judged, so the entry compiles to a skip registration:
|
|
256
|
+
routing stays, the body is dropped, assembly names the fault once on stderr, and every matching
|
|
257
|
+
change afterwards records `skipped` rather than a verdict. A typo therefore never passes itself off
|
|
258
|
+
as adapter vocabulary — but it does leave the discipline inert, and the `skipped` rows are where
|
|
259
|
+
that shows.
|
|
260
|
+
|
|
261
|
+
`when` (optional) is the trigger: an added-direction delta regex, combinable with
|
|
262
|
+
`requirePrecedent` and with nothing else. When it is absent, every change inside `in`
|
|
263
|
+
scope triggers the discipline. The two keys divide the work — `in` says which files are
|
|
264
|
+
watched, `when` says which change in them demands the precedent.
|
|
265
|
+
|
|
266
|
+
**A caution on line anchors.** These patterns are matched against the file's whole content
|
|
267
|
+
as a single string, and the config schema takes a regex string with no flags. `^` therefore
|
|
268
|
+
anchors to the start of the *file*, not the start of a line, so a line-shaped pattern
|
|
269
|
+
written with `^` matches only the first line and the discipline silently stops firing —
|
|
270
|
+
the regex still compiles, the judgment still runs, and the verdict is `passed`. Write
|
|
271
|
+
`(^|\n)` when you mean the start of a line. This is why the example above carries
|
|
272
|
+
`(^|\n)\s*"[^"]+"…` rather than `^\s*"[^"]+"…`.
|
|
273
|
+
|
|
274
|
+
**And a caution on match length.** The delta keys on the matched *text*: a change is only
|
|
275
|
+
seen as added when the matched string itself differs between the file's before and after.
|
|
276
|
+
A pattern that stops mid-value — say at the first digit of a version — produces the same
|
|
277
|
+
match text for `4.0.5` and `4.0.6`, so a version bump adds nothing to the delta and the
|
|
278
|
+
discipline silently passes. Make the pattern span the whole value that can change; the
|
|
279
|
+
example above runs through the closing quote (`\d[^"]*"`) for exactly this reason. Both
|
|
280
|
+
failure shapes are the same class: the regex compiles, the verdict says `passed`, and
|
|
281
|
+
nothing tells you the discipline is inert — so when you add an entry, measure it against
|
|
282
|
+
a real file and a realistic edit, not a one-line snippet.
|
|
283
|
+
|
|
284
|
+
The kind of change matters at the trigger. With `when` present, a deletion never triggers
|
|
285
|
+
— deleting adds no content. With `when` absent, deletion triggers like any other change in
|
|
286
|
+
scope, since the declared scope is the whole mutation.
|
|
287
|
+
|
|
288
|
+
**The cheap way through is the honest one.** Unlike the witness, this evidence lives on the
|
|
289
|
+
AI's own surface, so it is not forgery-proof. It does not need to be: the least effortful
|
|
290
|
+
way to open this gate is to actually call the tool, and that is exactly the behaviour the
|
|
291
|
+
discipline exists to induce.
|
|
292
|
+
|
|
293
|
+
Adding a discipline is a data edit — no code, no plumbing. Custom judge bodies remain the
|
|
294
|
+
escape layer for the few rules data cannot express.
|
|
@@ -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
|
+
[the configuration reference](./configuration.md).
|
|
87
|
+
- **The JSON Schema artifact** — `@polydeukes/core/schema.json`, an exports subpath, for a
|
|
88
|
+
project that installs this package directly. A consumer of the umbrella names the copy
|
|
89
|
+
bundled there instead; both 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,108 @@
|
|
|
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 [the configuration reference's `disciplines` section](./configuration.md#disciplines).
|
|
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
|
+
**Six words** are the telemetry contract — five verdicts and one observation. A row in
|
|
52
|
+
`.polydeukes/roi.log` carries exactly one of them, and the CLI, the docs, and the tests use
|
|
53
|
+
the same word for the same 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
|
+
| `unattributed` | A protected entry's on-disk state moved and no judgment row explains it. **Not a verdict** — no call is blocked or passed by it; the session surface writes it after comparing state against a stored baseline |
|
|
64
|
+
|
|
65
|
+
`unattributed` answers a question the other five cannot. They are all written by a judge
|
|
66
|
+
about a call it was handed, so a write that arrives without a declared call — through an
|
|
67
|
+
interpreter, a test runner's child process, a script that assembles the path from its own
|
|
68
|
+
arguments — leaves no row at all. The comparison observes the result rather than the
|
|
69
|
+
spelling, so it records that write after the fact. It never blocks: the write already
|
|
70
|
+
happened, and the comparison fails open on both sides of the verdict.
|
|
71
|
+
|
|
72
|
+
## Where the consumer touches it
|
|
73
|
+
|
|
74
|
+
- **The `disciplines:` block** in your config. One entry compiles into one registration,
|
|
75
|
+
carrying its own telemetry label.
|
|
76
|
+
- **`protectedPaths`**, which the path-routing dispatcher matches against.
|
|
77
|
+
- **The `witness` block**, which arms the TTL valve.
|
|
78
|
+
- **`.polydeukes/roi.log`**, where every judgment lands as one row.
|
|
79
|
+
|
|
80
|
+
No import. The umbrella assembles this package for both surfaces.
|
|
81
|
+
|
|
82
|
+
## Declared limits
|
|
83
|
+
|
|
84
|
+
- **The shell axis leaves `skipped` rows, and that row is the contract.** Predicting a
|
|
85
|
+
shell command's target from its text is undecidable, so the invariant this axis holds is
|
|
86
|
+
not "nothing gets through" — it is that **no call passes unrecorded**. A new spelling
|
|
87
|
+
landing in `skipped` is the declared limit showing itself. A pass with no row at all, or
|
|
88
|
+
one recorded `passed` without a judgment, is the defect class.
|
|
89
|
+
- **The context family cannot be judged without a session.** On the commit surface there is
|
|
90
|
+
none, so a matching `requirePrecedent` entry always records `skipped`. That is a permanent
|
|
91
|
+
condition of that surface.
|
|
92
|
+
- **The command family is absent from the commit surface, and absent without a row.**
|
|
93
|
+
`forbidCommand` entries are filtered out before compilation there, since a staged diff
|
|
94
|
+
carries no command line to judge. Unlike the context family this leaves nothing in
|
|
95
|
+
`.polydeukes/roi.log`, so the log cannot separate a command discipline that never
|
|
96
|
+
triggered from one that was never registered on that surface.
|
|
97
|
+
- **An unjudgeable entry compiles to a skip registration** — routing intact, no body. A
|
|
98
|
+
pattern that does not compile skips the same way. Assembly therefore never throws: one
|
|
99
|
+
unresolvable entry cannot take down its siblings, the meta-covenants, and the valve,
|
|
100
|
+
which would leave no way to fix the config that caused it.
|
|
101
|
+
- **Complete containment is a non-goal.** There are no blocklists here — enumerating bypass
|
|
102
|
+
spellings is always one step behind, so the logic is inverted: a mention of a protected
|
|
103
|
+
path blocks unless proven safe. Residual vectors such as indirect path computation are
|
|
104
|
+
telemetry targets, not block targets. The two friction valves — the read-only allowlist
|
|
105
|
+
and the TTL witness — both leave a measurable trace.
|
|
106
|
+
- **The valve stands after the verdict.** Only a judgment that actually blocked can be
|
|
107
|
+
witnessed open, a mid-sentence mention of the token does not arm it, and an AI can never
|
|
108
|
+
open the valve for itself.
|