polydeukes 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +9 -3
- package/README.md +12 -4
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +19 -2
- package/dist/claude-code-hook.d.ts +7 -5
- package/dist/claude-code-hook.js +69 -17
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +10 -17
- package/dist/covenant-check.js +44 -12
- package/dist/covenant-module.d.ts +2 -2
- package/dist/covenant-module.js +9 -1
- package/dist/docs/configuration.md +1 -1
- package/dist/docs/installation.md +41 -12
- package/dist/docs/reference/adapter-claude-code.md +6 -4
- package/dist/docs/reference/adapter-git.md +8 -6
- package/dist/docs/reference/configuration.md +222 -104
- package/dist/docs/reference/core.md +11 -5
- package/dist/docs/reference/covenant.md +31 -23
- package/dist/docs/reference/polydeukes.md +62 -34
- package/dist/docs/troubleshooting.md +36 -9
- package/dist/explain.d.ts +3 -5
- package/dist/explain.js +48 -47
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/init-claude-code.d.ts +5 -3
- package/dist/init-claude-code.js +76 -30
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +5 -1
- package/dist/load-config.js +2 -1
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.js +48 -8
- package/dist/schema/polydeukes.schema.json +38 -91
- package/package.json +7 -7
|
@@ -92,16 +92,17 @@ shell axes, the session transcript, an assembly that cannot judge (missing or in
|
|
|
92
92
|
config, unbuilt judge, unparseable payload, a routing that could not answer) — plus any
|
|
93
93
|
entry promoted with `enforce: block`. Every other discipline entry lands `advised` there.
|
|
94
94
|
|
|
95
|
-
**
|
|
96
|
-
at, so a `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
105
106
|
|
|
106
107
|
This is the same disposition the session surface uses whenever it has no transcript to
|
|
107
108
|
read. One rule, both surfaces: evidence that cannot be evaluated is skipped and measured,
|
|
@@ -176,10 +177,10 @@ recorded as `witnessed`, never silent.
|
|
|
176
177
|
## `disciplines`
|
|
177
178
|
|
|
178
179
|
Optional. Each entry is one discipline: a practice the team imposes on itself, declared as
|
|
179
|
-
data.
|
|
180
|
-
telemetry label), and optionally a `why` (the
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
182
|
+
reason, which travels with the block message the agent reads) and an `enforce` level. The
|
|
183
|
+
closed key set is `id` · `why` · `enforce` · `declare`; any other key is refused.
|
|
183
184
|
|
|
184
185
|
**`draft` — an unpromoted entry.** The one shape that carries no predicate:
|
|
185
186
|
`{ id, why, draft: true }` and nothing else. A draft registers a practice as prose ahead of
|
|
@@ -215,124 +216,241 @@ other value is rejected at load time. `pdks explain` prints the level an entry d
|
|
|
215
216
|
the session header states the default.
|
|
216
217
|
|
|
217
218
|
```yaml
|
|
218
|
-
- id: '
|
|
219
|
-
why: '
|
|
220
|
-
forbid: 'console\.log\('
|
|
219
|
+
- id: 'hooks-stay-armed'
|
|
220
|
+
why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
|
|
221
221
|
enforce: advise
|
|
222
|
+
declare:
|
|
223
|
+
mechanism: 'forbidden-command'
|
|
224
|
+
scope: { source: 'command' }
|
|
225
|
+
extract:
|
|
226
|
+
hits:
|
|
227
|
+
- { op: 'source', of: 'command' }
|
|
228
|
+
- { op: 'lines' }
|
|
229
|
+
- { op: 'matches', re: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath' }
|
|
230
|
+
relate:
|
|
231
|
+
- { id: 'gates-armed', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
|
|
222
232
|
```
|
|
223
233
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
**Added-direction content is a declaration.** A promise about what an edit *adds* — a
|
|
235
|
+
banned word, a stray `.only`, a citation that resolves nowhere — is written as an
|
|
236
|
+
`added-only` declaration: `pre` and `post` are each cut into lines and keyed by the match,
|
|
237
|
+
`onlyIn` keeps what `post` has and `pre` lacks, and `empty` over that difference is the
|
|
238
|
+
verdict. Existing occurrences are forgiven, so adopting the discipline never blocks a legacy
|
|
239
|
+
codebase. `supply: empty` is what lets a file creation (no `pre`) count as all-added and a
|
|
240
|
+
deletion (no `post`) as adding nothing; the `scope` block replaces `in`/`except` with
|
|
241
|
+
regular expressions over the path.
|
|
228
242
|
|
|
229
243
|
```yaml
|
|
230
244
|
disciplines:
|
|
231
245
|
- id: 'covenant-vocabulary'
|
|
232
246
|
why: 'control-framing vocabulary is banned in package sources.'
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
declare:
|
|
248
|
+
mechanism: 'added-only'
|
|
249
|
+
scope: { source: 'target.path', include: ['^packages/[^/]+/src/'] }
|
|
250
|
+
supply: { pre: 'empty', post: 'empty' }
|
|
251
|
+
extract:
|
|
252
|
+
before:
|
|
253
|
+
- { op: 'source', of: 'pre' }
|
|
254
|
+
- { op: 'lines' }
|
|
255
|
+
- { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
|
|
256
|
+
after:
|
|
257
|
+
- { op: 'source', of: 'post' }
|
|
258
|
+
- { op: 'lines' }
|
|
259
|
+
- { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
|
|
260
|
+
added:
|
|
261
|
+
- { op: 'onlyIn', of: 'after', notIn: 'before' }
|
|
262
|
+
relate:
|
|
263
|
+
- id: 'nothing-added'
|
|
264
|
+
relation: { op: 'empty', of: 'added' }
|
|
265
|
+
message: 'adds {key}: {value}'
|
|
236
266
|
```
|
|
237
267
|
|
|
238
|
-
|
|
239
|
-
new
|
|
268
|
+
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.
|
|
271
|
+
|
|
272
|
+
**A frozen path is a declaration too.** A file that may be created once and never modified
|
|
273
|
+
or deleted: `pre` present means a modification, `post` absent means a deletion, and either
|
|
274
|
+
breaks.
|
|
240
275
|
|
|
241
276
|
```yaml
|
|
242
277
|
- id: 'archived-records-stay-frozen'
|
|
243
278
|
why: 'an archive that can be edited is not an archive.'
|
|
244
|
-
|
|
279
|
+
declare:
|
|
280
|
+
mechanism: 'self-absolution-ban'
|
|
281
|
+
scope: { source: 'target.path', include: ['^records/archive/'] }
|
|
282
|
+
supply: { pre: 'empty', post: 'empty' }
|
|
283
|
+
extract:
|
|
284
|
+
prior: [{ op: 'source', of: 'pre' }]
|
|
285
|
+
here: [{ op: 'source', of: 'target.path' }]
|
|
286
|
+
after: [{ op: 'source', of: 'post' }]
|
|
287
|
+
deleted: [{ op: 'onlyIn', of: 'here', notIn: 'after' }]
|
|
288
|
+
touched: [{ op: 'union', of: ['prior', 'deleted'] }]
|
|
289
|
+
relate:
|
|
290
|
+
- { id: 'frozen', relation: { op: 'empty', of: 'touched' }, message: '{value} is frozen' }
|
|
245
291
|
```
|
|
246
292
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
**A command line is a source.** On the session surface a shell call carries its command
|
|
294
|
+
line as the fixed source `command`, and a call that changes no file is still one
|
|
295
|
+
observation — it is judged as a world of its own, with subject `-`. A `forbidden-command`
|
|
296
|
+
declaration reads that source, cuts it into lines, keeps the lines a pattern matches, and
|
|
297
|
+
requires the result to be `empty`. It scopes on `command` so that only shell calls are
|
|
298
|
+
admitted: an Edit carries no command line, and a declaration reading a source its world
|
|
299
|
+
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.
|
|
254
301
|
|
|
255
302
|
```yaml
|
|
256
303
|
- id: 'hooks-stay-armed'
|
|
257
304
|
why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
|
|
258
|
-
|
|
305
|
+
declare:
|
|
306
|
+
mechanism: 'forbidden-command'
|
|
307
|
+
scope: { source: 'command' }
|
|
308
|
+
extract:
|
|
309
|
+
hits:
|
|
310
|
+
- { op: 'source', of: 'command' }
|
|
311
|
+
- { op: 'lines' }
|
|
312
|
+
- { op: 'matches', re: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath' }
|
|
313
|
+
relate:
|
|
314
|
+
- { id: 'gates-armed', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
|
|
259
315
|
```
|
|
260
316
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
change
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
refused, and one that simply failed
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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.
|
|
317
|
+
**A precedent is a declaration over the session.** Most declarations ask "is this change
|
|
318
|
+
itself bad"; a `precedent` asks whether a required step happened earlier in the session.
|
|
319
|
+
The change is legitimate — what is missing is the procedure in front of it, so what gets
|
|
320
|
+
judged is the session history: `sources: { session: { transcript: true } }` hands the
|
|
321
|
+
declaration the user turns and tool calls as one snapshot, `toolUses` picks the calls,
|
|
322
|
+
`filter` keeps the ones that ran and **succeeded**, `select` reaches the command line, and
|
|
323
|
+
`matches` finds the required one; `nonEmpty` is the verdict. A call the covenant blocked,
|
|
324
|
+
one a human refused, and one that simply failed are not precedent. The pattern is matched
|
|
325
|
+
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.
|
|
278
328
|
|
|
279
329
|
```yaml
|
|
280
330
|
- id: 'dependency-needs-npm-view'
|
|
281
331
|
why: 'a dependency version must be measured before it is written.'
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
declare:
|
|
333
|
+
mechanism: 'precedent'
|
|
334
|
+
scope: { source: 'target.path', include: ['^(packages/[^/]+/)?package\.json$'] }
|
|
335
|
+
sources: { session: { transcript: true } }
|
|
336
|
+
supply: { session: 'pass' }
|
|
337
|
+
extract:
|
|
338
|
+
npmView:
|
|
339
|
+
- { op: 'source', of: 'session' }
|
|
340
|
+
- { op: 'toolUses', names: ['Bash'] }
|
|
341
|
+
- { op: 'filter', when: [{ field: 'succeeded', eq: true }] }
|
|
342
|
+
- { op: 'select', path: 'args.command' }
|
|
343
|
+
- { op: 'matches', re: '\bnpm view ' }
|
|
344
|
+
relate:
|
|
345
|
+
- { id: 'npm-view', relation: { op: 'nonEmpty', of: 'npmView' }, message: 'no successful npm view precedes this manifest edit' }
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
A tool call is evidence the same way: `toolUses` without `names`, then `field name` and
|
|
349
|
+
`matches` over the tool's name, or `toolUses` with `subagentType` for a spawn of one agent
|
|
350
|
+
kind. The other history mechanisms read the same snapshot — `phase-order` relates two
|
|
351
|
+
spawn ordinals with `ordered`, `turn-locality` keeps the user turns inside a time window
|
|
352
|
+
(`userTexts → ageMs → filter lte`), and `stated-ground` requires a user turn matching a
|
|
353
|
+
pattern; the last two are usually scoped on `command`, so that only the shell call they
|
|
354
|
+
apply to is judged.
|
|
355
|
+
|
|
356
|
+
**A caution on line anchors.** A declaration's `lines` step splits the text first, so `^`
|
|
357
|
+
inside `keyByPattern` or `matches` after it is the start of a line. A pattern that stops
|
|
358
|
+
mid-value — say at the first digit of a version — keys `4.0.5` and `4.0.6` alike, so a bump
|
|
359
|
+
adds nothing to an added-only difference and the discipline silently passes: make the
|
|
360
|
+
pattern span the whole value that can change. Both failure shapes compile, run, and answer
|
|
361
|
+
`passed`, so measure a new entry against a real file and a realistic edit.
|
|
362
|
+
|
|
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.
|
|
367
|
+
|
|
368
|
+
**`declare` — declaration family.** One judgment written as data, in the algebra grammar
|
|
369
|
+
the core publishes as `algebra-declaration.schema.json`: `judge = relate ∘ extract`. The
|
|
370
|
+
block carries the declaration's `scope`, `sources`, `supply`, `extract`, `relate`, and optional
|
|
371
|
+
`witness`; the entry's `id` is the declaration's name, so the block never carries a
|
|
372
|
+
`discipline` key, and `in`/`except`/`when` are refused — the `scope` block is the scope.
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
- id: 'db-only-under-knowledge'
|
|
376
|
+
why: 'a *.db file may exist only under _docs/knowledge/'
|
|
377
|
+
declare:
|
|
378
|
+
mechanism: 'naming'
|
|
379
|
+
scope: { source: 'target.path', include: ['\.db$'] }
|
|
380
|
+
extract:
|
|
381
|
+
outside:
|
|
382
|
+
- { op: 'source', of: 'target.path' }
|
|
383
|
+
- { op: 'matches', re: '^(?!_docs/knowledge/)' }
|
|
384
|
+
relate:
|
|
385
|
+
- id: 'placed'
|
|
386
|
+
relation: { op: 'empty', of: 'outside' }
|
|
387
|
+
message: '{value} is outside _docs/knowledge/'
|
|
288
388
|
```
|
|
289
389
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
the
|
|
315
|
-
`
|
|
316
|
-
`
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
390
|
+
This repository's live config carries the same declaration as `sqlite-only-under-knowledge`.
|
|
391
|
+
|
|
392
|
+
Each observation is judged as one **world** with seven source names: `target.path` (the
|
|
393
|
+
repo-relative path), `pre` and `post` (the file's text on the side the change carries —
|
|
394
|
+
a creation has no `pre`, a deletion no `post`), `state` (`{ pre, post }`, present only
|
|
395
|
+
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
|
|
397
|
+
call's command line — present on a shell call only, and a shell call that changes no file
|
|
398
|
+
is one world of its own, so a declaration scoped on `command` sees it while one scoped on
|
|
399
|
+
`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
|
|
406
|
+
file outside the target names it in a `sources` block, `sources: { en: { file:
|
|
407
|
+
'locales/en.json' } }`, and reads it as `{ op: 'source', of: 'en' }`; the path is
|
|
408
|
+
repo-relative (no leading `/`, no `..` segment) and the name may not be one of the seven. The
|
|
409
|
+
surface reads the file the way it observes the tree — the disk in a session, the index for a
|
|
410
|
+
staged commit, the `<to>` commit for a range — except that a named file the change itself
|
|
411
|
+
touches is read from the change's `post`, so both surfaces judge the same text. A second
|
|
412
|
+
kind, `sources: { spawns: { sidecar: true } }`, names the session's spawn-record channel
|
|
413
|
+
instead of a path — the subagent records the host keeps beside the transcript, supplied as
|
|
414
|
+
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
|
|
416
|
+
third kind, `sources: { session: { transcript: true } }`, names the session's own
|
|
417
|
+
conversation history — the user turns and tool calls the surface reads, handed to the
|
|
418
|
+
declaration as one snapshot whose entries carry their
|
|
419
|
+
observation ordinal; the history steps (`toolUses`, `userTexts`, `first`, `ageMs`) read it, and
|
|
420
|
+
`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
|
|
423
|
+
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' }`
|
|
425
|
+
followed by `select` on `agentType`; it derives the `actor` axis the `producer-owned` and
|
|
426
|
+
`actor-scope` mechanisms require, and this repository's live config carries one of each
|
|
427
|
+
(`tests-are-the-writers`, `commits-come-from-the-main-session`). A `supply` key must name
|
|
428
|
+
one of the seven fixed sources or one of the declaration's own `sources`; any other key is
|
|
429
|
+
refused. A source the change does not carry is absent, and the declaration's
|
|
430
|
+
`supply` block says what that means: `error` (the default) makes the call unjudgeable —
|
|
431
|
+
recorded `blocked` at either enforce level — `pass` leaves it unjudged, and `empty` reads
|
|
432
|
+
the absent side as an empty item list and judges on. `empty` is what lets an added-only
|
|
433
|
+
declaration see a creation as all-added and a deletion as adding nothing; it does not apply
|
|
434
|
+
to `state`, the paired source. A declaration comparing before with after therefore needs
|
|
435
|
+
`supply: { state: pass }` to let a file creation through.
|
|
436
|
+
|
|
437
|
+
A break is recorded like any other family's, with one addition: the telemetry row carries a
|
|
438
|
+
fifth field naming the elements the relation failed on (at most eight per relate entry, with
|
|
439
|
+
the true count beside them). A `skipped` row uses the same field for a reason token instead —
|
|
440
|
+
`no-observation` (the surface has no channel for what the entry reads), `config-fault` (the
|
|
441
|
+
block could not be assembled), or `supply-pass` (the declaration's own `supply: pass` let an
|
|
442
|
+
absent source through). Every declaration also names its `mechanism` — one of eighteen
|
|
443
|
+
catalogue names such as `naming`, `companion`, or `pairing` — and the validator refuses a
|
|
444
|
+
name whose shape the declaration does not match: the axes its sources derive (`change` for
|
|
445
|
+
the fixed names other than `actor`, `actor` for `actor`, `world` for a `file` or `sidecar`
|
|
446
|
+
source, `history` for a `transcript` source) and the relations it relates must fall inside
|
|
447
|
+
what that name admits. A block the compiler cannot resolve — a step name outside the
|
|
448
|
+
registry, an argument outside a step's keys — becomes a skip registration that names its
|
|
449
|
+
location on stderr and routes nothing. A shell write into the declaration's scope whose
|
|
450
|
+
result the judge can compute (a redirect, a heredoc, an append) is judged as the file change
|
|
451
|
+
it makes; one it cannot compute (`sed -i`, an opaque command) records `skipped`. The
|
|
452
|
+
declaration's own `witness` block joins the
|
|
453
|
+
human's witness as a second way to open a blocked verdict.
|
|
336
454
|
|
|
337
455
|
Adding a discipline is a data edit — no code, no plumbing. Custom judge bodies remain the
|
|
338
456
|
escape layer for the few rules data cannot express.
|
|
@@ -27,6 +27,13 @@ hand-rolled and the published JSON Schema is a sibling artifact the source never
|
|
|
27
27
|
*values* supplied by configs and adapters, so the core's agent-neutrality is a claim a grep
|
|
28
28
|
can check. Every other package depends on this one; this one depends on none of them.
|
|
29
29
|
|
|
30
|
+
**The judge and the two adapters name it as a `peerDependency`, not a dependency of their
|
|
31
|
+
own.** They share the vocabulary rather than each installing a copy — `SOURCE_KINDS` and
|
|
32
|
+
`parseInput` have to be one set of values for the validator and the engine to agree, and two
|
|
33
|
+
copies would disagree silently instead of failing at install time. The umbrella carries the
|
|
34
|
+
ordinary dependency that satisfies that peer, which is why a consumer still installs one
|
|
35
|
+
package and gets core transitively.
|
|
36
|
+
|
|
30
37
|
## The judged protocol
|
|
31
38
|
|
|
32
39
|
This is the contract the shipped judges speak: a judge receives a `CovenantInput` — parsed
|
|
@@ -40,6 +47,7 @@ type CovenantInput = {
|
|
|
40
47
|
toolCalls: { name: string; args?: Record<string, unknown>; fileChange?: FileChange }[];
|
|
41
48
|
subagentSpawns: { kind: string }[];
|
|
42
49
|
userMessages: { text: string }[];
|
|
50
|
+
actor?: { agentType?: string };
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
type FileChange =
|
|
@@ -55,7 +63,9 @@ fills into `name`; `kind` on a spawn is likewise a value. `FileChange` is a disc
|
|
|
55
63
|
union so that a deletion is first-class evidence rather than an unrepresentable case, and
|
|
56
64
|
impossible states — a deletion carrying resulting content, a creation carrying a baseline —
|
|
57
65
|
cannot be written down. `delete.pre` is absent when the baseline was a binary blob, because
|
|
58
|
-
a deletion needs no content to be judged.
|
|
66
|
+
a deletion needs no content to be judged. `actor` is the observation's actor as the host
|
|
67
|
+
envelope proves it — `agentType` inside a subagent, `{}` in the main session — and is absent
|
|
68
|
+
when the surface proves none; the judge never defaults it.
|
|
59
69
|
|
|
60
70
|
**Evidence has exactly one home: the call it belongs to.** `fileChange` absent means *this
|
|
61
71
|
call is unproven*, and no sibling call's evidence stands in for it.
|
|
@@ -102,10 +112,6 @@ Everything else here is reached through `polydeukes`.
|
|
|
102
112
|
not check that a namespace *name* is one anybody implements, and it does not look inside
|
|
103
113
|
the namespace at all. Unknown vocabulary inside `adapters.git` is rejected by the git
|
|
104
114
|
adapter's own validator, at its own layer — not here.
|
|
105
|
-
- **`requirePrecedent` evidence is layered the same way.** The core fully validates the
|
|
106
|
-
`command` key, because a shell command is the surface where an agent crosses into the
|
|
107
|
-
system. Every other key is validated for container shape alone — a flat object holding
|
|
108
|
-
exactly one key — and its value passes through verbatim for the owning adapter to judge.
|
|
109
115
|
- **The default transcript is a noop.** A consumer that injects no real transcript
|
|
110
116
|
converges on "nothing happened", which is the safe direction for a valve: it never opens.
|
|
111
117
|
Real transcripts live behind adapters.
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
The judge. Everything that turns a declared promise into a verdict lives here.
|
|
15
15
|
|
|
16
|
+
**This is the install unit for judgment.** An embedder that wants verdicts without the
|
|
17
|
+
umbrella's surfaces installs this package and provides `@polydeukes/core` alongside it — core
|
|
18
|
+
is a `peerDependency` here, so the vocabulary is shared rather than duplicated. The umbrella
|
|
19
|
+
is what satisfies that peer for an ordinary consumer.
|
|
20
|
+
|
|
16
21
|
| Unit | What it does |
|
|
17
22
|
|---|---|
|
|
18
23
|
| `runCovenant` wrapper | Runs a judge body, translates its non-blocking `1` into the blocking `2`, and logs every call. No covenant runs unmeasured |
|
|
@@ -24,20 +29,20 @@ The judge. Everything that turns a declared promise into a verdict lives here.
|
|
|
24
29
|
|
|
25
30
|
## Discipline families and meta-covenants
|
|
26
31
|
|
|
27
|
-
**A `disciplines:` entry
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
**A `disciplines:` entry is one declaration** — `judge = relate ∘ extract` over the
|
|
33
|
+
observation as a world. What the declaration's sources bind decides what evidence the
|
|
34
|
+
judgment needs, which is also what decides whether it can be judged on a given surface.
|
|
30
35
|
|
|
31
|
-
|
|
|
32
|
-
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
36
|
+
| Sources | Judges | Evidence needed |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| the fixed names `target.path` · `pre` · `post` · `state` · `changes` | The change itself | A file change |
|
|
39
|
+
| the fixed name `command` | The shell call's command line | A shell call — an Edit carries none |
|
|
40
|
+
| `{ transcript: true }` | Session history — was a qualifying call actually executed *before* this one | A session |
|
|
41
|
+
| `{ file: … }` · `{ sidecar: true }` | Another file, or the spawn-record channel | The surface's reader for it |
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
The writing guide for these entries is [the configuration reference's `disciplines`
|
|
44
|
+
section](./configuration.md#disciplines); the declaration grammar is the core's
|
|
45
|
+
`algebra-declaration.schema.json`.
|
|
41
46
|
|
|
42
47
|
**Three meta-covenants** protect the judging chain. They are covenants like any other; the
|
|
43
48
|
vocabulary below applies to them unchanged.
|
|
@@ -86,18 +91,21 @@ No import. The umbrella assembles this package for both surfaces.
|
|
|
86
91
|
not "nothing gets through" — it is that **no call passes unrecorded**. A new spelling
|
|
87
92
|
landing in `skipped` is the declared limit showing itself. A pass with no row at all, or
|
|
88
93
|
one recorded `passed` without a judgment, is the defect class.
|
|
89
|
-
- **
|
|
90
|
-
none, so a matching `
|
|
94
|
+
- **A declaration that reads the session cannot be judged without one.** On the commit
|
|
95
|
+
surface there is none, so a matching `precedent` (or any other transcript-reading)
|
|
96
|
+
declaration records `skipped` with the reason `supply-pass`. That is a permanent
|
|
91
97
|
condition of that surface.
|
|
92
|
-
- **
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
unresolvable entry cannot take down its siblings, the meta-covenants, and
|
|
100
|
-
which would leave no way to fix the config that caused it.
|
|
98
|
+
- **A declaration scoped on `command` is absent from the commit surface, and absent without
|
|
99
|
+
a row.** A staged diff carries no command line, so no world such a declaration observes is
|
|
100
|
+
admitted there. This leaves nothing in `.polydeukes/roi.log`, so the log cannot separate a
|
|
101
|
+
command discipline that never triggered from one whose surface never observed a command.
|
|
102
|
+
- **A declaration the compiler cannot resolve compiles to a skip registration** — routing
|
|
103
|
+
intact, no body: a step outside the registry, an argument outside a step's keys, a pattern
|
|
104
|
+
that does not compile, a mechanism whose shape the syntax does not fit. Assembly therefore
|
|
105
|
+
never throws: one unresolvable entry cannot take down its siblings, the meta-covenants, and
|
|
106
|
+
the valve, which would leave no way to fix the config that caused it. A source the world
|
|
107
|
+
lacks at judgment time is a different case — the declaration's own `supply` policy
|
|
108
|
+
disposes of it, and with no policy the body answers unjudgeable (exit 2), never upheld.
|
|
101
109
|
- **Complete containment is a non-goal.** There are no blocklists here — enumerating bypass
|
|
102
110
|
spellings is always one step behind, so the logic is inverted: a mention of a protected
|
|
103
111
|
path blocks unless proven safe. Residual vectors such as indirect path computation are
|