polydeukes 0.5.0 → 0.6.1
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/README.ko.md +54 -74
- package/README.md +55 -86
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +26 -6
- package/dist/claude-code-hook.d.ts +7 -5
- package/dist/claude-code-hook.js +69 -17
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +10 -17
- package/dist/covenant-check.js +44 -12
- package/dist/covenant-module.d.ts +2 -2
- package/dist/covenant-module.js +9 -1
- package/dist/docs/README.ko.md +60 -0
- package/dist/docs/README.md +64 -0
- package/dist/docs/catalog.json +464 -0
- package/dist/docs/concepts/judgment.ko.md +113 -0
- package/dist/docs/concepts/judgment.md +113 -0
- package/dist/docs/how-to/configure-project.ko.md +99 -0
- package/dist/docs/how-to/configure-project.md +95 -0
- package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
- package/dist/docs/how-to/connect-surfaces.md +118 -0
- package/dist/docs/how-to/write-disciplines.ko.md +124 -0
- package/dist/docs/how-to/write-disciplines.md +125 -0
- package/dist/docs/index.json +2046 -0
- package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
- package/dist/docs/reference/cli/covenant-check.md +98 -0
- package/dist/docs/reference/cli/docs.ko.md +97 -0
- package/dist/docs/reference/cli/docs.md +95 -0
- package/dist/docs/reference/cli/explain.ko.md +79 -0
- package/dist/docs/reference/cli/explain.md +84 -0
- package/dist/docs/reference/cli/init.ko.md +119 -0
- package/dist/docs/reference/cli/init.md +131 -0
- package/dist/docs/reference/configuration/index.ko.md +448 -0
- package/dist/docs/reference/configuration/index.md +474 -0
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
- package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
- package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
- package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
- package/dist/docs/reference/packages/core.ko.md +128 -0
- package/dist/docs/reference/{core.md → packages/core.md} +32 -13
- package/dist/docs/reference/packages/covenant.ko.md +115 -0
- package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
- package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
- package/dist/docs/reference/packages/polydeukes.md +139 -0
- package/dist/docs/troubleshooting.ko.md +142 -0
- package/dist/docs/troubleshooting.md +97 -122
- package/dist/docs/tutorials/first-judgment.ko.md +82 -0
- package/dist/docs/tutorials/first-judgment.md +81 -0
- package/dist/docs-catalog.d.ts +25 -0
- package/dist/docs-catalog.js +450 -0
- package/dist/docs-library.d.ts +23 -0
- package/dist/docs-library.js +347 -0
- package/dist/docs-markdown.d.ts +32 -0
- package/dist/docs-markdown.js +150 -0
- package/dist/docs-query.d.ts +11 -40
- package/dist/docs-query.js +28 -122
- package/dist/docs-types.d.ts +105 -0
- package/dist/docs-types.js +2 -0
- package/dist/explain.d.ts +3 -5
- package/dist/explain.js +48 -47
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/init-claude-code.d.ts +5 -3
- package/dist/init-claude-code.js +226 -63
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +5 -1
- package/dist/load-config.js +2 -1
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.js +48 -8
- package/dist/schema/polydeukes.schema.json +38 -91
- package/package.json +7 -7
- package/dist/docs/configuration.md +0 -103
- package/dist/docs/installation.md +0 -212
- package/dist/docs/reference/configuration.md +0 -338
- package/dist/docs/reference/polydeukes.md +0 -287
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Configuring Polydeukes
|
|
2
|
-
|
|
3
|
-
**English** · [한국어](./configuration.ko.md)
|
|
4
|
-
|
|
5
|
-
> Alpha. This guide covers the config surface as shipped today (schema v2, loader, and
|
|
6
|
-
> the four built-in discipline predicates). Fields and predicates will grow; what is
|
|
7
|
-
> written here is tested and enforced now.
|
|
8
|
-
|
|
9
|
-
`polydeukes.config.yaml` is the one file where a project declares its disciplines — the
|
|
10
|
-
promises the human and the AI partner both agree to be bound by. It is **data, not code**:
|
|
11
|
-
nothing in it can compute, so nothing in it can lie. The core validates it, the covenant
|
|
12
|
-
package enforces it, and every judgment it causes is measured.
|
|
13
|
-
|
|
14
|
-
This is the guide layer: the file itself, how discovery fails, the IDE wiring, and what
|
|
15
|
-
enforcement looks like. Every key, with its full rules and pitfalls, lives in
|
|
16
|
-
[the configuration reference](./reference/configuration.md).
|
|
17
|
-
|
|
18
|
-
## The file
|
|
19
|
-
|
|
20
|
-
Put exactly one of these at the project root:
|
|
21
|
-
|
|
22
|
-
| Filename | Note |
|
|
23
|
-
|---|---|
|
|
24
|
-
| `polydeukes.config.yaml` | canonical |
|
|
25
|
-
| `polydeukes.config.yml` | accepted variant |
|
|
26
|
-
| `polydeukes.config.json` | accepted variant (read by the same parser — YAML is a JSON superset) |
|
|
27
|
-
|
|
28
|
-
Discovery is deliberately strict, and every failure refuses loudly instead of guessing:
|
|
29
|
-
|
|
30
|
-
- **No config found** → error naming all three candidate filenames. A missing config never
|
|
31
|
-
silently loads defaults — silent defaults would mean silently unprotected.
|
|
32
|
-
- **More than one found** → error naming the collisions. Ambiguity never picks a winner.
|
|
33
|
-
- **Parse error, or a custom YAML tag** → error naming the file. Custom tags are rejected
|
|
34
|
-
even though the parser cannot execute them — config data stays uncomputable by contract.
|
|
35
|
-
- **Schema violation** → error naming the key and the file. Unknown keys are rejected
|
|
36
|
-
wherever the core owns the vocabulary — the top level, and the fixed keys inside a
|
|
37
|
-
discipline entry — so `protectedPath:` for `protectedPaths:`, or `adaptors:` for
|
|
38
|
-
`adapters:`, is caught here. Two maps stay open, because their keys are your values
|
|
39
|
-
rather than the core's: language names under `languages`, and adapter names under
|
|
40
|
-
`adapters`. A misspelt adapter name is accepted and its block simply goes unread, which
|
|
41
|
-
leaves that adapter on its defaults — check the name against the adapter's own reference.
|
|
42
|
-
Inside a namespace the vocabulary belongs to that adapter: the core passes contents
|
|
43
|
-
through verbatim, and the adapter's own validator rejects what it does not recognise,
|
|
44
|
-
naming the full field path (see
|
|
45
|
-
[the `adapters` reference](./reference/configuration.md#adapters)).
|
|
46
|
-
|
|
47
|
-
## IDE support
|
|
48
|
-
|
|
49
|
-
The JSON Schema gives autocompletion and validation in editors. It ships inside the
|
|
50
|
-
`polydeukes` package, so the line names a path into your own `node_modules`:
|
|
51
|
-
|
|
52
|
-
```yaml
|
|
53
|
-
# yaml-language-server: $schema=node_modules/polydeukes/dist/schema/polydeukes.schema.json
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
For a JSON config, use the standard top-level key instead. The loader accepts it and drops
|
|
57
|
-
it from the resolved config:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{ "$schema": "node_modules/polydeukes/dist/schema/polydeukes.schema.json" }
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**The path is resolved against the directory your config sits in**, not against a project
|
|
64
|
-
root the editor infers. The spelling above is right when the two are the same place. When
|
|
65
|
-
they are not — a config in a monorepo sub-package whose dependencies installed at the
|
|
66
|
-
workspace root — count the levels up yourself:
|
|
67
|
-
|
|
68
|
-
```yaml
|
|
69
|
-
# yaml-language-server: $schema=../../node_modules/polydeukes/dist/schema/polydeukes.schema.json
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
`pdks init claude-code` writes the line only when the schema is where the plain spelling
|
|
73
|
-
names it. If the generated config has no such line, that is the case above, and the prefix
|
|
74
|
-
is yours to add — an unresolvable path costs you validation without reporting anything.
|
|
75
|
-
|
|
76
|
-
If you installed `@polydeukes/core` directly rather than the umbrella, name its own copy:
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
79
|
-
# yaml-language-server: $schema=node_modules/@polydeukes/core/schema/polydeukes.schema.json
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Every value here is a **file path**, not a module specifier: `$schema` is a static string an
|
|
83
|
-
editor reads, so no module resolver runs on it. Code that reads the schema at runtime uses
|
|
84
|
-
the package subpath `polydeukes/schema.json` instead.
|
|
85
|
-
|
|
86
|
-
## What enforcement looks like
|
|
87
|
-
|
|
88
|
-
A `disciplines:` entry lands at **advise** by default: a break is recorded as `advised`
|
|
89
|
-
with the discipline's `id` in the telemetry record, the break message with its `why` goes
|
|
90
|
-
to stderr, and the call proceeds (exit 0) — the judgment measures instead of stopping.
|
|
91
|
-
Writing `enforce: block` on an entry is the promotion: that entry then **blocks (exit 2)**
|
|
92
|
-
before the call runs. The sanctioned valve on a block is the witness — a human supplying
|
|
93
|
-
the pass condition on a judgment that actually blocked, recorded as `witnessed` — never
|
|
94
|
-
silent.
|
|
95
|
-
|
|
96
|
-
What blocks without being asked is the judging chain's own protection, a finite list: the
|
|
97
|
-
`protectedPaths` entries (tool-axis and shell-axis mutations, and mentions without a
|
|
98
|
-
read-only head), the session transcript, and the assembly itself — a missing, ambiguous, or
|
|
99
|
-
invalid config, an unbuilt judge, an unparseable payload, or a routing that could not
|
|
100
|
-
answer. At either level the system fails closed on these, because a dead gate that waves
|
|
101
|
-
things through is the cheapest bypass of all. On the commit surface `adapters.git.enforce: advise` relaxes
|
|
102
|
-
the protected-path verdicts to `advised` as well — it is the observer's setting — while an
|
|
103
|
-
assembly that cannot judge still fails closed.
|
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
This is the getting-started layer: from an empty project to a first judged call.
|
|
9
|
-
|
|
10
|
-
One devDependency, one command per surface. The umbrella package `polydeukes` is the only
|
|
11
|
-
thing you install — it carries the core, the judge, and the adapters as its own
|
|
12
|
-
dependencies, and `pdks` is its CLI (an alias of `polydeukes`).
|
|
13
|
-
|
|
14
|
-
**Two surfaces ship, for two different situations — pick the one that matches how the
|
|
15
|
-
project is developed.** A project built alongside an AI partner in Claude Code wires the
|
|
16
|
-
**session surface**: a PreToolUse hook that judges every editing tool call and shell
|
|
17
|
-
command as it is declared. A project you develop yourself wires the **commit surface**: a
|
|
18
|
-
pre-commit hook that judges the staged diff, so the discipline you declared for yourself
|
|
19
|
-
is applied at the moment work becomes history. They enforce the same config vocabulary,
|
|
20
|
-
but they answer different situations — there is no general reason to wire both in one
|
|
21
|
-
project.
|
|
22
|
-
|
|
23
|
-
## Prerequisites
|
|
24
|
-
|
|
25
|
-
- **Node.js ≥ 24** — the engines floor of every published package.
|
|
26
|
-
- **A package manager** — pnpm and npm both work; examples below use pnpm.
|
|
27
|
-
- **Claude Code** — only for the session surface. The commit surface needs no AI tool at
|
|
28
|
-
all: just git and a way to run a pre-commit hook.
|
|
29
|
-
|
|
30
|
-
## Install
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
pnpm add -D polydeukes
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
(or `npm install --save-dev polydeukes`.)
|
|
37
|
-
|
|
38
|
-
This must be a real project dependency, not a one-off `npx` run — both surfaces load the
|
|
39
|
-
judge from your project's own installed package.
|
|
40
|
-
|
|
41
|
-
## The session surface — developing with an AI partner
|
|
42
|
-
|
|
43
|
-
From the project root:
|
|
44
|
-
|
|
45
|
-
```sh
|
|
46
|
-
pnpm exec pdks init claude-code
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The command installs into the directory it is invoked from, and it proves the `polydeukes`
|
|
50
|
-
package resolves there **before writing anything** — if it does not (say, the install step
|
|
51
|
-
was skipped), it prints the install command and exits 2 with zero files written, never a
|
|
52
|
-
half-wired tree.
|
|
53
|
-
|
|
54
|
-
Six artifacts, none ever overwritten. What exists is reported and kept — the hook, the
|
|
55
|
-
config, and the discipline files are left alone, the settings file is merged, and
|
|
56
|
-
`.gitignore` is only ever appended to — so re-running is always safe:
|
|
57
|
-
|
|
58
|
-
| Artifact | What it is |
|
|
59
|
-
|---|---|
|
|
60
|
-
| `.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. |
|
|
61
|
-
| `.claude/settings.json` | The PreToolUse registration for editing tools and shell calls. **Merged, never replaced** — your other hooks and permissions stay. |
|
|
62
|
-
| `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. |
|
|
63
|
-
| `.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. |
|
|
64
|
-
| `.claude/skills/discipline-draft/SKILL.md` | The classification procedure. Describe a recurring problem to your AI partner and it lands as a config entry — judged at advise when a current family can express it, a `draft: true` entry otherwise — and the same file tells the agent to consult `advised` rows in the telemetry log at task boundaries. |
|
|
65
|
-
| `.gitignore` | An appended ignore rule for `.polydeukes/`, with its comment line — telemetry is local observation data and never belongs in history. |
|
|
66
|
-
|
|
67
|
-
## First edit — `languages`
|
|
68
|
-
|
|
69
|
-
The generated config ships a placeholder language profile, because the installer cannot
|
|
70
|
-
know your stack:
|
|
71
|
-
|
|
72
|
-
```yaml
|
|
73
|
-
languages:
|
|
74
|
-
placeholder:
|
|
75
|
-
productionGlob: 'src/**'
|
|
76
|
-
testCmd: 'echo "set a verification command for {scope}"'
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Rename the key to your language, point `productionGlob` at your production sources, and put
|
|
80
|
-
your real verification command in `testCmd`. (On the commit-surface path you write this
|
|
81
|
-
block yourself as part of the config below.) The placeholder is valid as generated and no
|
|
82
|
-
judgment path reads these values yet, so it cannot produce a wrong verdict while it waits —
|
|
83
|
-
but `languages` is the schema's one required block, so *removing* it (or emptying it) makes
|
|
84
|
-
the config invalid, and an invalid config blocks every call. Edit it, don't delete it.
|
|
85
|
-
|
|
86
|
-
## The commit surface — developing by yourself
|
|
87
|
-
|
|
88
|
-
This path is for applying your own discipline to your own commits — no AI tool involved.
|
|
89
|
-
It has no installer today; the wiring is two small manual steps.
|
|
90
|
-
|
|
91
|
-
**First, the config.** Create `polydeukes.config.yaml` at the project root (there is no
|
|
92
|
-
generator on this path — the file is yours from the first line):
|
|
93
|
-
|
|
94
|
-
```yaml
|
|
95
|
-
languages:
|
|
96
|
-
typescript:
|
|
97
|
-
productionGlob: 'src/**'
|
|
98
|
-
testCmd: 'pnpm test'
|
|
99
|
-
|
|
100
|
-
# Judged at commit time: a staged change to these paths stops the commit
|
|
101
|
-
# until you answer the witness prompt in person.
|
|
102
|
-
protectedPaths:
|
|
103
|
-
- 'db/migrations'
|
|
104
|
-
|
|
105
|
-
witness:
|
|
106
|
-
token: 'pdks witness'
|
|
107
|
-
ttlMinutes: 10
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Add `.polydeukes/` to your `.gitignore` too — telemetry is local observation data.
|
|
111
|
-
|
|
112
|
-
**Then, the hook.** One command judges what is currently staged and exits 2 on a broken
|
|
113
|
-
covenant:
|
|
114
|
-
|
|
115
|
-
```sh
|
|
116
|
-
pnpm exec pdks covenant check
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Register it as a pre-commit hook. With **lefthook**:
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
# lefthook.yml
|
|
123
|
-
pre-commit:
|
|
124
|
-
commands:
|
|
125
|
-
covenant:
|
|
126
|
-
priority: 1
|
|
127
|
-
interactive: true # keep the witness prompt visible — see below
|
|
128
|
-
run: ./node_modules/.bin/pdks covenant check
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
With **husky**:
|
|
132
|
-
|
|
133
|
-
```sh
|
|
134
|
-
# .husky/pre-commit
|
|
135
|
-
./node_modules/.bin/pdks covenant check
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
With plain **`.git/hooks`** (make it executable):
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
#!/bin/sh
|
|
142
|
-
# .git/hooks/pre-commit
|
|
143
|
-
./node_modules/.bin/pdks covenant check
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Three things to know about this surface:
|
|
147
|
-
|
|
148
|
-
- **The valve is a TTY prompt.** At the default `block` level, a commit that stages a
|
|
149
|
-
protected change stops at a prompt only a human at a terminal can answer. Configure your
|
|
150
|
-
hook runner so it does not swallow that prompt (lefthook needs `interactive: true`).
|
|
151
|
-
- **Two discipline families judge here.** A staged diff carries file changes and nothing
|
|
152
|
-
else, so protection lists and the delta and path families (`forbid`, `immutable`) judge
|
|
153
|
-
in full. A command-family entry (`forbidCommand`) has no command line to read in a
|
|
154
|
-
staged diff and is not assembled on this surface, and a context-family entry
|
|
155
|
-
(`requirePrecedent`) is recorded as `skipped` — declare those two where an AI partner's
|
|
156
|
-
session exists to be judged.
|
|
157
|
-
- **The commit surface has its own additive scope.** Paths that are fine to edit freely
|
|
158
|
-
but whose promotion into history deserves a judged checkpoint go under the adapter
|
|
159
|
-
namespace, judged on top of the shared list:
|
|
160
|
-
|
|
161
|
-
```yaml
|
|
162
|
-
adapters:
|
|
163
|
-
git:
|
|
164
|
-
protectedPaths:
|
|
165
|
-
- 'src/policy'
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## The witness valve
|
|
169
|
-
|
|
170
|
-
Both surfaces carry the same valve, spelled for their situation. It sits **after** the
|
|
171
|
-
verdict — only a judgment that actually blocked can be witnessed open — and every allowance
|
|
172
|
-
is recorded as `witnessed`, never silent.
|
|
173
|
-
|
|
174
|
-
```yaml
|
|
175
|
-
witness:
|
|
176
|
-
token: 'pdks witness'
|
|
177
|
-
ttlMinutes: 10
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
- **Session surface:** a human types the token so it stands alone on the first line of a
|
|
181
|
-
conversation message; the window holds for `ttlMinutes`, then blocking resumes. An agent
|
|
182
|
-
cannot open the valve for itself — only human-authored messages count.
|
|
183
|
-
- **Commit surface:** the blocked commit shows a TTY prompt, and typing the full token
|
|
184
|
-
there opens that one commit.
|
|
185
|
-
|
|
186
|
-
Change the token and window as you like — the token is not a secret; the defence is
|
|
187
|
-
provenance, not confidentiality. **Keep the block**: on the session surface the generated
|
|
188
|
-
protection list covers `.claude/hooks`, so without a valve the first blocked call would
|
|
189
|
-
freeze the project until a human edits the config from their own terminal.
|
|
190
|
-
|
|
191
|
-
## Prove the gate is live
|
|
192
|
-
|
|
193
|
-
Prove it once on the surface you wired, then read the telemetry.
|
|
194
|
-
|
|
195
|
-
- **Session surface:** ask your agent to append a line to
|
|
196
|
-
`.claude/hooks/covenant-pretooluse.mjs` (a protected path). The call must come back
|
|
197
|
-
blocked.
|
|
198
|
-
- **Commit surface:** stage an edit to a path on your protection list and run
|
|
199
|
-
`git commit`. It must stop at the witness prompt (answer it, or abort with Ctrl-C).
|
|
200
|
-
|
|
201
|
-
```sh
|
|
202
|
-
cat .polydeukes/roi.log
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Every judgment appends exactly one record — `passed`, `blocked`, `witnessed`, `advised`, or
|
|
206
|
-
`skipped` — so the block you just caused is the last line. A gate you have watched block
|
|
207
|
-
once is a gate you know is wired.
|
|
208
|
-
|
|
209
|
-
From here: [the configuration guide](./configuration.md) for the file and its wiring,
|
|
210
|
-
[the configuration reference](./reference/configuration.md) for every field and for
|
|
211
|
-
writing your own disciplines, and [troubleshooting](./troubleshooting.md) when something
|
|
212
|
-
blocks and you don't know why.
|
|
@@ -1,338 +0,0 @@
|
|
|
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. What it blocks is the
|
|
90
|
-
judging chain's own protection — `protectedPaths` mutations and mentions on the tool and
|
|
91
|
-
shell axes, the session transcript, an assembly that cannot judge (missing or invalid
|
|
92
|
-
config, unbuilt judge, unparseable payload, a routing that could not answer) — plus any
|
|
93
|
-
entry promoted with `enforce: block`. Every other discipline entry lands `advised` there.
|
|
94
|
-
|
|
95
|
-
**Context-family disciplines skip on the commit surface.** A commit has no session to look
|
|
96
|
-
at, so a `requirePrecedent` entry cannot be judged there — demanding evidence a commit
|
|
97
|
-
cannot carry would block every matching commit with no legitimate way through.
|
|
98
|
-
|
|
99
|
-
They are not filtered out, though. They assemble like any other discipline and become
|
|
100
|
-
*skip registrations*: routing intact, no judge body. When one matches a staged change it
|
|
101
|
-
records a `skipped` telemetry event and lets the commit proceed. The record carries the
|
|
102
|
-
entry's `id` and the change it would have judged, so a gate that did nothing says so in
|
|
103
|
-
the data — and it appears **only when the entry's scope actually matched**, so a commit
|
|
104
|
-
touching nothing the entry cares about records nothing at all.
|
|
105
|
-
|
|
106
|
-
This is the same disposition the session surface uses whenever it has no transcript to
|
|
107
|
-
read. One rule, both surfaces: evidence that cannot be evaluated is skipped and measured,
|
|
108
|
-
never blocked and never silent.
|
|
109
|
-
|
|
110
|
-
## `telemetry`
|
|
111
|
-
|
|
112
|
-
Optional.
|
|
113
|
-
|
|
114
|
-
```yaml
|
|
115
|
-
telemetry:
|
|
116
|
-
logPath: '.polydeukes/roi.log' # default when omitted; keep it gitignored
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Every judgment — passed, blocked, witnessed, advised, or skipped — appends one record.
|
|
120
|
-
Telemetry is fail-open by design: a logging failure never changes a verdict. The path
|
|
121
|
-
itself is still validated at load time — an empty or whitespace-only `logPath` is
|
|
122
|
-
rejected.
|
|
123
|
-
|
|
124
|
-
## `witness`
|
|
125
|
-
|
|
126
|
-
Optional.
|
|
127
|
-
|
|
128
|
-
```yaml
|
|
129
|
-
witness:
|
|
130
|
-
token: 'covenant witness' # the phrase a human types in the conversation
|
|
131
|
-
ttlMinutes: 10 # validity window, in minutes, from that message
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The values of the time-boxed human valve, consumed where the covenants are assembled.
|
|
135
|
-
The valve is sudo, not an exemption: the one property a deterministic gate can compute
|
|
136
|
-
about a judgment chain is "is an accountable human present, right now", and the witness
|
|
137
|
-
is that human supplying the pass condition in person. When a covenant blocks a
|
|
138
|
-
legitimate edit, a human types the agreed token into the conversation; blocked judgments
|
|
139
|
-
can be witnessed open for `ttlMinutes` from that message's timestamp, then blocking
|
|
140
|
-
resumes automatically. Both keys are required when the section is present: the token
|
|
141
|
-
must be non-empty after trimming, the window a finite number greater than zero.
|
|
142
|
-
|
|
143
|
-
**The valve stands after the verdict, never instead of it.** The judge body always runs.
|
|
144
|
-
A call that would have passed anyway never consults the valve, so an open window changes
|
|
145
|
-
nothing about clean work — and a `witnessed` telemetry row therefore always names a real
|
|
146
|
-
block a human answered for, never a ritual. Only a judgment that actually blocked can be
|
|
147
|
-
witnessed open.
|
|
148
|
-
|
|
149
|
-
**The token must stand alone on the message's first line.** Invoking the witness is
|
|
150
|
-
distinct from talking about it: a message that quotes, questions, or explains the token
|
|
151
|
-
mid-sentence — or wraps it in backticks — does not open the valve, while a first line
|
|
152
|
-
carrying the token alone does, with any following lines free for the work itself.
|
|
153
|
-
|
|
154
|
-
A message that invokes — the token alone on the first line, the rest free:
|
|
155
|
-
|
|
156
|
-
```text
|
|
157
|
-
covenant witness
|
|
158
|
-
|
|
159
|
-
now fix the hook file
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
A message that merely mentions — the valve stays shut:
|
|
163
|
-
|
|
164
|
-
```text
|
|
165
|
-
so when does `covenant witness` expire?
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
The token's value is free — any phrase works, and it is never checked for a prefix or a
|
|
169
|
-
command shape. Only its placement is constrained.
|
|
170
|
-
|
|
171
|
-
The token is not a secret — the defense is provenance, not secrecy. A witness counts only
|
|
172
|
-
when the token arrives in a message positively identified as human-typed in the session
|
|
173
|
-
transcript, so an AI that knows the token still cannot forge one. Witnessed judgments are
|
|
174
|
-
recorded as `witnessed`, never silent.
|
|
175
|
-
|
|
176
|
-
## `disciplines`
|
|
177
|
-
|
|
178
|
-
Optional. Each entry is one discipline: a practice the team imposes on itself, declared as
|
|
179
|
-
data. An entry carries exactly **one** predicate (zero or two is rejected), an `id` (the
|
|
180
|
-
telemetry label), and optionally a `why` (the reason, which travels with the block message
|
|
181
|
-
the agent reads) plus, on a `forbid` or `requirePrecedent` entry, `in` (the file globs it
|
|
182
|
-
judges) and `except` (globs carved out of that scope).
|
|
183
|
-
|
|
184
|
-
**`draft` — an unpromoted entry.** The one shape that carries no predicate:
|
|
185
|
-
`{ id, why, draft: true }` and nothing else. A draft registers a practice as prose ahead of
|
|
186
|
-
its promotion — it makes no judgment and no telemetry record on either surface, and
|
|
187
|
-
`pdks explain` shows it as `unpromoted`. `why` is required here (the prose is the entry's
|
|
188
|
-
whole body), and the marker must be the literal `true` — a draft is declared, never
|
|
189
|
-
inferred, so an entry with neither a predicate nor `draft: true` is still a validation
|
|
190
|
-
error, and `draft: false` is rejected as dead data.
|
|
191
|
-
|
|
192
|
-
```yaml
|
|
193
|
-
disciplines:
|
|
194
|
-
- id: 'bilingual-docs-sync'
|
|
195
|
-
why: 'en and ko doc mirrors must move together.'
|
|
196
|
-
draft: true
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
A `why` is never judged — it changes no verdict. It is appended to the break message once a
|
|
200
|
-
verdict has blocked, so whoever reads the block gets the rationale in the same line instead
|
|
201
|
-
of having to open this file. A `why` spanning several lines is folded to spaces: the message
|
|
202
|
-
is one line.
|
|
203
|
-
|
|
204
|
-
**`enforce` — the entry's own level.** Optional on any judged entry: `block` or `advise`.
|
|
205
|
-
**Absent means `advise`.** Under `advise` a break is recorded as an `advised` telemetry
|
|
206
|
-
event and the call proceeds (exit 0), with the break message still written to stderr;
|
|
207
|
-
`block` is the promotion — it pins the entry at block. The entry's level composes with the
|
|
208
|
-
surface's (`adapters.git.enforce` on the commit surface; the session surface has none) and
|
|
209
|
-
the lenient side wins — an `advise` on either axis makes the entry advise, and an explicit
|
|
210
|
-
`block` never raises a surface the observer set to advise. An unjudgeable body (never
|
|
211
|
-
built, or one that cannot be loaded) still blocks whatever the level. A draft carries no
|
|
212
|
-
`enforce`; any
|
|
213
|
-
other value is rejected at load time. `pdks explain` prints the level an entry declares
|
|
214
|
-
(`enforce: block` or `enforce: advise`) on both surfaces and leaves an absent one unmarked;
|
|
215
|
-
the session header states the default.
|
|
216
|
-
|
|
217
|
-
```yaml
|
|
218
|
-
- id: 'no-console-log'
|
|
219
|
-
why: 'console output belongs to the logger; measure the habit before blocking it.'
|
|
220
|
-
forbid: 'console\.log\('
|
|
221
|
-
enforce: advise
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**`forbid` — content delta.** Blocks an edit that *adds* a new match of the pattern.
|
|
225
|
-
Existing occurrences are forgiven: adopting a discipline never blocks a legacy codebase,
|
|
226
|
-
because the judgment direction is "what did this edit add", not "what does the file
|
|
227
|
-
contain".
|
|
228
|
-
|
|
229
|
-
```yaml
|
|
230
|
-
disciplines:
|
|
231
|
-
- id: 'covenant-vocabulary'
|
|
232
|
-
why: 'control-framing vocabulary is banned in package sources.'
|
|
233
|
-
in:
|
|
234
|
-
- 'packages/*/src/**'
|
|
235
|
-
forbid: '\b(guard|harness|kb)\b'
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
**`immutable` — path family.** Blocks modification of existing files that match; creating
|
|
239
|
-
new files is allowed.
|
|
240
|
-
|
|
241
|
-
```yaml
|
|
242
|
-
- id: 'archived-records-stay-frozen'
|
|
243
|
-
why: 'an archive that can be edited is not an archive.'
|
|
244
|
-
immutable: 'records/archive/**'
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
**`forbidCommand` — command family.** Blocks shell commands matching the pattern, even
|
|
248
|
-
when the command mentions no protected path. This is how gate-disarming commands are
|
|
249
|
-
caught. A multi-line command is judged twice over — the pattern is tested against each
|
|
250
|
-
line and against the whole string, so `^` means the start of a line while a pattern
|
|
251
|
-
spanning a line boundary still matches (the whole-content caution further down applies
|
|
252
|
-
to the delta and context families). An empty pattern is rejected at load time, here and
|
|
253
|
-
on `forbid` alike — it would match every command.
|
|
254
|
-
|
|
255
|
-
```yaml
|
|
256
|
-
- id: 'hooks-stay-armed'
|
|
257
|
-
why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
|
|
258
|
-
forbidCommand: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath'
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**`requirePrecedent` — context family.** Blocks a change that arrives without a required
|
|
262
|
-
step having happened earlier in the session. The other three families all ask "is this
|
|
263
|
-
change itself bad"; this one asks something else. The change is legitimate — what is
|
|
264
|
-
missing is the procedure in front of it, so what gets judged is not the mutation but the
|
|
265
|
-
session history.
|
|
266
|
-
|
|
267
|
-
Evidence means an **execution**, not a request. A call the covenant blocked, one a human
|
|
268
|
-
refused, and one that simply failed all leave the same trace in a session, and none of
|
|
269
|
-
them is precedent — the transcript is read for what actually ran and reported success.
|
|
270
|
-
That is what keeps the cheapest way through the gate being the thing the discipline
|
|
271
|
-
asks for.
|
|
272
|
-
|
|
273
|
-
Two consequences are worth knowing before you write one. The outcome is read per command
|
|
274
|
-
LINE, so a chain where the required command ran but a later step failed does not count.
|
|
275
|
-
And the pattern is matched at the start of a simple command, so the same words in an
|
|
276
|
-
argument or a comment do not count either. **In both cases running the command on its own
|
|
277
|
-
opens the gate** — the block message says so.
|
|
278
|
-
|
|
279
|
-
```yaml
|
|
280
|
-
- id: 'dependency-needs-npm-view'
|
|
281
|
-
why: 'a dependency version must be measured before it is written.'
|
|
282
|
-
in:
|
|
283
|
-
- 'package.json'
|
|
284
|
-
- 'packages/*/package.json'
|
|
285
|
-
when: '(^|\n)\s*"[^"]+"\s*:\s*"[~^]?\d[^"]*"'
|
|
286
|
-
requirePrecedent:
|
|
287
|
-
command: 'npm view '
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
The evidence vocabulary is layered. `command` is the core's own key — a shell call is a surface
|
|
291
|
-
every agent shares — and the core validates it fully, rejecting an empty string or a pattern that
|
|
292
|
-
does not compile. It is matched **at the start of a simple command**, not anywhere in the command
|
|
293
|
-
line, so `echo "npm view yaml"` and a mention parked behind a `#` are not evidence while `cd pkg &&
|
|
294
|
-
npm view yaml` is. Every other key belongs to an adapter: the core checks the container only (a flat
|
|
295
|
-
object carrying exactly one evidence key) and passes the value through verbatim, and the adapter
|
|
296
|
-
that owns the word validates and judges it. The Claude Code adapter brings two: `subagent` (exact
|
|
297
|
-
match on a spawn kind) and `tool` (a regex over tool names) — so "query the docs tool before
|
|
298
|
-
touching this" is expressible today. Both follow the same execution rule as `command`. An evidence
|
|
299
|
-
key no assembled adapter recognizes cannot be judged, so the entry compiles to a skip registration:
|
|
300
|
-
routing stays, the body is dropped, assembly names the fault once on stderr, and every matching
|
|
301
|
-
change afterwards records `skipped` rather than a verdict. A typo therefore never passes itself off
|
|
302
|
-
as adapter vocabulary — but it does leave the discipline inert, and the `skipped` rows are where
|
|
303
|
-
that shows.
|
|
304
|
-
|
|
305
|
-
`when` (optional) is the trigger: an added-direction delta regex, combinable with
|
|
306
|
-
`requirePrecedent` and with nothing else. When it is absent, every change inside `in`
|
|
307
|
-
scope triggers the discipline. The two keys divide the work — `in` says which files are
|
|
308
|
-
watched, `when` says which change in them demands the precedent.
|
|
309
|
-
|
|
310
|
-
**A caution on line anchors.** These patterns are matched against the file's whole content
|
|
311
|
-
as a single string, and the config schema takes a regex string with no flags. `^` therefore
|
|
312
|
-
anchors to the start of the *file*, not the start of a line, so a line-shaped pattern
|
|
313
|
-
written with `^` matches only the first line and the discipline silently stops firing —
|
|
314
|
-
the regex still compiles, the judgment still runs, and the verdict is `passed`. Write
|
|
315
|
-
`(^|\n)` when you mean the start of a line. This is why the example above carries
|
|
316
|
-
`(^|\n)\s*"[^"]+"…` rather than `^\s*"[^"]+"…`.
|
|
317
|
-
|
|
318
|
-
**And a caution on match length.** The delta keys on the matched *text*: a change is only
|
|
319
|
-
seen as added when the matched string itself differs between the file's before and after.
|
|
320
|
-
A pattern that stops mid-value — say at the first digit of a version — produces the same
|
|
321
|
-
match text for `4.0.5` and `4.0.6`, so a version bump adds nothing to the delta and the
|
|
322
|
-
discipline silently passes. Make the pattern span the whole value that can change; the
|
|
323
|
-
example above runs through the closing quote (`\d[^"]*"`) for exactly this reason. Both
|
|
324
|
-
failure shapes are the same class: the regex compiles, the verdict says `passed`, and
|
|
325
|
-
nothing tells you the discipline is inert — so when you add an entry, measure it against
|
|
326
|
-
a real file and a realistic edit, not a one-line snippet.
|
|
327
|
-
|
|
328
|
-
The kind of change matters at the trigger. With `when` present, a deletion never triggers
|
|
329
|
-
— deleting adds no content. With `when` absent, deletion triggers like any other change in
|
|
330
|
-
scope, since the declared scope is the whole mutation.
|
|
331
|
-
|
|
332
|
-
**The cheap way through is the honest one.** Unlike the witness, this evidence lives on the
|
|
333
|
-
AI's own surface, so it is not forgery-proof. It does not need to be: the least effortful
|
|
334
|
-
way to open this gate is to actually call the tool, and that is exactly the behaviour the
|
|
335
|
-
discipline exists to induce.
|
|
336
|
-
|
|
337
|
-
Adding a discipline is a data edit — no code, no plumbing. Custom judge bodies remain the
|
|
338
|
-
escape layer for the few rules data cannot express.
|