Skip to content

Write disciplines

A discipline describes a practice you want checked. Choose the observed files or session evidence, write an extraction and relation, then exercise both a violation and a valid case. Leave enforcement at advise until you decide that the observed results justify blocking.

For the complete syntax of relations and extraction steps, see the Declaration language reference.

This declaration compares the key sets of two JSON translation files, including nested keys. Save the complete YAML below as polydeukes.config.yaml in an example project, not over an existing project’s configuration. In an existing project, copy only the discipline entry — the YAML below carries no protectedPaths and no witness block, so saving it over a generated config removes the witness valve. The first-judgment tutorial supplies installation steps.

languages:
json:
productionGlob: 'locales/**/*.json'
testCmd: 'pnpm test'
telemetry:
logPath: '.polydeukes/roi.log'
disciplines:
- id: 'locale-key-parity'
why: 'the ko and en locales must carry the same keys'
declare:
mechanism: 'pairing'
sources:
ko: { file: 'locales/ko.json' }
en: { file: 'locales/en.json' }
supply: { ko: 'error', en: 'error' }
scope: { source: 'target.path', include: ['^locales/(ko|en)\.json$'] }
extract:
koKeys:
- { op: 'source', of: 'ko' }
- { op: 'json' }
- { op: 'flattenKeys' }
enKeys:
- { op: 'source', of: 'en' }
- { op: 'json' }
- { op: 'flattenKeys' }
relate:
- id: 'parity'
relation: { op: 'equal', of: ['koKeys', 'enKeys'] }
messageBySide:
left: '{key} is in ko only'
right: '{key} is in en only'

flattenKeys extracts keys, not translation values. equal compares both directions and messageBySide reports which file has an unmatched key. The default enforcement is advise. Both source files must exist and contain valid JSON. The change-set surface reads them from the chosen observation; a session edit uses that edit’s proposed new contents for the file it changes.

From the example project’s root, prepare matching tracked files. The commit below requires your usual local git identity; it creates the baseline for the worktree comparison.

Terminal window
mkdir -p locales
printf '{"home":"Home"}\n' > locales/en.json
printf '{"home":"홈"}\n' > locales/ko.json
git add locales/en.json locales/ko.json
git commit -m 'docs: prepare locale example'
printf '{"home":"Home","settings":"Settings"}\n' > locales/en.json
git diff HEAD | pnpm exec pdks covenant check --diff

Expect an advised diagnostic for locale-key-parity naming settings as present only in English. The command still exits 0. Fix the mismatch and run the same observation again:

Terminal window
printf '{"home":"홈","settings":"설정"}\n' > locales/ko.json
git diff HEAD | pnpm exec pdks covenant check --diff

The parity diagnostic should disappear. The values differ intentionally; the keys now match. Restore the two example files to their committed baseline when finished:

Terminal window
git restore -- locales/en.json locales/ko.json

git diff HEAD reports changes against the last commit, so an untracked file needs git add -N before it appears in the diff. This example commits a baseline to exercise modifications and make cleanup predictable. A declaration does not run merely because its source exists: at least one observed change must match its scope.

Choose a discipline list by the sources the declaration reads. For example, a companion declaration reading only file sources belongs in disciplines; one reading changes belongs in changeSetDisciplines.

The declaration reads List Examples
the changed file and file sources only disciplines the file-shaped types — added-only, one-way markers, self-absolution bans, controlled vocabulary, naming, companion over a file source, fingerprint sync, monotonic order
the transcript sessionDisciplines the four history types — precedent, phase order, turn locality, stated ground
the actor sessionDisciplines producer-owned, actor scope
the command line sessionDisciplines forbidden command
the spawn-record channel (sidecar) sessionDisciplines any declaration binding { sidecar: true }
changes changeSetDisciplines pairing over a change set — implies between two paths that must move together

Write the entry in the list its sources point at. A misplaced entry is a load-time error that names the entry, the channels it reads, and the list it belongs in, so the fix is to move the body unchanged. The rule itself and the error shapes are in the configuration reference.

Include the witness block’s sources when choosing a list. If its extract reads the transcript, the whole entry belongs in sessionDisciplines.

Posture on an unattended real-time surface

Section titled “Posture on an unattended real-time surface”

An unattended adapter hook or SDK caller needs an explicit response to blocked and advised judgments.

Use enforce: block when an advisory alone will not lead the loop to correct a violation. The caller must give the model the reason and a way to retry. Keep an entry at advise if the loop cannot act on its diagnostic, and arrange for someone to review the recorded violations. Choose the level based on observations from that loop.

Return diagnostics to the caller. checkCovenant returns { verdict: 'blocked', reason } with the judge’s stderr, or { verdict: 'upheld', advisories } with the advisory output of an exit-0 run. The SDK accepts no separate witness argument. The caller decides whether to send diagnostics to the model, record them in an issue or log, and retry or stop. An advisory reaches the model only if the caller forwards it. See the SDK reference for the return types.

The SDK defaults to enforce: 'block' for the whole run. At that level, protected paths and entries with enforce: block can stop the call; other discipline violations remain advised. The three agent adapters use the same setting.

If the promise is real but the grammar cannot express it yet, write a draft.

languages:
json:
productionGlob: 'locales/**/*.json'
testCmd: 'pnpm test'
disciplines:
- id: 'benchmark-supports-performance-claim'
why: 'a performance claim must be supported by a fresh benchmark run during judgment.'
draft: true

Use draft: true only for a promise the current engine cannot judge. A draft does not produce a verdict or telemetry. It is still part of the config, so the file remains loadable.

After you save the config, run the judgment path that can actually see it.

  • git diff HEAD | pdks covenant check --diff shows the same entry against the current tree.
  • pdks explain shows the registration and whether it is a declare or a draft.
  • A one-sided edit to locales/en.json or locales/ko.json is a good smoke test for the pairing example.

If no judgment appears, first check the observation: is the file included rather than ignored, did it change in the selected comparison, does the scope match, and can the surface supply the evidence? Inspect pdks explain and the telemetry log for config-fault, no-observation, or supply-pass. Do not treat a missing diagnostic as proof that the declaration works.

The draft above is deliberately different from key pairing. The current engine does not run a new benchmark during judgment. It can compare supplied evidence, but that is not the same promise. See declarations and their limits.