polydeukes 0.6.0 → 0.7.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 (117) hide show
  1. package/README.ko.md +30 -70
  2. package/README.md +31 -84
  3. package/dist/baseline.d.ts +37 -1
  4. package/dist/baseline.js +68 -1
  5. package/dist/bin.d.ts +3 -4
  6. package/dist/bin.js +79 -93
  7. package/dist/covenant/bash-line.d.ts +130 -0
  8. package/dist/covenant/bash-line.js +566 -0
  9. package/dist/covenant/declaration-engine.d.ts +108 -0
  10. package/dist/covenant/declaration-engine.js +415 -0
  11. package/dist/covenant/discipline.d.ts +103 -0
  12. package/dist/covenant/discipline.js +667 -0
  13. package/dist/covenant/dispatch.d.ts +153 -0
  14. package/dist/covenant/dispatch.js +237 -0
  15. package/dist/covenant/extract-steps.d.ts +122 -0
  16. package/dist/covenant/extract-steps.js +0 -0
  17. package/dist/covenant/mention.d.ts +117 -0
  18. package/dist/covenant/mention.js +209 -0
  19. package/dist/covenant/module.d.ts +25 -0
  20. package/dist/covenant/module.js +23 -0
  21. package/dist/covenant/mutation-rules.d.ts +40 -0
  22. package/dist/covenant/mutation-rules.js +146 -0
  23. package/dist/covenant/relations.d.ts +46 -0
  24. package/dist/covenant/relations.js +68 -0
  25. package/dist/covenant/run-covenant.d.ts +95 -0
  26. package/dist/covenant/run-covenant.js +164 -0
  27. package/dist/covenant/self-mod.d.ts +55 -0
  28. package/dist/covenant/self-mod.js +84 -0
  29. package/dist/covenant/shell-evidence.d.ts +38 -0
  30. package/dist/covenant/shell-evidence.js +247 -0
  31. package/dist/covenant/shell-mod.d.ts +84 -0
  32. package/dist/covenant/shell-mod.js +213 -0
  33. package/dist/covenant/supply.d.ts +54 -0
  34. package/dist/covenant/supply.js +50 -0
  35. package/dist/covenant/transcript-mod.d.ts +60 -0
  36. package/dist/covenant/transcript-mod.js +254 -0
  37. package/dist/covenant/ttl-witness.d.ts +47 -0
  38. package/dist/covenant/ttl-witness.js +80 -0
  39. package/dist/covenant-check.d.ts +72 -39
  40. package/dist/covenant-check.js +260 -121
  41. package/dist/diff-ir.d.ts +28 -0
  42. package/dist/diff-ir.js +310 -0
  43. package/dist/docs/README.ko.md +60 -0
  44. package/dist/docs/README.md +64 -0
  45. package/dist/docs/catalog.json +480 -0
  46. package/dist/docs/concepts/judgment.ko.md +113 -0
  47. package/dist/docs/concepts/judgment.md +113 -0
  48. package/dist/docs/how-to/configure-project.ko.md +99 -0
  49. package/dist/docs/how-to/configure-project.md +98 -0
  50. package/dist/docs/how-to/connect-surfaces.ko.md +120 -0
  51. package/dist/docs/how-to/connect-surfaces.md +121 -0
  52. package/dist/docs/how-to/write-disciplines.ko.md +179 -0
  53. package/dist/docs/how-to/write-disciplines.md +181 -0
  54. package/dist/docs/index.json +2280 -0
  55. package/dist/docs/reference/cli/covenant-check.ko.md +148 -0
  56. package/dist/docs/reference/cli/covenant-check.md +153 -0
  57. package/dist/docs/reference/cli/docs.ko.md +97 -0
  58. package/dist/docs/reference/cli/docs.md +95 -0
  59. package/dist/docs/reference/cli/explain.ko.md +84 -0
  60. package/dist/docs/reference/cli/explain.md +89 -0
  61. package/dist/docs/reference/cli/init.ko.md +119 -0
  62. package/dist/docs/reference/cli/init.md +119 -0
  63. package/dist/docs/reference/configuration/index.ko.md +553 -0
  64. package/dist/docs/reference/{configuration.md → configuration/index.md} +215 -90
  65. package/dist/docs/reference/packages/adapter-claude-code.ko.md +110 -0
  66. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +47 -19
  67. package/dist/docs/reference/packages/adapter-grok.ko.md +77 -0
  68. package/dist/docs/reference/packages/adapter-grok.md +79 -0
  69. package/dist/docs/reference/packages/core.ko.md +174 -0
  70. package/dist/docs/reference/{core.md → packages/core.md} +74 -12
  71. package/dist/docs/reference/packages/polydeukes.ko.md +195 -0
  72. package/dist/docs/reference/packages/polydeukes.md +204 -0
  73. package/dist/docs/reference/packages/sdk-ts.ko.md +161 -0
  74. package/dist/docs/reference/packages/sdk-ts.md +164 -0
  75. package/dist/docs/troubleshooting.ko.md +144 -0
  76. package/dist/docs/troubleshooting.md +103 -151
  77. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  78. package/dist/docs/tutorials/first-judgment.md +81 -0
  79. package/dist/docs-catalog.d.ts +25 -0
  80. package/dist/docs-catalog.js +450 -0
  81. package/dist/docs-library.d.ts +23 -0
  82. package/dist/docs-library.js +347 -0
  83. package/dist/docs-markdown.d.ts +32 -0
  84. package/dist/docs-markdown.js +150 -0
  85. package/dist/docs-query.d.ts +11 -40
  86. package/dist/docs-query.js +28 -122
  87. package/dist/docs-types.d.ts +105 -0
  88. package/dist/docs-types.js +2 -0
  89. package/dist/explain.d.ts +1 -1
  90. package/dist/explain.js +37 -22
  91. package/dist/load-config.d.ts +1 -1
  92. package/dist/load-config.js +1 -1
  93. package/dist/pre-state-reader.d.ts +1 -1
  94. package/dist/pre-state-reader.js +1 -1
  95. package/dist/scaffold-project.d.ts +2 -2
  96. package/dist/scaffold-project.js +9 -3
  97. package/dist/schema/polydeukes.schema.json +12 -0
  98. package/dist/worktree-reader.d.ts +19 -0
  99. package/dist/worktree-reader.js +30 -0
  100. package/package.json +3 -17
  101. package/dist/claude-code-hook.d.ts +0 -79
  102. package/dist/claude-code-hook.js +0 -372
  103. package/dist/claude-code.d.ts +0 -6
  104. package/dist/claude-code.js +0 -6
  105. package/dist/covenant-module.d.ts +0 -25
  106. package/dist/covenant-module.js +0 -42
  107. package/dist/docs/configuration.md +0 -103
  108. package/dist/docs/installation.md +0 -241
  109. package/dist/docs/reference/adapter-git.md +0 -100
  110. package/dist/docs/reference/covenant.md +0 -116
  111. package/dist/docs/reference/polydeukes.md +0 -315
  112. package/dist/index.d.ts +0 -22
  113. package/dist/index.js +0 -21
  114. package/dist/init-claude-code.d.ts +0 -52
  115. package/dist/init-claude-code.js +0 -469
  116. package/dist/init-grok.d.ts +0 -51
  117. package/dist/init-grok.js +0 -242
