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.
Files changed (79) hide show
  1. package/README.ko.md +54 -74
  2. package/README.md +55 -86
  3. package/dist/baseline.d.ts +82 -0
  4. package/dist/baseline.js +166 -0
  5. package/dist/bin.d.ts +1 -1
  6. package/dist/bin.js +26 -6
  7. package/dist/claude-code-hook.d.ts +7 -5
  8. package/dist/claude-code-hook.js +69 -17
  9. package/dist/claude-code.d.ts +6 -0
  10. package/dist/claude-code.js +6 -0
  11. package/dist/covenant-check.d.ts +10 -17
  12. package/dist/covenant-check.js +44 -12
  13. package/dist/covenant-module.d.ts +2 -2
  14. package/dist/covenant-module.js +9 -1
  15. package/dist/docs/README.ko.md +60 -0
  16. package/dist/docs/README.md +64 -0
  17. package/dist/docs/catalog.json +464 -0
  18. package/dist/docs/concepts/judgment.ko.md +113 -0
  19. package/dist/docs/concepts/judgment.md +113 -0
  20. package/dist/docs/how-to/configure-project.ko.md +99 -0
  21. package/dist/docs/how-to/configure-project.md +95 -0
  22. package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
  23. package/dist/docs/how-to/connect-surfaces.md +118 -0
  24. package/dist/docs/how-to/write-disciplines.ko.md +124 -0
  25. package/dist/docs/how-to/write-disciplines.md +125 -0
  26. package/dist/docs/index.json +2046 -0
  27. package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
  28. package/dist/docs/reference/cli/covenant-check.md +98 -0
  29. package/dist/docs/reference/cli/docs.ko.md +97 -0
  30. package/dist/docs/reference/cli/docs.md +95 -0
  31. package/dist/docs/reference/cli/explain.ko.md +79 -0
  32. package/dist/docs/reference/cli/explain.md +84 -0
  33. package/dist/docs/reference/cli/init.ko.md +119 -0
  34. package/dist/docs/reference/cli/init.md +131 -0
  35. package/dist/docs/reference/configuration/index.ko.md +448 -0
  36. package/dist/docs/reference/configuration/index.md +474 -0
  37. package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
  38. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
  39. package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
  40. package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
  41. package/dist/docs/reference/packages/core.ko.md +128 -0
  42. package/dist/docs/reference/{core.md → packages/core.md} +32 -13
  43. package/dist/docs/reference/packages/covenant.ko.md +115 -0
  44. package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
  45. package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
  46. package/dist/docs/reference/packages/polydeukes.md +139 -0
  47. package/dist/docs/troubleshooting.ko.md +142 -0
  48. package/dist/docs/troubleshooting.md +97 -122
  49. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  50. package/dist/docs/tutorials/first-judgment.md +81 -0
  51. package/dist/docs-catalog.d.ts +25 -0
  52. package/dist/docs-catalog.js +450 -0
  53. package/dist/docs-library.d.ts +23 -0
  54. package/dist/docs-library.js +347 -0
  55. package/dist/docs-markdown.d.ts +32 -0
  56. package/dist/docs-markdown.js +150 -0
  57. package/dist/docs-query.d.ts +11 -40
  58. package/dist/docs-query.js +28 -122
  59. package/dist/docs-types.d.ts +105 -0
  60. package/dist/docs-types.js +2 -0
  61. package/dist/explain.d.ts +3 -5
  62. package/dist/explain.js +48 -47
  63. package/dist/index.d.ts +2 -3
  64. package/dist/index.js +1 -2
  65. package/dist/init-claude-code.d.ts +5 -3
  66. package/dist/init-claude-code.js +226 -63
  67. package/dist/init-grok.d.ts +51 -0
  68. package/dist/init-grok.js +242 -0
  69. package/dist/load-config.d.ts +5 -1
  70. package/dist/load-config.js +2 -1
  71. package/dist/pre-state-reader.d.ts +22 -0
  72. package/dist/pre-state-reader.js +32 -0
  73. package/dist/scaffold-project.js +48 -8
  74. package/dist/schema/polydeukes.schema.json +38 -91
  75. package/package.json +7 -7
  76. package/dist/docs/configuration.md +0 -103
  77. package/dist/docs/installation.md +0 -212
  78. package/dist/docs/reference/configuration.md +0 -338
  79. 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.