polydeukes 0.4.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 +11 -4
- package/README.md +21 -5
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +5 -6
- package/dist/bin.js +92 -38
- package/dist/claude-code-hook.d.ts +40 -17
- package/dist/claude-code-hook.js +187 -175
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +50 -36
- package/dist/covenant-check.js +167 -193
- package/dist/covenant-module.d.ts +25 -0
- package/dist/covenant-module.js +42 -0
- package/dist/docs/configuration.md +17 -9
- package/dist/docs/installation.md +42 -12
- package/dist/docs/reference/adapter-claude-code.md +6 -4
- package/dist/docs/reference/adapter-git.md +23 -10
- package/dist/docs/reference/configuration.md +265 -103
- package/dist/docs/reference/core.md +15 -7
- package/dist/docs/reference/covenant.md +32 -24
- package/dist/docs/reference/polydeukes.md +132 -32
- package/dist/docs/troubleshooting.md +37 -8
- package/dist/docs-query.d.ts +10 -10
- package/dist/docs-query.js +12 -12
- package/dist/explain.d.ts +25 -0
- package/dist/explain.js +153 -0
- package/dist/index.d.ts +11 -16
- package/dist/index.js +10 -15
- package/dist/init-claude-code.d.ts +31 -18
- package/dist/init-claude-code.js +254 -40
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +17 -15
- package/dist/load-config.js +13 -12
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.d.ts +23 -14
- package/dist/scaffold-project.js +97 -32
- package/dist/schema/polydeukes.schema.json +54 -81
- package/package.json +7 -7
|
@@ -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.
|
|
@@ -58,7 +63,7 @@ the same word for the same event. How to read a row is in
|
|
|
58
63
|
| `passed` | The call was judged and upheld the covenant |
|
|
59
64
|
| `blocked` | The call was judged and broke it |
|
|
60
65
|
| `witnessed` | A **blocked** verdict a human opened in person. Never silent, never a clean call |
|
|
61
|
-
| `advised` |
|
|
66
|
+
| `advised` | A break recorded without stopping the call — the default for every discipline entry on both surfaces, and the commit surface's outcome under `adapters.git.enforce: advise` |
|
|
62
67
|
| `skipped` | The call reached a registration that could not judge it. **Not a pass** — the recorded absence of a judgment |
|
|
63
68
|
| `unattributed` | A protected entry's on-disk state moved and no judgment row explains it. **Not a verdict** — no call is blocked or passed by it; the session surface writes it after comparing state against a stored baseline |
|
|
64
69
|
|
|
@@ -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
|
|
@@ -12,6 +12,11 @@ every other dependency in this repository runs one way, through the core alone.
|
|
|
12
12
|
this page the consumer-facing surface: the four scoped packages are transitive dependencies
|
|
13
13
|
you do not install and do not import.
|
|
14
14
|
|
|
15
|
+
The judge and the two adapters take the core as a `peerDependency` rather than a dependency
|
|
16
|
+
of their own, so they share one copy of the vocabulary instead of each carrying its own. The
|
|
17
|
+
umbrella's ordinary dependency on the core is what satisfies that peer, which is why
|
|
18
|
+
installing this one package is still all a consumer does.
|
|
19
|
+
|
|
15
20
|
| Package | Reference | Owns |
|
|
16
21
|
|---|---|---|
|
|
17
22
|
| `@polydeukes/core` | [core](./core.md) | The protocol, the config schema, telemetry |
|
|
@@ -21,34 +26,51 @@ you do not install and do not import.
|
|
|
21
26
|
|
|
22
27
|
## Subcommands
|
|
23
28
|
|
|
24
|
-
The bin is `pdks`, with `polydeukes` as an alias.
|
|
25
|
-
|
|
29
|
+
The bin is `pdks`, with `polydeukes` as an alias. Every argument form is matched against a
|
|
30
|
+
finite table: `covenant check` takes an optional domain flag, `init claude-code` and
|
|
31
|
+
`init grok` are exact, `explain` takes one word, and `docs` takes an optional topic.
|
|
26
32
|
|
|
27
33
|
### `pdks covenant check`
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
`pdks covenant check [--worktree | --range <base>..<head>]` — the commit-surface judgment
|
|
36
|
+
runner. It discovers the config at the working directory,
|
|
37
|
+
collects one observation of the repository through the git adapter, translates it into the
|
|
38
|
+
covenant input IR, and dispatches it through the same in-process judges the session hook calls.
|
|
39
|
+
Which observation is the domain flag's choice — the same violation receives the same verdict
|
|
40
|
+
in all three:
|
|
41
|
+
|
|
42
|
+
| Form | Domain | `pre` → `post` |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `pdks covenant check` | The staging area — what a pre-commit hook judges | HEAD blob → staged blob |
|
|
45
|
+
| `pdks covenant check --worktree` | The working tree, untracked (non-ignored) files included | HEAD blob → bytes on disk |
|
|
46
|
+
| `pdks covenant check --range <base>..<head>` | Two refs; `<base>...<head>` reads from their merge-base, the PR reading | base blob → head blob |
|
|
47
|
+
|
|
48
|
+
`--worktree` and `--range` are diagnostic calls — run them after a task, before a PR, or in
|
|
49
|
+
CI. Only the staged form is a gate, so only it can prompt for the witness token; the other
|
|
50
|
+
two report exit `2` without a prompt, since there is no commit for a human to open.
|
|
33
51
|
|
|
34
52
|
| Situation | Result |
|
|
35
53
|
|---|---|
|
|
36
|
-
|
|
|
54
|
+
| The domain's changes break nothing | exit `0` |
|
|
37
55
|
| A staged change breaks a covenant, `enforce: block`, config has a `witness` block | Prompts once on `/dev/tty` for the witness token; an unanswered or wrong answer exits `2` |
|
|
38
56
|
| The same, with no `witness` block in the config | exit `2` with no prompt — the valve is built from that block, so without it nothing can open a block |
|
|
39
|
-
| A
|
|
40
|
-
|
|
|
57
|
+
| A worktree or range change breaks a covenant, `enforce: block` | exit `2`, never a prompt |
|
|
58
|
+
| A change breaks a covenant, `enforce: advise` | One advisory line on stderr, exit `0`, recorded `advised` |
|
|
59
|
+
| Empty domain (nothing staged, a clean tree, identical refs) | exit `0` — an explicit pass, not a skipped run |
|
|
60
|
+
| A range naming a ref git cannot resolve, or two refs with no merge-base | exit `2`, one `blocked` row |
|
|
61
|
+
| `--range` without an argument, an argument without `..`, both flags at once, or an unknown flag | The usage line on stderr, exit `2` |
|
|
41
62
|
| No config, more than one config, or an invalid one | exit `2` |
|
|
42
63
|
| A judge body that cannot be loaded | exit `2` |
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
surface has no session to read: a match records
|
|
46
|
-
permanent condition of the commit surface, not a
|
|
65
|
+
Declarations that read the session (`precedent` and the other history mechanisms) assemble
|
|
66
|
+
here like any other entry, but this surface has no session to read: a match records
|
|
67
|
+
`skipped` and the commit proceeds. That is a permanent condition of the commit surface, not a
|
|
68
|
+
fault.
|
|
47
69
|
|
|
48
70
|
### `pdks init claude-code`
|
|
49
71
|
|
|
50
72
|
The session-surface installer. It proves that `polydeukes` resolves from the directory it
|
|
51
|
-
was invoked in **before writing anything**, then creates
|
|
73
|
+
was invoked in **before writing anything**, then creates six artifacts:
|
|
52
74
|
|
|
53
75
|
| Artifact | Kind |
|
|
54
76
|
|---|---|
|
|
@@ -56,6 +78,7 @@ was invoked in **before writing anything**, then creates five artifacts:
|
|
|
56
78
|
| `.claude/settings.json` | Merged — the PreToolUse registration is added to whatever the file already carries |
|
|
57
79
|
| `polydeukes.config.yaml` | Created — the starter policy, with a placeholder `languages` block |
|
|
58
80
|
| `.claude/rules/polydeukes.md` | Created — tells the AI partner to ask [`pdks docs`](#pdks-docs-topic) instead of searching the web |
|
|
81
|
+
| `.claude/skills/discipline-draft/SKILL.md` | Created — the classification procedure: a described problem becomes a config entry (judged at advise, or `draft: true`), and advised rows are consulted in the telemetry log |
|
|
59
82
|
| `.gitignore` | Appended — one line for `.polydeukes/` |
|
|
60
83
|
|
|
61
84
|
Nothing existing is overwritten: an artifact already present is reported as skipped and left
|
|
@@ -63,6 +86,22 @@ alone, so a re-run is a no-op. A precondition failure — the package not resolv
|
|
|
63
86
|
coexisting config spellings, an unparseable settings file — writes zero files and exits `2`,
|
|
64
87
|
never a half-wired tree.
|
|
65
88
|
|
|
89
|
+
### `pdks init grok`
|
|
90
|
+
|
|
91
|
+
The Grok session-surface installer. Same preflight and the same shared scaffold (config and
|
|
92
|
+
the `.polydeukes/` ignore line). A Grok-only tree gets four artifacts (hook JSON, the grok
|
|
93
|
+
delegator, config, ignore line) and no `.claude/` directory. The JSON registration carries
|
|
94
|
+
`timeout` 60 (the host default is 5 seconds; a timed-out hook fails open). If
|
|
95
|
+
`.claude/hooks/covenant-pretooluse.mjs` already exists, the JSON command points at that file
|
|
96
|
+
so the host does not spawn two judges. A later run of either installer retargets an
|
|
97
|
+
installer-generated grok-mjs command the same way, and the JSON matcher follows the
|
|
98
|
+
`.claude/settings.json` entry for that command (Grok collapses two registrations only when
|
|
99
|
+
command and matcher both match); a command pointed elsewhere is left alone.
|
|
100
|
+
|
|
101
|
+
An already-open Grok session keeps the hook snapshot from start; reload from the Hooks tab
|
|
102
|
+
(`r`) or start a new session. The witness valve does not open on Grok: the session log is
|
|
103
|
+
ACP `updates.jsonl`, not Claude's JSONL.
|
|
104
|
+
|
|
66
105
|
### `pdks docs [topic]`
|
|
67
106
|
|
|
68
107
|
The offline documentation reader. The guides and this reference layer ship inside the
|
|
@@ -96,29 +135,77 @@ the package.
|
|
|
96
135
|
`pdks init claude-code` writes a discovery file that points an AI partner at this
|
|
97
136
|
subcommand; see the artifact table above.
|
|
98
137
|
|
|
138
|
+
### `pdks explain`
|
|
139
|
+
|
|
140
|
+
The assembly reader. It loads the config at the working directory, assembles both surfaces'
|
|
141
|
+
registration sets through the same functions the two judgment runners use, and prints them
|
|
142
|
+
without judging — no judge thunk is called, no telemetry row is written, no transcript is
|
|
143
|
+
read.
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
pdks explain — polydeukes.config.yaml
|
|
147
|
+
|
|
148
|
+
surface: session (claude-code hook) · disciplines: advise unless enforce: block · meta: block
|
|
149
|
+
registrations 43 · declare 21 · skip 19 · meta 3 · draft 1
|
|
150
|
+
meta self-mod paths 14 (common; includes the config file itself)
|
|
151
|
+
declare covenant-vocabulary added-only · change · empty nothing-added · scope target.path · include 1 · exclude 1 · sources 0 · valve — · why ✓
|
|
152
|
+
declare pnpm-only forbidden-command · change · empty no-npm-mutation · scope command · include 0 · exclude 0 · sources 0 · valve — · why ✓
|
|
153
|
+
declare manifest-needs-evidence precedent · history · nonEmpty npm-view, context7 · scope target.path · include 1 · exclude 0 · sources 1 (transcript 1) · valve — · why ✓
|
|
154
|
+
skip covenant-vocabulary a shell write in scope whose result this layer cannot compute
|
|
155
|
+
...
|
|
156
|
+
surface: commit (git pre-commit) · enforce: advise · disciplines: advise unless enforce: block
|
|
157
|
+
registrations 24 · declare 22 · skip 1 · meta 1 · draft 1
|
|
158
|
+
declare manifest-needs-evidence precedent · history · nonEmpty npm-view, context7 · scope target.path · include 1 · exclude 0 · sources 1 (transcript 1) · valve — · why ✓
|
|
159
|
+
declare sqlite-only-under-knowledge naming · change · empty placed · scope target.path · include 1 · exclude 0 · sources 0 · valve — · why ✓
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
One line per registration, in the order the surface dispatches them. The kind column has
|
|
163
|
+
four words: `meta` (the registrations protecting the judging chain — `self-mod`,
|
|
164
|
+
`shell-mod`, and on the session surface `transcript-mod`), `declare` (a declaration entry,
|
|
165
|
+
with its mechanism, axes and relations, its scope source, the sizes of its include and
|
|
166
|
+
exclude lists, its sources, and whether it carries a valve and a `why`), `skip` (a
|
|
167
|
+
registration that records `skipped` instead of judging, with the reason the compiler gave —
|
|
168
|
+
the reason that otherwise reaches stderr only on a config fault), and `draft` (an unpromoted
|
|
169
|
+
`draft: true` entry, shown on both surfaces since it belongs to neither). `registrations`
|
|
170
|
+
counts `meta`, `declare`, and `skip`; `draft` is tallied apart because it never becomes a
|
|
171
|
+
registration.
|
|
172
|
+
The commit surface's header also names its `adapters.git.enforce` level, since an advising
|
|
173
|
+
surface records the same table but blocks nothing.
|
|
174
|
+
|
|
175
|
+
The session surface is rendered as the hook sees it under a normal payload — with a
|
|
176
|
+
transcript present — so `transcript-mod` and the session-reading declarations appear as they
|
|
177
|
+
do in a session; on the commit surface those declarations record `skipped` when matched,
|
|
178
|
+
which is that surface's permanent condition.
|
|
179
|
+
|
|
180
|
+
| Call | Result |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `pdks explain` | Both surfaces on stdout, exit `0` |
|
|
183
|
+
| `pdks explain <anything>` | The usage line on stderr, exit `2` |
|
|
184
|
+
| no config, two configs, or an invalid one | `pdks explain: <reason>` on stderr, stdout at zero bytes, exit `2` |
|
|
185
|
+
|
|
99
186
|
### Any other argument form
|
|
100
187
|
|
|
101
|
-
Anything that is not one of these forms writes
|
|
102
|
-
`usage: pdks covenant check |
|
|
103
|
-
exits `2`.
|
|
188
|
+
Anything that is not one of these forms writes the usage line —
|
|
189
|
+
`usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain |
|
|
190
|
+
pdks init claude-code | pdks init grok | pdks docs [topic]` — to stderr and exits `2`.
|
|
104
191
|
|
|
105
192
|
## Exit codes
|
|
106
193
|
|
|
107
194
|
Three codes exist, and they live at two layers. **What a consumer's hook observes is only
|
|
108
|
-
`0` or `2`** — both composition roots
|
|
195
|
+
`0` or `2`** — both composition roots resolve to a named outcome carrying `exitCode: 0 | 2`.
|
|
109
196
|
|
|
110
197
|
| Code | Constant | Emitted by | Means |
|
|
111
198
|
|---|---|---|---|
|
|
112
|
-
| `0` | `EXIT_UPHOLD` | Judge
|
|
113
|
-
| `1` | `EXIT_BREAK_NON_BLOCKING` | Judge
|
|
199
|
+
| `0` | `EXIT_UPHOLD` | Judge outcome, wrapper, bin | The promise was upheld — the call or commit proceeds |
|
|
200
|
+
| `1` | `EXIT_BREAK_NON_BLOCKING` | Judge outcome only | A break reported as a signal. The wrapper translates it — into `2` under `enforce: block`, into `0` + an `advised` row under `advise`. It never reaches the surface either way |
|
|
114
201
|
| `2` | `EXIT_BREAK_BLOCKING` | Wrapper, bin, fail-closed paths | The call or commit is refused |
|
|
115
202
|
|
|
116
|
-
The asymmetry is the protocol's responsibility boundary. A
|
|
117
|
-
promise was broken and
|
|
118
|
-
wrapper, and that is the one place `enforce` is read. A
|
|
119
|
-
reasoned about without knowing whether the surface it runs under blocks or advises.
|
|
120
|
-
verdict relaxes: every unjudgeable outcome —
|
|
121
|
-
stays `2` at either level.
|
|
203
|
+
The asymmetry is the protocol's responsibility boundary. A judge decides *whether* a
|
|
204
|
+
promise was broken and answers `0` or `1` in its outcome; deciding what a break *costs* belongs
|
|
205
|
+
to the wrapper, and that is the one place `enforce` is read. A judge can therefore be run,
|
|
206
|
+
tested, and reasoned about without knowing whether the surface it runs under blocks or advises.
|
|
207
|
+
Only the verdict relaxes: every unjudgeable outcome — an outcome of `2` or higher, a throw
|
|
208
|
+
from the judge — stays `2` at either level.
|
|
122
209
|
|
|
123
210
|
**Everything unjudgeable resolves to `2`.** A missing config, an invalid one, an
|
|
124
211
|
unparseable payload, a judge body that was never built — each fails closed. The one
|
|
@@ -135,7 +222,9 @@ This is the whole public API; the scoped packages are not part of it.
|
|
|
135
222
|
**Type signature:**
|
|
136
223
|
|
|
137
224
|
```ts
|
|
138
|
-
function loadConfig(
|
|
225
|
+
function loadConfig(spec: LoadConfigSpec): LoadedConfig;
|
|
226
|
+
|
|
227
|
+
type LoadConfigSpec = { rootDir: string };
|
|
139
228
|
|
|
140
229
|
type LoadedConfig = {
|
|
141
230
|
config: ResolvedConfig; // protectedPaths already includes the config file itself
|
|
@@ -153,18 +242,27 @@ a silently unprotected project.
|
|
|
153
242
|
**Type signature:**
|
|
154
243
|
|
|
155
244
|
```ts
|
|
156
|
-
function runCovenantCheck(spec: CovenantCheckSpec): Promise<
|
|
245
|
+
function runCovenantCheck(spec: CovenantCheckSpec): Promise<CovenantCheckOutcome>;
|
|
246
|
+
|
|
247
|
+
type CovenantCheckOutcome = { exitCode: 0 | 2 };
|
|
157
248
|
|
|
158
249
|
type CovenantCheckSpec = {
|
|
159
|
-
repoRoot: string; // config discovery and
|
|
250
|
+
repoRoot: string; // config discovery and collection anchor here
|
|
160
251
|
telemetryPath?: string; // overrides the config's log path
|
|
161
252
|
covenantDist?: string; // overrides the resolved judge directory
|
|
162
253
|
ttyPrompt?: (prompt: string) => string | null; // the TTY valve seam
|
|
254
|
+
domain?: CheckDomain; // which observation to judge; absent = staged
|
|
163
255
|
};
|
|
256
|
+
|
|
257
|
+
type CheckDomain =
|
|
258
|
+
| { kind: 'staged' }
|
|
259
|
+
| { kind: 'worktree' }
|
|
260
|
+
| { kind: 'range'; base: string; head: string; ancestry?: 'merge-base' };
|
|
164
261
|
```
|
|
165
262
|
|
|
166
263
|
The commit surface's composition root — what [`pdks covenant
|
|
167
|
-
check`](#pdks-covenant-check) runs.
|
|
264
|
+
check`](#pdks-covenant-check) runs. `ancestry: 'merge-base'` is the `<base>...<head>`
|
|
265
|
+
reading; the adapter resolves the merge-base.
|
|
168
266
|
|
|
169
267
|
`ttyPrompt` absent means a non-TTY environment, and the valve then has no way to open — an
|
|
170
268
|
agent-spawned commit and a CI run reach the same state. The valve is a human at a terminal
|
|
@@ -175,7 +273,9 @@ or nothing.
|
|
|
175
273
|
**Type signature:**
|
|
176
274
|
|
|
177
275
|
```ts
|
|
178
|
-
function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<
|
|
276
|
+
function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<ClaudeCodeHookOutcome>;
|
|
277
|
+
|
|
278
|
+
type ClaudeCodeHookOutcome = { exitCode: 0 | 2 };
|
|
179
279
|
|
|
180
280
|
type ClaudeCodeHookSpec = {
|
|
181
281
|
repoRoot: string; // config discovery and discipline glob scoping anchor here
|
|
@@ -201,8 +301,8 @@ needs no second dependency.
|
|
|
201
301
|
|
|
202
302
|
| Specifier | Carries |
|
|
203
303
|
|---|---|
|
|
204
|
-
| `polydeukes` | The barrel — `loadConfig`, `runCovenantCheck`,
|
|
205
|
-
| `polydeukes/claude-code` | `runClaudeCodeHook` and `
|
|
304
|
+
| `polydeukes` | The barrel — `loadConfig`, `runCovenantCheck`, their spec and outcome types, `ResolvedConfig`. The session hook lives on its own subpath below and nowhere else |
|
|
305
|
+
| `polydeukes/claude-code` | `runClaudeCodeHook`, `ClaudeCodeHookSpec`, and `ClaudeCodeHookOutcome` alone |
|
|
206
306
|
| `polydeukes/schema.json` | The config JSON Schema, copied from the core at build time |
|
|
207
307
|
|
|
208
308
|
The generated hook delegator imports the subpath, not the barrel. ESM imports are eager, so
|
|
@@ -29,7 +29,10 @@ Discovery looks for exactly these, in this order: `polydeukes.config.yaml`,
|
|
|
29
29
|
defaults — silent defaults would mean silently unprotected.
|
|
30
30
|
|
|
31
31
|
**Recovery.** Restore the file from git. On the session path,
|
|
32
|
-
`pnpm exec pdks init claude-code`
|
|
32
|
+
`pnpm exec pdks init claude-code` or `pdks init grok` recreates files that
|
|
33
|
+
are absent. An existing grok JSON is not rewritten except when its `command`
|
|
34
|
+
still names the grok delegator and a Claude delegator is on disk — then that
|
|
35
|
+
command is retargeted and the matcher follows the settings entry. On the commit path the
|
|
33
36
|
config is hand-written — the [install guide](./installation.md)'s commit-surface section
|
|
34
37
|
has a starting point.
|
|
35
38
|
|
|
@@ -58,6 +61,30 @@ empty `languages` block, the schema's one required entry.
|
|
|
58
61
|
**Recovery.** Fix the named key in the named file. The error is specific on purpose — no
|
|
59
62
|
rewrite-and-hope needed.
|
|
60
63
|
|
|
64
|
+
## A Grok session does not pick up a newly installed hook
|
|
65
|
+
|
|
66
|
+
**Symptom.** `pdks init grok` reported created files, but this session's tool calls still
|
|
67
|
+
leave no telemetry row.
|
|
68
|
+
|
|
69
|
+
**Cause.** Grok loads hooks at session start. An already-open session keeps that snapshot.
|
|
70
|
+
|
|
71
|
+
**Recovery.** Press `r` in the Hooks tab, or start a new session. The witness valve also
|
|
72
|
+
does not open on Grok — a block is recovered from another terminal or the commit-surface TTY.
|
|
73
|
+
|
|
74
|
+
## One Grok tool call leaves two telemetry rows
|
|
75
|
+
|
|
76
|
+
**Symptom.** In a tree wired for both Claude Code and Grok, every `write` or
|
|
77
|
+
`run_terminal_command` in a Grok session appends two rows to `.polydeukes/roi.log`,
|
|
78
|
+
milliseconds apart.
|
|
79
|
+
|
|
80
|
+
**Cause.** Grok reads `.claude/settings.json` as well as `.grok/hooks/*.json` and collapses
|
|
81
|
+
the two registrations only when `command` and `matcher` are both identical. A grok JSON
|
|
82
|
+
whose matcher differs from the settings entry spawns the judge a second time.
|
|
83
|
+
|
|
84
|
+
**Recovery.** Make the grok JSON's `matcher` the same string as the settings entry that
|
|
85
|
+
registers the same command. Re-running either installer does it: every grok entry that
|
|
86
|
+
names the Claude hook takes the settings entry's matcher.
|
|
87
|
+
|
|
61
88
|
## `pdks init claude-code` refuses to run
|
|
62
89
|
|
|
63
90
|
**Symptom.** The installer prints an install command and exits 2 without creating anything.
|
|
@@ -144,16 +171,18 @@ that *cannot judge* (missing or invalid config, an unresolvable judge) still exi
|
|
|
144
171
|
|
|
145
172
|
## `skipped` rows on the commit surface
|
|
146
173
|
|
|
147
|
-
**Symptom.** A `
|
|
148
|
-
|
|
174
|
+
**Symptom.** A `precedent` declaration that judges normally in sessions always lands as
|
|
175
|
+
`skipped` on commits.
|
|
149
176
|
|
|
150
|
-
**Cause.**
|
|
151
|
-
actually executed before this change. A commit has no session to read, so the
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
**Cause.** A declaration that reads the session judges *session history* — was the required
|
|
178
|
+
step actually executed before this change. A commit has no session to read, so the
|
|
179
|
+
declaration's own `supply: { session: 'pass' }` takes over: when its scope matches a staged
|
|
180
|
+
change it records `skipped` with the reason `supply-pass` and the entry's id, and proceeds.
|
|
154
181
|
|
|
155
182
|
**Recovery.** None needed — this is a declared condition of the surface, not a defect. The
|
|
156
|
-
row is the point: a gate that did nothing says so in the data.
|
|
183
|
+
row is the point: a gate that did nothing says so in the data. `pdks explain` shows the
|
|
184
|
+
same fact before any commit: the entry appears under the commit surface with its
|
|
185
|
+
`transcript` source, which that surface never supplies. Such a declaration is
|
|
157
186
|
really a session-surface tool — on a project that wires only the commit surface, such an
|
|
158
187
|
entry only ever buys telemetry, so declare it where an AI partner's session exists to be
|
|
159
188
|
judged. The row appears only when the entry's scope actually matched, so an unrelated
|
package/dist/docs-query.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `queryDocs` — the offline documentation query
|
|
2
|
+
* `queryDocs` — the offline documentation query.
|
|
3
3
|
*
|
|
4
4
|
* The bundled English guides, answered from the installed version. An AI partner that
|
|
5
5
|
* searches the web gets whatever release the internet indexed; this returns the document
|
|
6
6
|
* that shipped with the code doing the judging, with no network at all.
|
|
7
7
|
*
|
|
8
|
-
* The domain is the five topics below and nothing else
|
|
9
|
-
*
|
|
8
|
+
* The domain is the five topics below and nothing else. An unknown topic throws instead of
|
|
9
|
+
* resolving to something near it: an answer to a question we never mapped is
|
|
10
10
|
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
11
|
*
|
|
12
|
-
* Every failure throws
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Every failure throws so the bin can leave stdout at zero bytes and exit 2. Text written
|
|
13
|
+
* halfway is read as the document and quoted as the document — the same direction the
|
|
14
|
+
* judging surface fails in, for the same reason.
|
|
15
15
|
*/
|
|
16
|
-
/** The finite query domain
|
|
16
|
+
/** The finite query domain — the topic list `pdks docs` prints with no argument. */
|
|
17
17
|
export declare const TOPICS: readonly ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
18
18
|
/**
|
|
19
19
|
* The body of one section of `markdown`: from the line equal to `heading` up to just before
|
|
20
|
-
* the next heading of the same or a higher level, returned verbatim
|
|
20
|
+
* the next heading of the same or a higher level, returned verbatim.
|
|
21
21
|
*
|
|
22
22
|
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
23
23
|
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
@@ -36,10 +36,10 @@ export type QueryDocsSpec = {
|
|
|
36
36
|
topic?: string;
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
|
-
* Answer one documentation query
|
|
39
|
+
* Answer one documentation query.
|
|
40
40
|
*
|
|
41
41
|
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
42
|
-
* With one, it is the
|
|
42
|
+
* With one, it is the mapped section body followed by the bundled reference to read next.
|
|
43
43
|
*/
|
|
44
44
|
export declare function queryDocs(spec: QueryDocsSpec): {
|
|
45
45
|
text: string;
|
package/dist/docs-query.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `queryDocs` — the offline documentation query
|
|
2
|
+
* `queryDocs` — the offline documentation query.
|
|
3
3
|
*
|
|
4
4
|
* The bundled English guides, answered from the installed version. An AI partner that
|
|
5
5
|
* searches the web gets whatever release the internet indexed; this returns the document
|
|
6
6
|
* that shipped with the code doing the judging, with no network at all.
|
|
7
7
|
*
|
|
8
|
-
* The domain is the five topics below and nothing else
|
|
9
|
-
*
|
|
8
|
+
* The domain is the five topics below and nothing else. An unknown topic throws instead of
|
|
9
|
+
* resolving to something near it: an answer to a question we never mapped is
|
|
10
10
|
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
11
|
*
|
|
12
|
-
* Every failure throws
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Every failure throws so the bin can leave stdout at zero bytes and exit 2. Text written
|
|
13
|
+
* halfway is read as the document and quoted as the document — the same direction the
|
|
14
|
+
* judging surface fails in, for the same reason.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
/** The finite query domain
|
|
18
|
+
/** The finite query domain — the topic list `pdks docs` prints with no argument. */
|
|
19
19
|
export const TOPICS = ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
20
|
-
/** The
|
|
20
|
+
/** The mapping, as data: which document answers a topic, and what to read next. */
|
|
21
21
|
const TOPIC_MAP = {
|
|
22
22
|
install: {
|
|
23
23
|
sections: [{ file: 'installation.md' }],
|
|
@@ -55,7 +55,7 @@ function headingLevel(line) {
|
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* The body of one section of `markdown`: from the line equal to `heading` up to just before
|
|
58
|
-
* the next heading of the same or a higher level, returned verbatim
|
|
58
|
+
* the next heading of the same or a higher level, returned verbatim.
|
|
59
59
|
*
|
|
60
60
|
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
61
61
|
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
@@ -101,7 +101,7 @@ export function extractSection(markdown, heading) {
|
|
|
101
101
|
if (start === -1) {
|
|
102
102
|
throw new Error(`heading not found: ${heading}`);
|
|
103
103
|
}
|
|
104
|
-
// A section that closes the document ends at end of file; the
|
|
104
|
+
// A section that closes the document ends at end of file; the topic map points at one.
|
|
105
105
|
return lines.slice(start).join('\n');
|
|
106
106
|
}
|
|
107
107
|
function readSection(docsRoot, section) {
|
|
@@ -115,10 +115,10 @@ function readSection(docsRoot, section) {
|
|
|
115
115
|
return section.heading === undefined ? markdown : extractSection(markdown, section.heading);
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
* Answer one documentation query
|
|
118
|
+
* Answer one documentation query.
|
|
119
119
|
*
|
|
120
120
|
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
121
|
-
* With one, it is the
|
|
121
|
+
* With one, it is the mapped section body followed by the bundled reference to read next.
|
|
122
122
|
*/
|
|
123
123
|
export function queryDocs(spec) {
|
|
124
124
|
if (spec.topic === undefined) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pdks explain` — render both surfaces' assembled registration sets without judging.
|
|
3
|
+
*
|
|
4
|
+
* This module calls the composition roots' OWN assembly functions and renders what they
|
|
5
|
+
* return, so it reports the table the judgment uses rather than a second opinion about it.
|
|
6
|
+
*
|
|
7
|
+
* It never dispatches, never writes telemetry or a baseline, and never opens a transcript
|
|
8
|
+
* file — the session assembly receives core's `noopTranscript`, which answers queries with
|
|
9
|
+
* nothing and reads no disk. Every failure throws: an answer that cannot be given is never
|
|
10
|
+
* given halfway.
|
|
11
|
+
*/
|
|
12
|
+
/** `explain` input — the repository whose config is read. */
|
|
13
|
+
export type ExplainSpec = {
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
18
|
+
*
|
|
19
|
+
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
20
|
+
* exists here exactly as it does under a normal hook payload — the path is never read,
|
|
21
|
+
* because the injected transcript is the no-op one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function explain(spec: ExplainSpec): Promise<{
|
|
24
|
+
text: string;
|
|
25
|
+
}>;
|