@@ -1,13 +1,15 @@
1
1
  # Configuration reference
2
2
 
3
- **English** · [한국어](./configuration.ko.md)
3
+ **English** · [한국어](index.ko.md)
4
4
 
5
5
  Every key of `polydeukes.config.yaml`, one section per key. The guide — what the file is,
6
6
  how discovery fails, and the IDE wiring — is
7
- [Configuring Polydeukes](../configuration.md), and what a verdict looks like when a
7
+ [Configuring Polydeukes](../../how-to/configure-project.md), and what a verdict looks like when a
8
8
  discipline fires is its
9
- [What enforcement looks like](../configuration.md#what-enforcement-looks-like) section.
9
+ [What enforcement looks like](../../how-to/configure-project.md#choose-advise-or-block)
10
+ section.
10
11
 
12
+ <a id="languages"></a>
11
13
  ## `languages`
12
14
 
13
15
  Required. The language axis, first-class. Keys are your values (`typescript`, `python`, …) —
@@ -24,6 +26,7 @@ languages:
24
26
  all other braces (`${VAR}`, `{a,b}`, `awk '{print}'`) pass through untouched. A command
25
27
  that ignores scope (`pnpm test`) is equally valid.
26
28
 
29
+ <a id="protectedpaths"></a>
27
30
  ## `protectedPaths`
28
31
 
29
32
  Optional. Raw path patterns whose files the covenants protect from modification — by
@@ -42,6 +45,7 @@ to `protectedPaths` — an edit that would lower your own gates goes through the
42
45
  as everything else. If the file that declares the disciplines were not itself under the
43
46
  disciplines, the whole chain would be decoration.
44
47
 
48
+ <a id="adapters"></a>
45
49
  ## `adapters`
46
50
 
47
51
  Optional. Adapter namespaces. One config file, one namespace per adapter: each key names an
@@ -52,62 +56,37 @@ rejected by that adapter's validator, with the full field path in the error.
52
56
 
53
57
  ```yaml
54
58
  adapters:
55
- git:
56
- enforce: advise
57
- protectedPaths:
58
- - 'packages/core/src'
59
+ example:
60
+ someKey: 'a value the example adapter defines'
59
61
  ```
60
62
 
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
63
+ **`protectedPaths` is one list for both surfaces.** There is no additive change-set-only scope
64
+ and no surface-level enforcement key in the config. The change-set surface judges the same
65
+ normalized list the session surface does. A violation exits 2 on the session surface; on the
66
+ change-set surface it lands `advised` at exit 0 unless the check runs with `--enforce block`. The
67
+ judge emits only that exit code: whether a commit stops is the user's hook wiring, so a
68
+ telemetry row records the verdict, not the commit's fate — a `blocked` row can sit beside a
69
+ commit that proceeded because the wiring ignored the exit code.
70
+
71
+ The session surface (the editor-time hook) has no level setting either. What it blocks is the
90
72
  judging chain's own protection — `protectedPaths` mutations and mentions on the tool and
91
73
  shell axes, the session transcript, an assembly that cannot judge (missing or invalid
92
74
  config, unbuilt judge, unparseable payload, a routing that could not answer) — plus any
93
75
  entry promoted with `enforce: block`. Every other discipline entry lands `advised` there.
94
76
 
95
- **Declarations that read the session skip on the commit surface.** A commit has no session
96
- to look at, so a declaration whose `sources` bind the transcript — a `precedent`,
97
- `phase-order`, `turn-locality` or `stated-ground` entry — cannot be judged there; demanding
98
- evidence a commit cannot carry would block every matching commit with no legitimate way
99
- through. The declaration's own `supply: { session: 'pass' }` disposes of the absence: when
100
- its scope matches a staged change it records a `skipped` telemetry event carrying the reason
101
- token `supply-pass` and lets the commit proceed. The record carries the entry's `id` and the
102
- change it would have judged, so a gate that did nothing says so in the data — and it appears
103
- **only when the entry's scope actually matched**. A declaration scoped on the `command`
104
- source records nothing at all there: a staged diff carries no command line, so no world it
105
- observes is ever admitted.
106
-
107
- This is the same disposition the session surface uses whenever it has no transcript to
108
- read. One rule, both surfaces: evidence that cannot be evaluated is skipped and measured,
109
- never blocked and never silent.
77
+ **A declaration reaches only the surface that observes its channels.** A transcript reader is
78
+ written in `sessionDisciplines` and is never compiled for a change set; a `changes` reader is
79
+ written in `changeSetDisciplines` and is never compiled for one call. The three lists and the
80
+ rule that places an entry in one of them are in [the three discipline
81
+ lists](#three-lists).
82
+
83
+ A declaration's `supply` policy still governs a channel the surface registers but the host
84
+ does not prove. A host that carries no session (the Grok adapter) leaves a
85
+ `sources: { session: { transcript: true } }` entry's source absent, and `supply: { session: 'pass' }`
86
+ records a `skipped` telemetry event carrying the reason token `supply-pass` and lets the call
87
+ proceed. Without that policy a missing source is unjudgeable (exit 2), not an automatic skip.
110
88
 
89
+ <a id="telemetry"></a>
111
90
  ## `telemetry`
112
91
 
113
92
  Optional.
@@ -122,6 +101,7 @@ Telemetry is fail-open by design: a logging failure never changes a verdict. The
122
101
  itself is still validated at load time — an empty or whitespace-only `logPath` is
123
102
  rejected.
124
103
 
104
+ <a id="witness"></a>
125
105
  ## `witness`
126
106
 
127
107
  Optional.
@@ -174,11 +154,141 @@ when the token arrives in a message positively identified as human-typed in the
174
154
  transcript, so an AI that knows the token still cannot forge one. Witnessed judgments are
175
155
  recorded as `witnessed`, never silent.
176
156
 
157
+ <a id="three-lists"></a>
158
+ ## The three discipline lists
159
+
160
+ A discipline is written in one of three lists, and which one is a fact about the declaration
161
+ rather than a choice the author makes. A declaration names the evidence channels it reads in
162
+ its own syntax, and a surface observes some channels and not others, so the list follows from
163
+ the channels.
164
+
165
+ | List | Judged on | What its declarations read |
166
+ |---|---|---|
167
+ | `disciplines` | both surfaces | the changed file itself (`target.path` · `pre` · `post` · `state`) and `file` sources — nothing else. Drafts live here too |
168
+ | `sessionDisciplines` | the session surface only | at least one of `command` · `transcript` · the `sidecar` channel · `actor`, and never `changes` |
169
+ | `changeSetDisciplines` | the change-set surface only | `changes`, and no session channel |
170
+
171
+ The session surface is one call a host observed before it ran — an adapter hook, or a program
172
+ calling the SDK with an input IR. It carries the command line, the conversation history, the
173
+ spawn-record channel, and the actor, and it does not carry the finished change set. The
174
+ change-set surface is `pdks covenant check --diff` over a change set some producer finished.
175
+ It carries `changes`, and it carries no command line, no history, and no actor.
176
+
177
+ <a id="channel-to-list"></a>
178
+ ### Channel to list
179
+
180
+ Each channel appears in a declaration in a fixed syntactic position, and the loader reads
181
+ those positions to derive the list.
182
+
183
+ | Channel | How it appears in a declaration | List |
184
+ |---|---|---|
185
+ | `transcript` | `sources: { session: { transcript: true } }` | `sessionDisciplines` |
186
+ | `channel` | `sources: { spawns: { sidecar: true } }` | `sessionDisciplines` |
187
+ | `command` | `scope: { source: 'command' }`, or `{ op: 'source', of: 'command' }` in any pipeline | `sessionDisciplines` |
188
+ | `actor` | `{ op: 'source', of: 'actor' }` in any pipeline | `sessionDisciplines` |
189
+ | `changes` | `{ op: 'source', of: 'changes' }` in any pipeline | `changeSetDisciplines` |
190
+ | none of the five | the declaration reads the changed file and `file` sources alone | `disciplines` |
191
+
192
+ A `witness` block's own `extract` is walked with the body's, so a valve reading the transcript
193
+ puts its entry in `sessionDisciplines` like any other transcript reader.
194
+
195
+ <a id="placement-rule"></a>
196
+ ### The placement rule
197
+
198
+ `disciplines` takes an entry binding none of the five channels, plus every draft.
199
+ `sessionDisciplines` takes an entry binding at least one session channel and not `changes`.
200
+ `changeSetDisciplines` takes an entry binding `changes` and no session channel. Anything else
201
+ is a `ConfigValidationError` at load time, on both surfaces, and the message names the entry,
202
+ the channels it reads, and the list it belongs in:
203
+
204
+ ```text
205
+ disciplines[7] ('merge-is-the-users-call') reads transcript, command: it belongs in sessionDisciplines
206
+ ```
207
+
208
+ Two shapes have no destination to name. A declaration binding both `changes` and a session
209
+ channel is refused outright, because no surface observes both at once:
210
+
211
+ ```text
212
+ sessionDisciplines[2] ('pairs-across-a-session') reads transcript, changes: no surface observes both changes and a session channel
213
+ ```
214
+
215
+ A draft binds nothing — it carries no declaration — so it belongs in `disciplines`, and a
216
+ draft written in either surface list is refused with `a draft belongs in disciplines`.
217
+
218
+ Ids are unique across all three lists and the three meta-covenant labels (`self-mod` ·
219
+ `shell-mod` · `transcript-mod`): the telemetry label space is one space, and `pdks explain`
220
+ and every label-keyed reader index by the label alone.
221
+
222
+ <a id="lists-in-practice"></a>
223
+ ### What each surface compiles
224
+
225
+ `pdks covenant check` reading an input IR on stdin compiles `disciplines` followed by
226
+ `sessionDisciplines`; the same command with `--diff` compiles `disciplines` followed by
227
+ `changeSetDisciplines`. `pdks explain` prints both surfaces with each list's name and count in
228
+ the surface header, so the placement of every entry is readable without running a judgment.
229
+ This repository's live config places 11 judged entries and 1 draft in `disciplines`, 13 in
230
+ `sessionDisciplines`, and 1 (`docs-stay-bilingual`) in `changeSetDisciplines` — 24
231
+ registrations on the session surface and 12 on the change-set surface.
232
+
233
+ ```yaml
234
+ disciplines: # both surfaces
235
+ - id: 'covenant-vocabulary'
236
+ declare:
237
+ mechanism: 'added-only'
238
+ scope: { source: 'target.path', include: ['^packages/'] }
239
+ supply: { pre: 'empty', post: 'empty' }
240
+ extract:
241
+ before: [{ op: 'source', of: 'pre' }, { op: 'lines' }]
242
+ after: [{ op: 'source', of: 'post' }, { op: 'lines' }]
243
+ added: [{ op: 'onlyIn', of: 'after', notIn: 'before' }]
244
+ relate:
245
+ - { id: 'nothing-added', relation: { op: 'empty', of: 'added' }, message: 'adds {value}' }
246
+
247
+ sessionDisciplines: # the session surface only — reads command
248
+ - id: 'pnpm-only'
249
+ declare:
250
+ mechanism: 'forbidden-command'
251
+ scope: { source: 'command' }
252
+ extract:
253
+ hits:
254
+ - { op: 'source', of: 'command' }
255
+ - { op: 'lines' }
256
+ - { op: 'matches', re: '\bnpm install\b' }
257
+ relate:
258
+ - { id: 'no-npm', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
259
+
260
+ changeSetDisciplines: # the change-set surface only — reads changes
261
+ - id: 'docs-stay-bilingual'
262
+ declare:
263
+ mechanism: 'companion'
264
+ scope: { source: 'target.path', include: ['\.md$'] }
265
+ extract:
266
+ en:
267
+ - { op: 'source', of: 'target.path' }
268
+ - { op: 'keyByPattern', re: '^(.+?)(?<!\.ko)\.md$' }
269
+ koChanged:
270
+ - { op: 'source', of: 'changes' }
271
+ - { op: 'items' }
272
+ - { op: 'keyByPattern', re: '^(.+)\.ko\.md$' }
273
+ relate:
274
+ - id: 'ko-follows'
275
+ relation: { op: 'implies', of: 'en', requires: 'koChanged' }
276
+ message: '{value} changed without {key}.ko.md'
277
+ ```
278
+
279
+ Moving an entry between lists is the whole edit: the entry's body is unchanged by the move,
280
+ and the loader is what says whether the new place is the right one.
281
+
282
+ <a id="disciplines"></a>
177
283
  ## `disciplines`
178
284
 
179
285
  Optional. Each entry is one discipline: a practice the team imposes on itself, declared as
180
- data. A judged entry carries a `declare` block — the one judged form, a declaration whose
181
- `scope` lives inside the block — an `id` (the telemetry label), and optionally a `why` (the
286
+ data. Everything in this section applies to an entry in any of the three lists; which list an
287
+ entry is written in is decided by [the placement rule](#placement-rule), and
288
+ `sessionDisciplines` and `changeSetDisciplines` take the same entry shape as `disciplines`
289
+ minus the draft form. A judged entry carries a `declare` block — the one judged form, a
290
+ declaration whose `scope` lives inside the block — an `id` (the telemetry label), and
291
+ optionally a `why` (the
182
292
  reason, which travels with the block message the agent reads) and an `enforce` level. The
183
293
  closed key set is `id` · `why` · `enforce` · `declare`; any other key is refused.
184
294
 
@@ -192,11 +302,15 @@ error, and `draft: false` is rejected as dead data.
192
302
 
193
303
  ```yaml
194
304
  disciplines:
195
- - id: 'bilingual-docs-sync'
196
- why: 'en and ko doc mirrors must move together.'
305
+ - id: 'benchmark-supports-performance-claim'
306
+ why: 'a performance claim must be supported by a fresh benchmark run during judgment.'
197
307
  draft: true
198
308
  ```
199
309
 
310
+ A bilingual mirror pair is not a draft. The engine already judges that shape as `companion`
311
+ (this repository's `docs-stay-bilingual` entry). Use `draft: true` only for a promise the
312
+ current grammar cannot express.
313
+
200
314
  A `why` is never judged — it changes no verdict. It is appended to the break message once a
201
315
  verdict has blocked, so whoever reads the block gets the rationale in the same line instead
202
316
  of having to open this file. A `why` spanning several lines is folded to spaces: the message
@@ -205,10 +319,10 @@ is one line.
205
319
  **`enforce` — the entry's own level.** Optional on any judged entry: `block` or `advise`.
206
320
  **Absent means `advise`.** Under `advise` a break is recorded as an `advised` telemetry
207
321
  event and the call proceeds (exit 0), with the break message still written to stderr;
208
- `block` is the promotion — it pins the entry at block. The entry's level composes with the
209
- surface's (`adapters.git.enforce` on the commit surface; the session surface has none) and
210
- the lenient side wins — an `advise` on either axis makes the entry advise, and an explicit
211
- `block` never raises a surface the observer set to advise. An unjudgeable body (never
322
+ `block` is the promotion — it pins the entry at block. The config carries no surface-level
323
+ enforcement key: absent or `advise` means `advised` on both surfaces, `block` means exit 2 on
324
+ the session surface and on a change-set check run with `--enforce block` (the change-set
325
+ surface's default posture is advise for every verdict). An unjudgeable body (never
212
326
  built, or one that cannot be loaded) still blocks whatever the level. A draft carries no
213
327
  `enforce`; any
214
328
  other value is rejected at load time. `pdks explain` prints the level an entry declares
@@ -242,21 +356,21 @@ regular expressions over the path.
242
356
 
243
357
  ```yaml
244
358
  disciplines:
245
- - id: 'covenant-vocabulary'
246
- why: 'control-framing vocabulary is banned in package sources.'
359
+ - id: 'no-focused-tests-in-src'
360
+ why: 'a focused test must not land in shared source.'
247
361
  declare:
248
362
  mechanism: 'added-only'
249
- scope: { source: 'target.path', include: ['^packages/[^/]+/src/'] }
363
+ scope: { source: 'target.path', include: ['^src/', '^test/'] }
250
364
  supply: { pre: 'empty', post: 'empty' }
251
365
  extract:
252
366
  before:
253
367
  - { op: 'source', of: 'pre' }
254
368
  - { op: 'lines' }
255
- - { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
369
+ - { op: 'keyByPattern', re: '(\.only\()' }
256
370
  after:
257
371
  - { op: 'source', of: 'post' }
258
372
  - { op: 'lines' }
259
- - { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
373
+ - { op: 'keyByPattern', re: '(\.only\()' }
260
374
  added:
261
375
  - { op: 'onlyIn', of: 'after', notIn: 'before' }
262
376
  relate:
@@ -266,12 +380,13 @@ disciplines:
266
380
  ```
267
381
 
268
382
  The key is the match text, so a line carrying a word the file already has anywhere is
269
- forgiven, and a line carrying two new words surfaces the first one now and the second on the
270
- next judgment.
383
+ forgiven. A line carrying two new words surfaces only the first match; the second appears
384
+ on a later judgment **after that first match is removed**. Re-judging the same input yields
385
+ the same first match.
271
386
 
272
387
  **A frozen path is a declaration too.** A file that may be created once and never modified
273
388
  or deleted: `pre` present means a modification, `post` absent means a deletion, and either
274
- breaks.
389
+ breaks. Creating the file, including with empty contents (`post: ''`), passes.
275
390
 
276
391
  ```yaml
277
392
  - id: 'archived-records-stay-frozen'
@@ -297,7 +412,14 @@ declaration reads that source, cuts it into lines, keeps the lines a pattern mat
297
412
  requires the result to be `empty`. It scopes on `command` so that only shell calls are
298
413
  admitted: an Edit carries no command line, and a declaration reading a source its world
299
414
  lacks is unjudgeable. A multi-line command is judged line by line, so `^` means the start
300
- of a line; a pattern that would span a line boundary does not match.
415
+ of a line; a pattern that would span a line boundary does not match. Heredoc bodies and
416
+ herestring words are not part of the source: bash passes them to the command as stdin
417
+ data rather than executing them, so a banned token quoted inside one is not a match, and
418
+ whether that data becomes a file write is judged by the shell-evidence path instead. Text
419
+ bash expands first stays in the source — a body under an unquoted delimiter that carries
420
+ `$` or a backtick, and a herestring word holding a substitution — as does a line the
421
+ tokenizer could not finish reading. What the receiving command does with its stdin is not
422
+ judged: a script fed to an interpreter is data to bash and a program to the interpreter.
301
423
 
302
424
  ```yaml
303
425
  - id: 'hooks-stay-armed'
@@ -323,8 +445,9 @@ declaration the user turns and tool calls as one snapshot, `toolUses` picks the
323
445
  `matches` finds the required one; `nonEmpty` is the verdict. A call the covenant blocked,
324
446
  one a human refused, and one that simply failed are not precedent. The pattern is matched
325
447
  anywhere in a command line — a line that merely mentions the command counts, a declared
326
- limit. `supply: { session: 'pass' }` is what makes the commit surface record `skipped`
327
- instead of blocking every matching commit.
448
+ limit. `supply: { session: 'pass' }` is what disposes of an absent session on a host that proves
449
+ none; an entry reading the transcript is written in `sessionDisciplines`, so a change set never
450
+ compiles it at all.
328
451
 
329
452
  ```yaml
330
453
  - id: 'dependency-needs-npm-view'
@@ -360,10 +483,12 @@ adds nothing to an added-only difference and the discipline silently passes: mak
360
483
  pattern span the whole value that can change. Both failure shapes compile, run, and answer
361
484
  `passed`, so measure a new entry against a real file and a realistic edit.
362
485
 
363
- **The cheap way through is the honest one.** Unlike the witness, session evidence lives on
364
- the AI's own surface, so it is not forgery-proof. It does not need to be: the least
365
- effortful way to open this gate is to actually run the command, and that is exactly the
366
- behaviour the discipline exists to induce.
486
+ **Witnesses and precedent evidence are different.** A pattern seeking precedent evidence must
487
+ distinguish the required action from a mere mention. Session evidence lives on the AI's own
488
+ surface, so it is not forgery-proof; the design relies on the least effortful way to satisfy
489
+ the check being to actually run the command, which is the behaviour the discipline exists to
490
+ induce. A pattern alone does not eliminate the possibility of forged evidence. Test both valid
491
+ and violating cases.
367
492
 
368
493
  **`declare` — declaration family.** One judgment written as data, in the algebra grammar
369
494
  the core publishes as `algebra-declaration.schema.json`: `judge = relate ∘ extract`. The
@@ -372,37 +497,37 @@ block carries the declaration's `scope`, `sources`, `supply`, `extract`, `relate
372
497
  `discipline` key, and `in`/`except`/`when` are refused — the `scope` block is the scope.
373
498
 
374
499
  ```yaml
375
- - id: 'db-only-under-knowledge'
376
- why: 'a *.db file may exist only under _docs/knowledge/'
500
+ - id: 'db-files-only-under-data'
501
+ why: 'a *.db file may exist only under data/'
377
502
  declare:
378
503
  mechanism: 'naming'
379
504
  scope: { source: 'target.path', include: ['\.db$'] }
380
505
  extract:
381
506
  outside:
382
507
  - { op: 'source', of: 'target.path' }
383
- - { op: 'matches', re: '^(?!_docs/knowledge/)' }
508
+ - { op: 'matches', re: '^(?!data/)' }
384
509
  relate:
385
510
  - id: 'placed'
386
511
  relation: { op: 'empty', of: 'outside' }
387
- message: '{value} is outside _docs/knowledge/'
512
+ message: '{value} is outside data/'
388
513
  ```
389
514
 
390
- This repository's live config carries the same declaration as `sqlite-only-under-knowledge`.
515
+ This repository's live config uses the same mechanism with `_docs/knowledge/` as
516
+ `sqlite-only-under-knowledge`.
391
517
 
392
518
  Each observation is judged as one **world** with seven source names: `target.path` (the
393
519
  repo-relative path), `pre` and `post` (the file's text on the side the change carries —
394
520
  a creation has no `pre`, a deletion no `post`), `state` (`{ pre, post }`, present only
395
521
  on a modification), and `changes` (every path the observation changes — the one call on the
396
- session surface, the whole staged set on the commit surface), and `command` (the shell
522
+ session surface, the whole staged set on the change-set surface), and `command` (the shell
397
523
  call's command line — present on a shell call only, and a shell call that changes no file
398
524
  is one world of its own, so a declaration scoped on `command` sees it while one scoped on
399
525
  `target.path` does not), and `actor` (the observation's actor — described below). A
400
- declaration that reads `changes` is judged only where the whole change set is observed: the
401
- session surface records it `skipped`, the same disposition the commit surface gives a
402
- declaration that reads the session, because one call can never carry the other half of a
403
- pair. This repository's live config
404
- carries one — `docs-stay-bilingual`, an `implies` over the `.md`/`.ko.md` pair, advised
405
- on the commit surface when one side is staged without the other. A declaration that needs a
526
+ declaration that reads `changes` is written in `changeSetDisciplines` and compiled only where
527
+ the whole change set is observed, because one call never carries the other half of a pair.
528
+ This repository's live config carries one — `docs-stay-bilingual`, an `implies` over the
529
+ `.md`/`.ko.md` pair, advised on the change-set surface when one side is staged without the
530
+ other. A declaration that needs a
406
531
  file outside the target names it in a `sources` block, `sources: { en: { file:
407
532
  'locales/en.json' } }`, and reads it as `{ op: 'source', of: 'en' }`; the path is
408
533
  repo-relative (no leading `/`, no `..` segment) and the name may not be one of the seven. The
@@ -412,16 +537,16 @@ touches is read from the change's `post`, so both surfaces judge the same text.
412
537
  kind, `sources: { spawns: { sidecar: true } }`, names the session's spawn-record channel
413
538
  instead of a path — the subagent records the host keeps beside the transcript, supplied as
414
539
  one JSON array; where the channel lives is the surface's fact, so the value is the marker
415
- `true`, and on the commit surface (which has no session) the channel is always absent. A
540
+ `true`, and on the change-set surface (which has no session) the channel is always absent. A
416
541
  third kind, `sources: { session: { transcript: true } }`, names the session's own
417
542
  conversation history — the user turns and tool calls the surface reads, handed to the
418
543
  declaration as one snapshot whose entries carry their
419
544
  observation ordinal; the history steps (`toolUses`, `userTexts`, `first`, `ageMs`) read it, and
420
545
  `agentType` reads the parsed sidecar. This repository's live config carries one —
421
- `tests-before-implementation`, an `ordered` over the ordinals of two subagent spawns, which
422
- the commit surface (no session) records `skipped`. The seventh fixed name, `actor`, is the
546
+ `tests-before-implementation`, an `ordered` over the ordinals of two subagent spawns, written
547
+ in `sessionDisciplines`. The seventh fixed name, `actor`, is the
423
548
  observation's actor — `{ agentType }` inside a subagent, `{}` in the main session, absent
424
- where the surface proves none (the commit surface) — read as `{ op: 'source', of: 'actor' }`
549
+ where the surface proves none (the change-set surface) — read as `{ op: 'source', of: 'actor' }`
425
550
  followed by `select` on `agentType`; it derives the `actor` axis the `producer-owned` and
426
551
  `actor-scope` mechanisms require, and this repository's live config carries one of each
427
552
  (`tests-are-the-writers`, `commits-come-from-the-main-session`). A `supply` key must name
@@ -0,0 +1,110 @@
1
+ # `@polydeukes/adapter-claude-code`
2
+
3
+ [English](adapter-claude-code.md) · **한국어**
4
+
5
+ > **Claude Code의 설치 단위**입니다. PreToolUse 페이로드가 약속(covenant) 입력 IR이 되고,
6
+ > 판정기가 읽는 파일 변경 증거와 대화 기록(transcript) 통로가 함께 실립니다. 세션 표면을
7
+ > 프로젝트에 설치하는 것도 이 패키지가 합니다.
8
+ >
9
+ > 알파입니다. `polydeukes`와 함께 설치하며, `polydeukes`는 이 패키지의 `peerDependency`입니다.
10
+
11
+ <a id="ownership"></a>
12
+ ## 담당하는 기능
13
+
14
+ Claude Code의 입력을 공통 형식으로 번역합니다. 에이전트와 도구의 구체적인 이름은 이 패키지에서 처리하고 코어에는 넣지 않습니다. 코어가 특정 에이전트에 의존하지 않는다는
15
+ 제약은 테스트로 확인합니다.
16
+
17
+ | 단위 | 하는 일 |
18
+ |---|---|
19
+ | `pdks-claude-code` 실행 파일 | 하위 명령 하나 `pdks-claude-code init`으로 프로젝트에 세션 표면을 등록합니다 |
20
+ | `runHook` | PreToolUse 페이로드 하나를 입력 IR로 바꾸고 판정기를 스폰합니다 |
21
+ | 페이로드 상향 번역 | 원본 PreToolUse 페이로드가 `CovenantInput`이 됩니다 |
22
+ | 예상 변경 후 상태 | 편집이 적용되면 파일이 무엇을 담을지 디스크를 건드리지 않고 계산합니다 |
23
+ | 파일 변경 증거 | 디스크의 변경 전 상태와 예상 변경 후 상태를 짝지어 변경 증거를 만듭니다 |
24
+ | 대화 기록(transcript) 제공자 | 세션 JSONL 파일을 `CanonicalTranscript`로 바꿉니다 |
25
+
26
+ 생성된 훅 위임자가 불러오는 것이 `runHook({ repoRoot })`입니다. `tools`와 `session` 증거를
27
+ 실은 입력 IR을 만든 뒤 `repoRoot`에서 `pdks covenant check --enforce block`을 스폰하고 그
28
+ 자식 프로세스의 종료 코드를 그대로 돌려줍니다. 판정은 그 자식 프로세스가 하며, 이 패키지에는
29
+ 판정 코드가 없습니다.
30
+
31
+ **이 패키지는 텔레메트리 행을 쓰지 않습니다.** 스폰 전에 실패하면 그 사실을 한 줄로 만들어
32
+ `pdks`의 표준 입력으로 보내고, `pdks`가 fail-closed 행을 기록합니다. 호출 하나에 행 하나는
33
+ 그대로입니다. 판정기를 불러오지도 않습니다. `polydeukes`와 `@polydeukes/core` 모두
34
+ `peerDependencies`이므로 어휘와 판정기를 공유할 뿐 사본을 따로 설치하지 않습니다.
35
+
36
+ <a id="translation"></a>
37
+ ## 페이로드 번역과 세 축
38
+
39
+ **판정기에 증거를 전달하는 축은 셋**이며, 축마다 제공할 수 있는 증거가 다릅니다.
40
+
41
+ | 축 | 싣는 것 | 결과 |
42
+ |---|---|---|
43
+ | 도구 | 증명된 `fileChange`입니다. 도구가 실행되기 전에 계산된 변형 대상입니다 | 증명된 대상만 판정합니다. 편집 **내용** 안의 보호 경로는 언급이고 통과합니다 |
44
+ | 셸 | 실행 전에는 대상을 계산할 수 없는 경우가 많은 명령줄입니다 | 계산되는 쓰기는 편집처럼 판정하고, 나머지는 추측하지 않고 기록합니다 |
45
+ | 대화 기록 | 세션 자신의 기록입니다 | 전체 경로 등가로 판정하고 보호 조상으로는 보지 않습니다 |
46
+
47
+ 번역 과정에서 실패하면 어느 단계에서든 호출을 차단합니다(fail-closed).
48
+ 서브에이전트 종류가 지정된 `Task` 호출은 스폰에 대응시킵니다. 분류할 수 없는 페이로드는 번역 **실패**이고, `blocked` 한 행을 기록하고
49
+ 종료 `2`를 냅니다. 추측으로 물러서지 않습니다. 봉투 최상위의 `agent_type`은 IR의 `actor`가
50
+ 됩니다. 서브에이전트 안에서는 `{ agentType }`, 그 밖에는 `{}`입니다. `tool_input`은 에이전트
51
+ 자신의 텍스트이므로 여기서 읽지 않습니다.
52
+
53
+ **변경 후의 증거는 파일을 다시 읽는 대신 계산합니다.** `Edit`, `Write`, `MultiEdit`을
54
+ 메모리에서 적용해 예상 변경 후 상태를 구합니다. 다중 편집도 순서대로 적용합니다. 내용을
55
+ 확인하는 규율(discipline)은 현재 파일이 아니라 **제안된** 결과를 판정합니다.
56
+ 변경 후 상태를 계산할 수 없으면 증거를 만들지 않습니다. 실제 도구도 같은 편집을 거부하기
57
+ 때문입니다. 파일을 바꾸지 않는 호출에 변경 증거를 만들어 넣지도 않습니다.
58
+
59
+ **대화 기록은 사람이 친 것으로 확정된 메시지만 받아들입니다.** 증인 밸브가 사람 전용인
60
+ 근거가 이것입니다. AI는 자기 증인을 합성할 수 없습니다. 읽기 실패는 빈 대화 기록이 아니라
61
+ `undefined`로 답합니다. 빈 세션은 아직 아무 말도 하지 않은 것이라 판정 대상이고, 읽을 수
62
+ 없는 세션은 증거 통로 자체가 없는 것이라 건너뜁니다. 어느 경우에도 이 부재만으로 증인 밸브가 작업을 허용하지는 않습니다.
63
+
64
+ **선행(`precedent`)은 선언이지 어댑터 평가기가 아닙니다.** 이 어댑터는 대화 기록
65
+ 스냅샷을 공급합니다. 성공한 `toolUses`를 추출해 대조하는 것은 선언 엔진입니다. 이
66
+ 패키지에 별도 선례 평가기는 없습니다. 문법은
67
+ [설정 — 규율](../configuration/index.ko.md#disciplines)에 있습니다.
68
+
69
+ <a id="consumer-contract"></a>
70
+ ## 사용자와의 접점
71
+
72
+ 프로젝트 루트에서 두 줄이면 Claude Code 세션 표면이 섭니다.
73
+
74
+ ```sh
75
+ npm install --save-dev polydeukes @polydeukes/adapter-claude-code
76
+ npx pdks-claude-code init
77
+ ```
78
+
79
+ `pdks-claude-code init`은 프로젝트에서 `polydeukes`를 찾고, 에이전트와 무관한 초기 파일을
80
+ 만드는 `pdks init`을 스폰한 뒤, Claude Code 산출물 넷을 기존 파일을 덮지 않고 씁니다.
81
+ 다시 실행하면 이미 있는 산출물마다 `skipped`로 보고하고 아무것도 덮어쓰지 않습니다.
82
+ 산출물 전체 목록은 [`pdks init`](../cli/init.ko.md#init-claude-code)에 있습니다.
83
+
84
+ - **생성된 훅.** 이 패키지의 `runHook`을 불러옵니다. 패키지를 갱신하면 실행할 구현도 바뀌지만
85
+ 훅 파일은 그대로입니다.
86
+
87
+ 별도 설정 네임스페이스는 없습니다.
88
+
89
+ <a id="limits"></a>
90
+ ## 선언된 한계
91
+
92
+ - **자식 프로세스의 쓰기는 관측 밖입니다.** 이 표면이 판정하는 것은 **선언된 도구
93
+ 호출**입니다. 프로세스를 띄우고 그 프로세스가 파일을 쓰는 명령은, 테스트 러너든 빌드든,
94
+ 명령에 대해 판정되지 자식이 한 일에 대해 판정되지 않습니다. 추적되는 파일에 대해서는
95
+ 변경 집합 표면에서 결과를 별도로 관측할 수 있습니다.
96
+ - **증거는 후상태를 계산할 수 있는 곳에만 있습니다.** 변형 도구 넷이 전부 증거를 내고
97
+ 노트북도 그렇습니다. `NotebookEdit`는 셀 단위 `modify` 증거를 냅니다. 증거가 없는 것은
98
+ 이 어댑터가 해소하지 못하는 페이로드입니다. 읽을 수 없거나 파싱되지 않는 노트북, 이름을
99
+ 붙일 수 없는 셀, 알지 못하는 편집 모드가 그렇습니다.
100
+ - **증거 없는 호출은 보수적으로 판정합니다.** 증명된 대상이 없으므로 호출 인자에 보호
101
+ 경로가 언급됐는지 대조합니다.
102
+ - **저장소 밖 조상은 범위 밖입니다.** 프로젝트 루트 위의 경로는 여기서 관측하지 않습니다.
103
+ 그 범위는 에이전트 실행 환경의 권한 정책으로 제한해야 합니다.
104
+ - **`polydeukes`를 찾지 못하면 행이 남지 않습니다.** 프로젝트에서 우산 패키지를 찾지 못하면
105
+ 스폰할 프로세스도, 기록할 로그 경로도 없습니다. 훅은 stderr에 한 줄을 남기고 종료 코드
106
+ `2`를 내며 텔레메트리 로그에는 아무것도 추가되지 않습니다. 스폰 전 실패 가운데 이 경우만
107
+ 그렇고, 나머지는 모두 `pdks`에 도달해 행을 남깁니다.
108
+ - **Grok는 `@polydeukes/adapter-grok`로 설치합니다.** 그 패키지가 자기 위임자와 명부를
109
+ 씁니다. 두 세션 어댑터를 한 프로젝트에 함께 설치하면 호출마다 판정기가 두 번 실행될 수
110
+ 있습니다.