polydeukes 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/covenant/shell-mod.d.ts +15 -4
  2. package/dist/covenant/shell-mod.js +63 -8
  3. package/dist/covenant/transcript-mod.js +8 -5
  4. package/dist/docs/README.ko.md +43 -18
  5. package/dist/docs/README.md +39 -17
  6. package/dist/docs/catalog.json +50 -162
  7. package/dist/docs/concepts/judgment.ko.md +2 -0
  8. package/dist/docs/concepts/judgment.md +2 -0
  9. package/dist/docs/how-to/connect-surfaces.ko.md +19 -7
  10. package/dist/docs/how-to/connect-surfaces.md +24 -12
  11. package/dist/docs/how-to/write-disciplines.ko.md +68 -31
  12. package/dist/docs/how-to/write-disciplines.md +69 -32
  13. package/dist/docs/index.json +577 -231
  14. package/dist/docs/reference/cli/init.ko.md +10 -7
  15. package/dist/docs/reference/cli/init.md +16 -11
  16. package/dist/docs/reference/configuration/index.ko.md +14 -20
  17. package/dist/docs/reference/configuration/index.md +16 -24
  18. package/dist/docs/reference/declaration-language/index.ko.md +280 -0
  19. package/dist/docs/reference/declaration-language/index.md +277 -0
  20. package/dist/docs/reference/packages/adapter-claude-code.ko.md +1 -1
  21. package/dist/docs/reference/packages/adapter-claude-code.md +1 -1
  22. package/dist/docs/reference/packages/adapter-codex.ko.md +34 -17
  23. package/dist/docs/reference/packages/adapter-codex.md +39 -21
  24. package/dist/docs/reference/packages/core.ko.md +2 -2
  25. package/dist/docs/reference/packages/core.md +2 -2
  26. package/dist/docs/reference/packages/polydeukes.ko.md +2 -2
  27. package/dist/docs/reference/packages/polydeukes.md +3 -3
  28. package/dist/docs/reference/packages/sdk-ts.ko.md +15 -18
  29. package/dist/docs/reference/packages/sdk-ts.md +14 -17
  30. package/dist/docs/troubleshooting.ko.md +18 -10
  31. package/dist/docs/troubleshooting.md +19 -11
  32. package/package.json +2 -2
@@ -0,0 +1,277 @@
1
+ # Declaration language reference
2
+
3
+ **English** · [한국어](./index.ko.md)
4
+
5
+ Use this reference to write the `declare` block of a discipline. The tables list every supported
6
+ source, extraction step, combinator, relation, and mechanism. For installation and worked examples,
7
+ see [Write disciplines](../../how-to/write-disciplines.md). Project settings and enforcement are in
8
+ the [configuration reference](../configuration/index.md).
9
+
10
+ <a id="declaration-shape"></a>
11
+ ## Declaration structure
12
+
13
+ A judged entry has `id`, `declare`, and optional `why` and `enforce` fields. Put it in the
14
+ [list that can observe its sources](../configuration/index.md#three-lists).
15
+
16
+ | Field inside `declare` | Required | Meaning |
17
+ |---|---|---|
18
+ | `mechanism` | Yes | A name from the mechanism table below; it restricts the allowed axes and relations. |
19
+ | `scope` | No | Select observations using regular expressions over one source. |
20
+ | `sources` | No | Bind names to files or session evidence. |
21
+ | `supply` | No | Choose what happens when a source is absent. Unspecified sources use `error`. |
22
+ | `extract` | Yes | A map from extraction names to non-empty lists of steps. |
23
+ | `relate` | Yes | A non-empty list of comparisons and their diagnostic messages. |
24
+ | `witness` | No | Additional comparisons that can allow a violation through. |
25
+
26
+ The outer `id` names the discipline; do not repeat it as `discipline` inside `declare`.
27
+ Names and keys are case-sensitive. Unknown declaration keys are rejected.
28
+
29
+ This complete entry reports `.db` paths outside `data/`:
30
+
31
+ ```yaml
32
+ disciplines:
33
+ - id: 'database-location'
34
+ why: 'Keep database files under data/.'
35
+ enforce: advise
36
+ declare:
37
+ mechanism: 'naming'
38
+ scope: { source: 'target.path', include: ['\.db$'] }
39
+ extract:
40
+ outside:
41
+ - { op: 'source', of: 'target.path' }
42
+ - { op: 'matches', re: '^(?!data/)' }
43
+ relate:
44
+ - id: 'location'
45
+ relation: { op: 'empty', of: 'outside' }
46
+ message: '{value} must be under data/'
47
+ ```
48
+
49
+ <a id="scope"></a>
50
+ ## Scope
51
+
52
+ | Key | Meaning |
53
+ |---|---|
54
+ | `source` | Required. One of `target.path`, `pre`, `post`, `command`, or a named `file` source. |
55
+ | `include` | Regex strings. At least one must match. Omitted or empty accepts any string. |
56
+ | `exclude` | Regex strings. Any match excludes the observation. Omitted or empty excludes nothing. |
57
+ | `excludeIgnoreCase` | Optional boolean, default `false`. Applies only to `exclude`. |
58
+
59
+ Without `scope`, every observation is eligible. With `scope`, an absent source does not match.
60
+ These patterns are regular expressions, not path globs. `include` is always case-sensitive.
61
+
62
+ <a id="fixed-sources"></a>
63
+ ## Fixed sources
64
+
65
+ Each observation supplies the sources it can prove. File changes are evaluated separately, so
66
+ `target.path`, `pre`, and `post` refer to the current change.
67
+
68
+ | Source | Value | Availability |
69
+ |---|---|---|
70
+ | `target.path` | Repository-relative path string | An observation with a file target. |
71
+ | `pre` | File text before the change | Modifications; deletions when the prior text is available. Absent on creation. |
72
+ | `post` | Proposed file text after the change | Creations and modifications. Absent on deletion. |
73
+ | `state` | Before/after pair `{ pre, post }` | Modifications. Its pipeline runs separately on each side. |
74
+ | `changes` | Array of paths in the observed change set | Read in `changeSetDisciplines`; use `items` to extract individual paths. |
75
+ | `command` | Shell command text | Session shell calls, including calls with no file target. Literal stdin data is excluded from this source. |
76
+ | `actor` | Object with optional `agentType` | A session host that proves the actor: `{}` for its main session. Absent when the host supplies no actor. |
77
+
78
+ An absent source differs from a present empty string or empty array. Use `supply` to handle absence.
79
+ `state` does not store workflow progress between calls. Only `unchanged` accepts its paired result.
80
+ The [command-source example](../configuration/index.md#disciplines) explains shell stdin handling.
81
+
82
+ <a id="source-kinds"></a>
83
+ ## Additional source kinds
84
+
85
+ `sources` maps a new name to exactly one binding. A new name cannot replace a fixed source name.
86
+
87
+ | Kind | Binding example | Supplied value |
88
+ |---|---|---|
89
+ | `file` | `en: { file: 'locales/en.json' }` | File text. Paths are repository-relative, with no leading `/` or `..` segment. |
90
+ | `sidecar` | `spawns: { sidecar: true }` | JSON text containing the host's spawn records. Parse with `json` before `agentType` or `items`. |
91
+ | `transcript` | `session: { transcript: true }` | Session snapshot with `observedAtMs`, `toolCalls`, and `userMessages`. |
92
+
93
+ Read a binding with `{ op: 'source', of: 'en' }`. A changed file uses its proposed `post` text;
94
+ other named files use the surface's observation of the project. `sidecar` and `transcript` require
95
+ `sessionDisciplines` and a host that supplies those channels. Their marker is the literal `true`.
96
+
97
+ <a id="supply-policies"></a>
98
+ ## Supply policies
99
+
100
+ | Policy | On an absent source |
101
+ |---|---|
102
+ | `error` | Default. The observation cannot be judged and is blocked, including at `advise`. |
103
+ | `pass` | Record `skipped` with reason `supply-pass`; do not judge this declaration. |
104
+ | `empty` | Continue with an empty item list. Valid for single sources, never `state`. |
105
+
106
+ Every `supply` key must name a fixed or bound source. For before/after comparisons that should skip
107
+ creations and deletions, use `supply: { state: 'pass' }`. For added-only content checks, use
108
+ `supply: { pre: 'empty', post: 'empty' }`. Invalid JSON is a supply error even with `pass` or
109
+ `empty`; those policies apply to absent sources.
110
+
111
+ <a id="items-and-pipelines"></a>
112
+ ## Items and pipelines
113
+
114
+ An extraction produces ordered items shaped as `{ key, value }`. The key identifies an item for
115
+ combinators and key comparisons. The value is the data compared by value relations. Re-keying an
116
+ item does not change its value.
117
+
118
+ Each pipeline begins with `source` or a combinator. Combinators reference other extraction names
119
+ and can appear only first. References must exist and cannot form cycles. A combinator cannot
120
+ combine a paired extraction from `state`. Further steps transform the result in sequence.
121
+
122
+ <a id="extract-steps"></a>
123
+ ## Extraction steps
124
+
125
+ The table lists all 17 unary steps. Example arguments show their exact keys; an unknown argument
126
+ causes a compilation error. Unless stated otherwise, steps preserve item order.
127
+
128
+ | Step | Arguments | Result |
129
+ |---|---|---|
130
+ | `source` | `of: 'post'` (required) | Start from the named source as one item with key `'0'`. `state` starts a paired extraction. |
131
+ | `json` | None | Parse each string value as JSON, keeping its key. Invalid JSON fails supply. |
132
+ | `select` | `path: 'args.command'` (required) | Follow a dot path through objects. Drop missing paths. An array result becomes items keyed by position; a scalar keeps its key. |
133
+ | `items` | None | Expand each array by one level into items keyed by zero-based position. Drop non-arrays. |
134
+ | `keyBy` | `field: 'id'` (required) | Set the key to the string form of an object's field. Drop non-objects and absent, null, or object-valued fields. Keep the original value. |
135
+ | `keyByPattern` | `re: '^(.+)\.ts$'` (required), `i: true` (optional, default `false`) | Set the key to capture group 1 of the first regex match. Drop non-matches and unbound captures. Keep the original value. |
136
+ | `field` | `name: 'version'` (required) | Keep the key and replace the value with that object property. An absent property yields `undefined`; a non-object is dropped. |
137
+ | `filter` | `when: [{ field: 'succeeded', eq: true }]` (required) | Keep items satisfying every predicate. `when: []` keeps all items. See the predicate table below. |
138
+ | `flattenKeys` | None | List nested leaf paths, such as `home.title`, as both keys and values. Translation text is discarded. |
139
+ | `sort` | None | Stable ascending sort by value: numeric if all values are numbers, otherwise by string comparison. |
140
+ | `lines` | None | Split stringified values on newline, trim each line, and drop empty lines. Keys are original one-based line numbers. |
141
+ | `matches` | `re: '^test:'` (required), `i: true` (optional, default `false`) | Keep items whose stringified value matches the regex; preserve keys and values. |
142
+ | `toolUses` | `names: ['Bash']`, `subagentType: 'reviewer'` (both optional) | Extract calls from a session snapshot, keyed by observation ordinal. Supplied filters must both match. Does not require success automatically. |
143
+ | `userTexts` | `re: '^approved$'` (required), `i: true` (optional, default `false`) | Extract matching user messages, keyed by ordinal. Each value also receives the snapshot's `observedAtMs`. |
144
+ | `agentType` | `is: 'reviewer'` (required) | Keep matching parsed sidecar records, keyed by position. Accepts a record array or a single object. |
145
+ | `first` | None | Keep the first item and its key. An empty input stays empty. Does not sort. |
146
+ | `ageMs` | None | Add `ageMs = observedAtMs - timestampMs` to object values. Drop missing/non-numeric timestamps and future observations. |
147
+
148
+ `items` and array-valued `select` number each array separately. If several arrays are expanded,
149
+ their keys can collide; use `keyBy` when a later comparison needs an object's identifier.
150
+ `flattenKeys` descends through plain objects. An array is a leaf at its property's path, so array
151
+ indices are not enumerated. Empty objects produce no paths, including when nested.
152
+
153
+ Regex steps use JavaScript regular expressions. `i` is the supported flag; there is no `g` or `m`
154
+ argument. A regex over whole file text anchors `^` at the start of that text. Put `lines` first to
155
+ match each trimmed line. `keyByPattern` requires a capturing group, and uses only its first match.
156
+
157
+ <a id="filter-predicates"></a>
158
+ ## Filter predicates
159
+
160
+ Each predicate contains `field` and exactly one operator. `field` names a direct object property,
161
+ not a dot path. Non-object values fail a predicate. All predicates in `when` must pass.
162
+
163
+ | Operator | Example | Condition |
164
+ |---|---|---|
165
+ | `eq` | `{ field: 'succeeded', eq: true }` | Structural equality with the constant. |
166
+ | `ne` | `{ field: 'status', ne: 'draft' }` | Structural inequality with the constant. |
167
+ | `size` | `{ field: 'errors', size: 0 }` | The field is an array with exactly this many elements. |
168
+ | `notIn` | `{ field: 'status', notIn: ['draft', 'failed'] }` | The field value is unequal to every constant in the array. |
169
+ | `lte` | `{ field: 'ageMs', lte: 600000 }` | The field is a number less than or equal to the numeric bound. |
170
+ | `gte` | `{ field: 'count', gte: 1 }` | The field is a number greater than or equal to the numeric bound. |
171
+
172
+ `size`, `lte`, and `gte` take numbers; `notIn` takes an array. An absent property is `undefined`,
173
+ so it can satisfy `ne` or `notIn`. Neither operator establishes that the property exists.
174
+
175
+ <a id="combinators"></a>
176
+ ## Combinators
177
+
178
+ The operands are two distinct extraction names. `onlyIn` and `intersect` compare **keys**;
179
+ `union` concatenates the lists. All three preserve item values and do not sort or deduplicate.
180
+
181
+ | Combinator | Syntax | Result |
182
+ |---|---|---|
183
+ | `union` | `{ op: 'union', of: ['a', 'b'] }` | All items from `a`, followed by all items from `b`, including duplicate keys. |
184
+ | `onlyIn` | `{ op: 'onlyIn', of: 'a', notIn: 'b' }` | Items from `a` whose keys do not occur in `b`. |
185
+ | `intersect` | `{ op: 'intersect', of: ['a', 'b'] }` | Items from `a` whose keys occur in `b`, with values from `a`. |
186
+
187
+ <a id="relations"></a>
188
+ ## Relations
189
+
190
+ All seven relations return the items that violate the condition. No returned items means the
191
+ condition holds. `a` and `b` below name extractions, not source files.
192
+
193
+ | Relation | Syntax | Condition |
194
+ |---|---|---|
195
+ | `empty` | `{ op: 'empty', of: 'a' }` | `a` has no items. Every item is reported on failure. |
196
+ | `nonEmpty` | `{ op: 'nonEmpty', of: 'a' }` | `a` has at least one item. Failure reports the extraction name with value `null`. |
197
+ | `equal` | `{ op: 'equal', of: ['a', 'b'] }` | The sets of values are equal in both directions. Reports left-only items, then right-only items. |
198
+ | `subset` | `{ op: 'subset', of: 'a', in: 'b' }` | Every value in `a` occurs in `b`. Reports unmatched items from `a`. |
199
+ | `implies` | `{ op: 'implies', of: 'a', requires: 'b' }` | Every key in `a` occurs in `b`. Reports items from `a` with missing required keys. |
200
+ | `ordered` | `{ op: 'ordered', of: 'a', strict: false }` | Values are ascending. `strict` defaults to `false`; `true` also rejects equal neighbours. Reports the later item in each failing pair. |
201
+ | `unchanged` | `{ op: 'unchanged', of: 'a' }` | For a paired extraction from `state`, values at shared keys agree before and after. Added and removed keys do not violate this relation. |
202
+
203
+ `equal` and `subset` compare values structurally, ignoring item keys, collection order, and
204
+ duplicate counts. Arrays *inside* values remain ordered. `implies` compares keys and ignores
205
+ values. For example, `{ key: 'en', value: 'home' }` and `{ key: 'ko', value: 'home' }` satisfy
206
+ `equal`, but the first does not imply the second because their keys differ.
207
+
208
+ `ordered` compares numerically when every value is a number; otherwise it compares string forms.
209
+ It does not sort. Empty and single-item inputs satisfy it. Sorting immediately before `ordered`
210
+ cannot establish that the original input was ordered.
211
+
212
+ Only `unchanged` accepts a pair; all other relations take single extractions. `equal`, `subset`,
213
+ and `implies` require two distinct extraction names.
214
+
215
+ <a id="messages-and-witness"></a>
216
+ ## Messages and declaration witnesses
217
+
218
+ Each `relate` entry requires a unique `id`, a `relation`, and exactly one message form:
219
+
220
+ | Field | Use |
221
+ |---|---|
222
+ | `message` | One diagnostic template for any relation. |
223
+ | `messageBySide` | `{ left: '…', right: '…' }`, allowed only for `equal`. |
224
+
225
+ Templates substitute `{key}` and `{value}` from the first violating item. `{before}` is its
226
+ previous value for `unchanged`, or an empty string when absent. Multiple violations add a count
227
+ suffix. Object values use their JavaScript string form; extract the field you want to display.
228
+
229
+ The optional declaration `witness` has its own optional `extract` and required `relate`. It can
230
+ reference the body's extractions; the body cannot reference witness extractions, and witness
231
+ extraction names cannot shadow body names. If the body fails and every witness comparison holds,
232
+ the declaration is witnessed. A witness supply failure does not release the violation.
233
+ The top-level [human witness setting](../configuration/index.md#witness) is configured separately.
234
+
235
+ <a id="mechanisms"></a>
236
+ ## Mechanisms
237
+
238
+ Every declaration names one mechanism. The compiler derives axes from `source` steps: fixed
239
+ sources except `actor` give `change`; `actor` gives `actor`; file and sidecar bindings give
240
+ `world`; transcript bindings give `history`. A scope alone does not add an axis. Body relation
241
+ names and derived axes must fit the selected mechanism. Witness extraction sources also contribute
242
+ axes. The mechanism does not supply a predicate; write the extraction and comparison yourself.
243
+
244
+ | Mechanism | Allowed axes | Allowed body relations | Purpose or required structure |
245
+ |---|---|---|---|
246
+ | `pairing` | `world` | `equal`, `subset` | Compare corresponding data from supplied files. |
247
+ | `companion` | `change`, `world` | `implies` | Require matching keys in another extraction. |
248
+ | `monotonic-order` | `change`, `world` | `ordered` | Check a sequence's order. |
249
+ | `fingerprint-sync` | `world` | `equal` | Compare extracted fingerprint values. |
250
+ | `producer-owned` | `actor` | `empty`, `nonEmpty` | Check the observed actor. |
251
+ | `self-absolution-ban` | `change` | `unchanged`, `empty` | Check changes to the file's own contents. |
252
+ | `actor-scope` | `actor` | `empty`, `nonEmpty` | Restrict work by the observed actor. |
253
+ | `precedent` | `history`, `world` | `nonEmpty` | Require prior evidence. |
254
+ | `phase-order` | `history` | `ordered` | Compare extracted observation ordinals. |
255
+ | `turn-locality` | `history` | `nonEmpty` | Require evidence within a declared time window. |
256
+ | `stated-ground` | `history` | `nonEmpty` | Require a matching user statement. |
257
+ | `controlled-vocabulary` | `change`, `world` | `subset` | Compare extracted values with an allowed set. |
258
+ | `naming` | `change` | `empty`, `nonEmpty` | `scope.source` must be `target.path`. |
259
+ | `added-only` | `change` | `empty` | Usually compares the `post`/`pre` difference. |
260
+ | `one-way-marker` | `change` | `subset` | Require selected values to remain present. |
261
+ | `delegated-scope` | — | — | Reserved; rejected at load time. |
262
+ | `scoped-valve` | `change`, `actor`, `world`, `history` | All seven | A declaration `witness` block is required. |
263
+ | `forbidden-command` | `change` | `empty` | `scope.source` must be `command`. |
264
+
265
+ The 18 names include one reserved name, so 17 can be used. Mechanism constraints and
266
+ [discipline-list placement](../configuration/index.md#placement-rule) are separate checks.
267
+
268
+ <a id="validation"></a>
269
+ ## Validate a declaration
270
+
271
+ Run `pnpm exec pdks explain` and confirm the entry is a `declare` registration on the intended
272
+ surface. A `skip` registration with `config-fault` means compilation failed; read its location
273
+ and reason. Unknown keys or invalid source/list combinations can instead fail configuration loading.
274
+
275
+ Then exercise a violating input and a valid input through the matching surface. See
276
+ [the worked locale example](../../how-to/write-disciplines.md#locale-key-pairing).
277
+ An exit code of 0 alone is insufficient: `advised` and `skipped` can both exit 0.
@@ -69,7 +69,7 @@ Claude Code의 입력을 공통 형식으로 번역합니다. 에이전트와
69
69
  <a id="consumer-contract"></a>
70
70
  ## 사용자와의 접점
71
71
 
72
- 프로젝트 루트에서 두 줄이면 Claude Code 세션 표면이 섭니다.
72
+ 프로젝트 루트에서 다음 명령을 실행하면 Claude Code 세션 표면을 설치하고 연결합니다.
73
73
 
74
74
  ```sh
75
75
  npm install --save-dev polydeukes @polydeukes/core @polydeukes/adapter-claude-code
@@ -72,7 +72,7 @@ there is no separate precedent evaluator in this package. The grammar is in
72
72
  <a id="consumer-contract"></a>
73
73
  ## Where the consumer touches it
74
74
 
75
- Two lines install the Claude Code session surface, run from the project root:
75
+ Run these commands from the project root to install and connect the Claude Code session surface:
76
76
 
77
77
  ```sh
78
78
  npm install --save-dev polydeukes @polydeukes/core @polydeukes/adapter-claude-code
@@ -2,9 +2,9 @@
2
2
 
3
3
  [English](adapter-codex.md) · **한국어**
4
4
 
5
- > **Codex의 설치 단위**입니다. `PreToolUse` 페이로드가 약속(covenant) 입력 IR이 되고, 패치가
6
- > 건드리는 파일마다 원소 하나가 실립니다. 세션 표면을 프로젝트에 설치하는 것도 이 패키지가
7
- > 합니다.
5
+ > **Codex의 설치 단위**입니다. 생명주기 이벤트는 어댑터 소유 세션 증거가 되고,
6
+ > `PreToolUse` 페이로드는 패치가 건드리는 파일마다 원소 하나를 실은 약속(covenant) 입력 IR이
7
+ > 됩니다.
8
8
  >
9
9
  > 베타입니다. `polydeukes`와 함께 설치하며, `polydeukes`는 이 패키지의 `peerDependency`입니다.
10
10
 
@@ -17,23 +17,26 @@ Codex의 입력을 공통 형식으로 번역합니다. 에이전트와 도구
17
17
  | 단위 | 하는 일 |
18
18
  |---|---|
19
19
  | `pdks-codex` 실행 파일 | 하위 명령 하나 `pdks-codex init`으로 프로젝트에 세션 표면을 등록합니다 |
20
- | `runHook` | `PreToolUse` 페이로드 하나를 입력 IR로 바꾸고 판정기를 스폰합니다 |
20
+ | `runHook` | 생명주기 증거를 기록하거나, `PreToolUse` 페이로드 하나를 입력 IR로 바꾸고 판정기를 스폰합니다 |
21
+ | 세션 증거 | 시각을 붙인 사람 메시지와 완료된 도구 호출을 SHA-256 세션 파일에 저장하고 `SessionEnd`에서 지웁니다 |
21
22
  | 페이로드 검증 | 호스트의 생성 스키마가 필수로 지정한 키를 모두 요구하며, 그 호스트가 보내는 한 가지 표기만 받습니다 |
22
23
  | 패치 해석 | `apply_patch` 호출의 원본 패치 텍스트를 건드리는 파일마다 변경 하나로 풉니다 |
23
24
  | 경로 기준 변환 | 패치 경로를 호출의 작업 디렉터리 기준으로 해석한 뒤 프로젝트 루트 기준으로 실습니다 |
24
25
 
25
- 생성된 훅 위임자가 불러오는 것이 `runHook({ repoRoot })`입니다. `tools` 명부를 실은 입력 IR을
26
- 만든 뒤 — `session`과 `actor` 키는 없습니다 — `repoRoot`에서
26
+ 생성된 훅 위임자가 불러오는 것이 `runHook({ repoRoot })`입니다. `UserPromptSubmit`과
27
+ `PostToolUse`는 `.polydeukes/codex-sessions/` 아래에 엄격한 JSONL 레코드를 추가하고,
28
+ `SessionEnd`는 그 세션 파일만 지웁니다. `PreToolUse`는 `tools` 명부와 이 레코드에서 만든
29
+ `session`을 싣되 `actor`나 `channels`를 합성하지 않은 뒤, `repoRoot`에서
27
30
  `pdks covenant check --enforce block`을 스폰하고 그 자식 프로세스의 종료 코드를 그대로
28
31
  돌려줍니다. 판정은 그 자식 프로세스가 하며, 이 패키지에는 판정 코드가 없습니다.
29
32
 
30
- **이 패키지는 텔레메트리 행을 쓰지 않습니다.** 스폰 전에 실패하면 그 사실을 한 줄로 만들어
31
- `pdks`의 stdin으로 보내므로, 그 호출이 남기는 행은 언제나 한 곳에서만 기록됩니다.
33
+ **텔레메트리는 `pdks`가 기록합니다.** 어댑터는 스폰 전에 발생한 실패도 `pdks`의 stdin으로
34
+ 보내 같은 경로로 기록합니다.
32
35
 
33
36
  <a id="apply-patch"></a>
34
37
  ## 형제 어댑터는 인자를 읽는데 이 어댑터가 텍스트를 해석하는 이유
35
38
 
36
- Codex는 모든 파일 편집을 `apply_patch`라는 이름 하나로 정규화하고, 패치 자체를
39
+ Codex는 훅에 도달하는 모든 파일 편집을 `apply_patch`라는 이름 하나로 정규화하고, 패치 자체를
37
40
  `tool_input.command`에 담습니다. 셸 호출이 명령줄을 싣는 것과 같은 자리입니다. 읽을 경로
38
41
  인자가 없습니다. `Edit`과 `Write`는 `.codex/hooks.json`에 적을 수 있는 matcher 별칭이지만,
39
42
  페이로드는 언제나 도구 이름을 `apply_patch`로 보냅니다. 그래서 별칭을 기준으로 삼은 명부나
@@ -55,7 +58,8 @@ npx pdks-codex init
55
58
  ```
56
59
 
57
60
  `pdks-codex init`은 프로젝트에서 `polydeukes`를 찾아 확인한 뒤, 에이전트 중립 초기 구성을
58
- 위해 `pdks init`을 스폰하고, 위임자를 쓰고 자기 항목을 `.codex/hooks.json`에 병합합니다.
61
+ 위해 `pdks init`을 스폰하고, 위임자를 쓴 뒤 `PreToolUse`, `UserPromptSubmit`, `PostToolUse`,
62
+ `SessionEnd` 항목을 `.codex/hooks.json`에 병합합니다.
59
63
  병합은 기존 내용을 지우지 않습니다. 다른 이벤트, 다른 matcher, 이 패키지가 모르는 키는 그대로
60
64
  둡니다. 다시 실행하면 이미 있는 산출물을 건너뛴 것으로 보고하고 아무것도 덮어쓰지 않습니다.
61
65
  전체 산출물 목록은 [`pdks init`](../cli/init.ko.md#init-codex)에 있습니다.
@@ -77,19 +81,32 @@ npx pdks-codex init
77
81
  <a id="limits"></a>
78
82
  ## 선언된 한계
79
83
 
80
- 앞의 셋은 호스트 자신의 한계이고 호스트 훅 문서가 밝힌 사항입니다. 어떤 어댑터도 이 범위를
81
- 좁힐 수 없습니다.
82
-
84
+ 앞의 넷은 호스트 자신의 한계이고 어떤 어댑터도 이 범위를 좁힐 수 없습니다. 첫째는 문서가
85
+ 아니라 실제 측정으로 확인한 사항이고, 다음 셋은 호스트 훅 문서가 밝힌 사항입니다.
86
+
87
+ - **Code Mode의 `exec` 호출은 관측되지 않습니다.** codex-cli 0.154는 Code Mode `exec` 호출에도,
88
+ 그 JavaScript 안에 중첩된 `tools.apply_patch` · `tools.exec_command` 호출에도 `PreToolUse`를
89
+ 발화하지 않습니다([openai/codex#23411](https://github.com/openai/codex/issues/23411),
90
+ [#38850](https://github.com/openai/codex/issues/38850)). `/hooks`에 Active로 표시된 훅도 그
91
+ 표면에서는 아무것도 보지 못하고, 거기서 편집된 보호 경로는 telemetry 행을 남기지 않습니다.
92
+ `pdks-codex init`이 이 사실을 `note:` 줄로 출력합니다.
93
+ - **무엇을 판정하는지는 matcher가 아니라 명부가 정합니다.** 어댑터는 `apply_patch`와 `Bash`만
94
+ 번역하고, 그 밖의 이름(Code Mode 이름, MCP 도구, `write_stdin`)은 판정기를 띄우기 전에
95
+ exit 2와 `blocked` 러너 행으로 거부합니다. `.codex/hooks.json`의 matcher를 그런 이름까지
96
+ 넓히면 그 아래의 모든 호출이 판정 대신 차단되고, 그 편집은 신뢰 해시를 바꾸므로 `/hooks`에서
97
+ 다시 승인하기 전까지 훅이 건너뛰어집니다.
83
98
  - **`write_stdin`은 다시 판정되지 않습니다.** 이미 `PreToolUse`를 통과한 unified-exec 세션에
84
99
  입력을 전달합니다. 입력을 받으려고 열어 둔 셸은 그것을 연 호출에서 한 번 판정됩니다.
85
100
  - **호스트가 제공하는 도구는 이 경로를 지나지 않습니다.** 웹 검색 같은 도구는 로컬 함수 도구
86
101
  훅을 거치지 않으므로 어떤 약속에도 닿지 않습니다.
87
102
  - **호스트는 자기 도구 훅을 완전한 차단 경계가 아니라 보조 장치라고 밝힙니다.** 일부 특수한
88
103
  도구 경로는 기본 훅 경로를 지나지 않을 수 있습니다.
89
- - **대화 기록 채널이 없습니다.** IR에 `session`과 `actor` 키가 없습니다. 페이로드가 대화 기록
90
- 경로를 지목하지만 호스트가 그 형식을 안정된 인터페이스가 아니라고 밝혔으므로 어떤 판정도
91
- 그것을 읽지 않습니다. 그래서 세션 증인 밸브가 필요로 하는 사람 메시지 증거를 얻을 수
92
- 없습니다. 의도적으로 차단된 편집을 하려면 사용자의 터미널을 쓰십시오.
104
+ - **불안정한 대화 기록은 채널로 쓰지 않습니다.** 페이로드가 대화 기록 경로를 지목하지만
105
+ 호스트가 그 형식을 안정된 인터페이스가 아니라고 밝혔으므로 어떤 판정도 그것을 읽지
106
+ 않습니다. 대신 어댑터가 `UserPromptSubmit`과 `PostToolUse`로 `session`을 만듭니다. 설정된
107
+ 토큰을 사람 메시지의 첫 줄에 단독으로 보내면 다시 시도한 보호 호출을 허용할 수 있습니다.
108
+ 사람 증거가 기록되지 않았거나 저장에 실패했다면 복구 메시지가 사용자 터미널을 안내합니다.
109
+ `actor`, `channels`, 도구 성공 여부는 합성하지 않습니다.
93
110
  - **프로젝트 밖으로 해석되는 패치는 판정하지 않고 거부합니다.** 그런 경로는 프로젝트 기준
94
111
  상대 경로가 없고, 그것을 실은 원소는 어떤 관측 범위에도 들어가지 않아 판정 없이 지나가게
95
112
  됩니다.
@@ -2,9 +2,8 @@
2
2
 
3
3
  **English** · [한국어](adapter-codex.ko.md)
4
4
 
5
- > **The Codex install unit** — `PreToolUse` payloads become the covenant input IR, with one
6
- > element per file the patch touches, and the package installs the session surface into a
7
- > project.
5
+ > **The Codex install unit** — lifecycle events become adapter-owned session evidence, and
6
+ > `PreToolUse` payloads become covenant input IR with one element per file the patch touches.
8
7
  >
9
8
  > Beta. Install it next to `polydeukes`, which it names as a `peerDependency`.
10
9
 
@@ -17,27 +16,30 @@ by design, so that they never reach the core.
17
16
  | Unit | What it does |
18
17
  |---|---|
19
18
  | `pdks-codex` bin | One subcommand, `pdks-codex init`, which registers the session surface in a project |
20
- | `runHook` | Translates one `PreToolUse` payload into the input IR and spawns the judge |
19
+ | `runHook` | Records lifecycle evidence, or translates one `PreToolUse` payload into the input IR and spawns the judge |
20
+ | Session evidence | Stores timestamped human messages and completed tool calls under a SHA-256 session filename, then removes it at `SessionEnd` |
21
21
  | Payload validation | Demands every key the host's generated schema marks required, in the one spelling that host sends |
22
22
  | Patch parsing | Turns the raw patch text of an `apply_patch` call into one file change per file it touches |
23
23
  | Path rebasing | Resolves a patch path against the call's working directory and carries it relative to the project root |
24
24
 
25
- `runHook({ repoRoot })` is what the generated hook delegator imports. It builds the IR — the
26
- `tools` roster included, with no `session` or `actor` key — then spawns
25
+ `runHook({ repoRoot })` is what the generated hook delegator imports. `UserPromptSubmit` and
26
+ `PostToolUse` append strict JSONL records under `.polydeukes/codex-sessions/`; `SessionEnd`
27
+ removes only that session's file. `PreToolUse` builds the IR with the `tools` roster and a
28
+ `session` sourced from those records, but no synthesized `actor` or `channels`, then spawns
27
29
  `pdks covenant check --enforce block` in `repoRoot` and returns the child's exit code. The
28
30
  judging happens in that child process; this package carries no judgment logic.
29
31
 
30
- **This package writes no telemetry rows.** A failure before the spawn is sent to `pdks` on
31
- stdin, so the row that call earns is written by the one writer.
32
+ **`pdks` writes the telemetry.** The adapter sends failures that occur before spawning to
33
+ `pdks` on stdin, so those failures are recorded through the same path.
32
34
 
33
35
  <a id="apply-patch"></a>
34
36
  ## Why this adapter parses text where its siblings read arguments
35
37
 
36
- Codex normalises every file edit into a single tool name, `apply_patch`, and puts the patch
37
- itself in `tool_input.command` — the same field a shell call uses for its command line. There
38
- is no path argument to read. `Edit` and `Write` exist as matcher aliases you may write in
39
- `.codex/hooks.json`, but the payload always names the tool `apply_patch`, so a roster or a
40
- branch keyed on the aliases matches nothing that ever arrives.
38
+ Codex normalises every file edit that reaches the hook into a single tool name, `apply_patch`,
39
+ and puts the patch itself in `tool_input.command` — the same field a shell call uses for its
40
+ command line. There is no path argument to read. `Edit` and `Write` exist as matcher aliases
41
+ you may write in `.codex/hooks.json`, but the payload always names the tool `apply_patch`, so
42
+ a roster or a branch keyed on the aliases matches nothing that ever arrives.
41
43
 
42
44
  One patch can create, update, delete and rename files in one call. The adapter carries each as
43
45
  its own element of one IR, on one spawn: every file is judged, and any one of them blocking
@@ -55,7 +57,8 @@ npx pdks-codex init
55
57
  ```
56
58
 
57
59
  `pdks-codex init` resolves `polydeukes` from the project, spawns `pdks init` for the
58
- agent-neutral scaffold, then writes the delegator and merges its entry into
60
+ agent-neutral scaffold, then writes the delegator and merges its entries for `PreToolUse`,
61
+ `UserPromptSubmit`, `PostToolUse`, and `SessionEnd` into
59
62
  `.codex/hooks.json` non-destructively — other events, other matchers and keys it does not know
60
63
  are left alone. A re-run reports each existing artifact as skipped and overwrites nothing. The
61
64
  full artifact list is in [`pdks init`](../cli/init.md#init-codex).
@@ -77,9 +80,22 @@ by default, so the registration this installer creates is covered by the config
77
80
  <a id="limits"></a>
78
81
  ## Declared limits
79
82
 
80
- The first three are the host's own, stated in its hook documentation, and no adapter can
81
- narrow them.
82
-
83
+ The first four are the host's own, and no adapter can narrow them. The first is measured
84
+ rather than documented; the next three are stated in the host's hook documentation.
85
+
86
+ - **A Code Mode `exec` dispatch is not observed.** In codex-cli 0.154 the host does not emit
87
+ `PreToolUse` for a Code Mode `exec` call, nor for the `tools.apply_patch` and
88
+ `tools.exec_command` calls nested in its JavaScript
89
+ ([openai/codex#23411](https://github.com/openai/codex/issues/23411),
90
+ [#38850](https://github.com/openai/codex/issues/38850)). A hook that `/hooks` lists as
91
+ Active still sees nothing on that surface, and a protected path edited there leaves no
92
+ telemetry row. `pdks-codex init` prints this as a `note:` line.
93
+ - **The roster, not the matcher, decides what is judged.** The adapter translates
94
+ `apply_patch` and `Bash`; every other name — a Code Mode name, an MCP tool, `write_stdin` —
95
+ is refused with exit 2 and a `blocked` runner row before the judge runs. Widening the
96
+ matcher in `.codex/hooks.json` to such a name blocks every call under it rather than
97
+ judging it, and the edit changes the trust hash, so the hook is skipped until `/hooks`
98
+ approves it again.
83
99
  - **`write_stdin` is not judged again.** It delivers input to a unified-exec session that
84
100
  already passed `PreToolUse`. A shell left open for input is judged once, at the call that
85
101
  opened it.
@@ -87,10 +103,12 @@ narrow them.
87
103
  function-tool hooks, so they reach no covenant.
88
104
  - **The host calls its tool hooks a guardrail rather than a complete enforcement boundary.**
89
105
  Some specialized tool paths can opt out of the default hook path.
90
- - **There is no transcript channel.** The IR omits `session` and `actor`. The payload names a
91
- transcript path, but the host documents that format as unstable, so no judgment reads it —
92
- which leaves the session witness valve without the human-message evidence it needs. For an
93
- intentional blocked edit, use your own terminal.
106
+ - **The unstable transcript is not a channel.** The payload names a transcript path, but the
107
+ host documents that format as unstable, so no judgment reads it. The adapter instead builds
108
+ `session` from `UserPromptSubmit` and `PostToolUse`. A fresh configured token, alone on the
109
+ first line of a human message, can release the retried protected call. If no human evidence
110
+ was recorded, or evidence storage failed, the recovery message directs the repair to the user
111
+ terminal. `actor`, `channels`, and tool success are never synthesized.
94
112
  - **A patch resolving outside the project is refused rather than judged.** Such a path has no
95
113
  project-relative form, and an element carrying one would land in no scope — judged over
96
114
  nothing, and passed.
@@ -141,8 +141,8 @@ function declarationChannels(body: Omit<AlgebraDeclaration, 'discipline'>): Decl
141
141
  다섯 가운데 하나도 이름 짓지 않는 본체는 변경된 파일과 저장소 파일만 읽고 그 둘은 두 표면이
142
142
  모두 공급하므로 `disciplines`에 속합니다. 그 밖은 항목과 통로와 가야 할 목록을 대는
143
143
  `ConfigValidationError`이며, 메시지는
144
- [설정 참조](../configuration/index.ko.md#placement-rule)에 있습니다. 우산 패키지는 같은 질문을
145
- 따로 답하지 않고 이 함수를 불러 씁니다.
144
+ [설정 참조](../configuration/index.ko.md#placement-rule)에 있습니다. 우산 패키지는 이 함수를
145
+ 사용해 적용할 규율 목록을 정합니다.
146
146
 
147
147
  <a id="consumer-contract"></a>
148
148
  ## 사용자와의 접점
@@ -147,8 +147,8 @@ compare the same list.
147
147
  A body naming none of the five reads the changed file and repository files alone, which both
148
148
  surfaces supply, so it belongs in `disciplines`. Anything else is a `ConfigValidationError`
149
149
  naming the entry, its channels, and the list it belongs in; the messages are in [the
150
- configuration reference](../configuration/index.md#placement-rule). The umbrella imports this
151
- function rather than answering the same question a second time.
150
+ configuration reference](../configuration/index.md#placement-rule). The umbrella uses this
151
+ function to select the applicable discipline list.
152
152
 
153
153
  <a id="consumer-contract"></a>
154
154
  ## Where the consumer touches it
@@ -113,7 +113,7 @@ IR의 `session` 키는 다른 것을 말합니다. 호스트가 그 호출에
113
113
  | 등록 | 축 | 판정 대상 |
114
114
  |---|---|---|
115
115
  | self-mod | 도구 | 편집 도구를 통한 보호 경로 변형입니다. 호출의 증명된 변형 대상만 대조하므로, 편집 **내용** 안의 보호 경로는 언급이고 통과합니다 |
116
- | shell-mod | 셸 | 같은 것을 명령줄로 합니다. 보호 경로를 언급하는 명령은 첫 낱말이 읽기 전용임을 증명할 때만 통과합니다 |
116
+ | shell-mod | 셸 | 같은 것을 명령줄로 합니다. 보호 경로를 언급하는 명령은 읽기 전용임이 증명될 때만 통과합니다. 허용 목록의 첫 낱말이나, `git ls-files`·변형·실행·실행 확인·파일 출력 동작이 없는 `find`·숫자 범위 `sed -n …p`처럼 지원하는 인자 판정형 읽기 명령이 이를 증명합니다 |
117
117
  | transcript-mod | 대화 기록 | 라이브 세션 대화 기록에 대한 쓰기입니다. 전체 경로 **등가**로 판정하고 보호 조상으로는 결코 보지 않습니다 |
118
118
 
119
119
  **낱말 여섯**이 텔레메트리 계약입니다. 판정 결과 다섯과 관측 하나입니다. `.polydeukes/roi.log`의
@@ -174,7 +174,7 @@ fail-open입니다.
174
174
  - **완전 봉쇄는 목표가 아닙니다.** 여기에 차단 목록은 없습니다. 우회 철자를 열거하는 일은
175
175
  언제나 한 걸음 뒤지므로 논리를 뒤집었습니다. 보호 경로를 언급하면 안전이 증명되지 않는
176
176
  한 차단합니다. 간접 경로 계산 같은 잔여 벡터는 차단 대상이 아니라 텔레메트리 대상입니다.
177
- 마찰 밸브 둘, 곧 읽기 전용 허용 목록과 TTL 증인은 둘 다 측정 가능한 흔적을 남깁니다.
177
+ 마찰 밸브 둘, 곧 읽기 전용 증명과 TTL 증인은 둘 다 측정 가능한 흔적을 남깁니다.
178
178
  - **밸브는 판정 뒤에 확인합니다.** 실제로 차단한 판정만 증언으로 열 수 있고, 문장 중간의 토큰
179
179
  언급은 밸브를 무장시키지 않으며, AI는 자기 밸브를 결코 스스로 열 수 없습니다.
180
180
 
@@ -114,7 +114,7 @@ vocabulary below applies to them unchanged.
114
114
  | Registration | Axis | Judges |
115
115
  |---|---|---|
116
116
  | self-mod | Tool | Mutations to protected paths through editing tools. Only the call's proven mutation target is compared — a protected path inside an edit's *content* is a mention and passes |
117
- | shell-mod | Shell | The same, through a command line. A command mentioning a protected path passes only if its leading word proves it read-only |
117
+ | shell-mod | Shell | The same, through a command line. A command mentioning a protected path passes only with read-only proof: an allowlisted leading word, or a supported argument-sensitive reader such as `git ls-files`, `find` without mutation, execution, prompting, or file-output actions, or numeric-range `sed -n …p` |
118
118
  | transcript-mod | Transcript | Writes to the live session transcript, judged by whole-path **equality** — never as a protected ancestor |
119
119
 
120
120
  **Six words** are the telemetry contract — five verdicts and one observation. A row in
@@ -179,8 +179,8 @@ No import. The umbrella assembles the module for both surfaces.
179
179
  - **Complete containment is a non-goal.** There are no blocklists here — enumerating bypass
180
180
  spellings is always one step behind, so the logic is inverted: a mention of a protected
181
181
  path blocks unless proven safe. Residual vectors such as indirect path computation are
182
- telemetry targets, not block targets. The two friction valves — the read-only allowlist
183
- and the TTL witness — both leave a measurable trace.
182
+ telemetry targets, not block targets. The two friction valves — read-only proof and the TTL
183
+ witness — both leave a measurable trace.
184
184
  - **The valve stands after the verdict.** Only a judgment that actually blocked can be
185
185
  witnessed open, a mid-sentence mention of the token does not arm it, and an AI can never
186
186
  open the valve for itself.