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
|
@@ -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 |
|
|
@@ -22,8 +27,8 @@ you do not install and do not import.
|
|
|
22
27
|
## Subcommands
|
|
23
28
|
|
|
24
29
|
The bin is `pdks`, with `polydeukes` as an alias. Every argument form is matched against a
|
|
25
|
-
finite table: `covenant check` takes an optional domain flag, `init claude-code`
|
|
26
|
-
`explain` takes one word, and `docs` takes an optional topic.
|
|
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.
|
|
27
32
|
|
|
28
33
|
### `pdks covenant check`
|
|
29
34
|
|
|
@@ -57,9 +62,10 @@ two report exit `2` without a prompt, since there is no commit for a human to op
|
|
|
57
62
|
| No config, more than one config, or an invalid one | exit `2` |
|
|
58
63
|
| A judge body that cannot be loaded | exit `2` |
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
surface has no session to read: a match records
|
|
62
|
-
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.
|
|
63
69
|
|
|
64
70
|
### `pdks init claude-code`
|
|
65
71
|
|
|
@@ -80,6 +86,22 @@ alone, so a re-run is a no-op. A precondition failure — the package not resolv
|
|
|
80
86
|
coexisting config spellings, an unparseable settings file — writes zero files and exits `2`,
|
|
81
87
|
never a half-wired tree.
|
|
82
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
|
+
|
|
83
105
|
### `pdks docs [topic]`
|
|
84
106
|
|
|
85
107
|
The offline documentation reader. The guides and this reference layer ship inside the
|
|
@@ -123,37 +145,37 @@ read.
|
|
|
123
145
|
```text
|
|
124
146
|
pdks explain — polydeukes.config.yaml
|
|
125
147
|
|
|
126
|
-
surface: session (claude-code hook)
|
|
127
|
-
registrations
|
|
128
|
-
meta self-mod
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
155
|
...
|
|
133
|
-
surface: commit (git pre-commit) · enforce: advise
|
|
134
|
-
registrations
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
draft bilingual-docs-sync unpromoted — no judgment
|
|
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 ✓
|
|
138
160
|
```
|
|
139
161
|
|
|
140
162
|
One line per registration, in the order the surface dispatches them. The kind column has
|
|
141
|
-
|
|
142
|
-
`shell-mod`, and on the session surface `transcript-mod`), `
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
that
|
|
146
|
-
|
|
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
|
|
147
169
|
`draft: true` entry, shown on both surfaces since it belongs to neither). `registrations`
|
|
148
|
-
counts
|
|
149
|
-
|
|
170
|
+
counts `meta`, `declare`, and `skip`; `draft` is tallied apart because it never becomes a
|
|
171
|
+
registration.
|
|
150
172
|
The commit surface's header also names its `adapters.git.enforce` level, since an advising
|
|
151
173
|
surface records the same table but blocks nothing.
|
|
152
174
|
|
|
153
175
|
The session surface is rendered as the hook sees it under a normal payload — with a
|
|
154
|
-
transcript present — so `transcript-mod` and the
|
|
155
|
-
session; the commit surface
|
|
156
|
-
permanent condition.
|
|
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.
|
|
157
179
|
|
|
158
180
|
| Call | Result |
|
|
159
181
|
|---|---|
|
|
@@ -165,12 +187,12 @@ permanent condition.
|
|
|
165
187
|
|
|
166
188
|
Anything that is not one of these forms writes the usage line —
|
|
167
189
|
`usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain |
|
|
168
|
-
pdks init claude-code | pdks docs [topic]` — to stderr and exits `2`.
|
|
190
|
+
pdks init claude-code | pdks init grok | pdks docs [topic]` — to stderr and exits `2`.
|
|
169
191
|
|
|
170
192
|
## Exit codes
|
|
171
193
|
|
|
172
194
|
Three codes exist, and they live at two layers. **What a consumer's hook observes is only
|
|
173
|
-
`0` or `2`** — both composition roots
|
|
195
|
+
`0` or `2`** — both composition roots resolve to a named outcome carrying `exitCode: 0 | 2`.
|
|
174
196
|
|
|
175
197
|
| Code | Constant | Emitted by | Means |
|
|
176
198
|
|---|---|---|---|
|
|
@@ -200,7 +222,9 @@ This is the whole public API; the scoped packages are not part of it.
|
|
|
200
222
|
**Type signature:**
|
|
201
223
|
|
|
202
224
|
```ts
|
|
203
|
-
function loadConfig(
|
|
225
|
+
function loadConfig(spec: LoadConfigSpec): LoadedConfig;
|
|
226
|
+
|
|
227
|
+
type LoadConfigSpec = { rootDir: string };
|
|
204
228
|
|
|
205
229
|
type LoadedConfig = {
|
|
206
230
|
config: ResolvedConfig; // protectedPaths already includes the config file itself
|
|
@@ -218,7 +242,9 @@ a silently unprotected project.
|
|
|
218
242
|
**Type signature:**
|
|
219
243
|
|
|
220
244
|
```ts
|
|
221
|
-
function runCovenantCheck(spec: CovenantCheckSpec): Promise<
|
|
245
|
+
function runCovenantCheck(spec: CovenantCheckSpec): Promise<CovenantCheckOutcome>;
|
|
246
|
+
|
|
247
|
+
type CovenantCheckOutcome = { exitCode: 0 | 2 };
|
|
222
248
|
|
|
223
249
|
type CovenantCheckSpec = {
|
|
224
250
|
repoRoot: string; // config discovery and collection anchor here
|
|
@@ -247,7 +273,9 @@ or nothing.
|
|
|
247
273
|
**Type signature:**
|
|
248
274
|
|
|
249
275
|
```ts
|
|
250
|
-
function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<
|
|
276
|
+
function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<ClaudeCodeHookOutcome>;
|
|
277
|
+
|
|
278
|
+
type ClaudeCodeHookOutcome = { exitCode: 0 | 2 };
|
|
251
279
|
|
|
252
280
|
type ClaudeCodeHookSpec = {
|
|
253
281
|
repoRoot: string; // config discovery and discipline glob scoping anchor here
|
|
@@ -273,8 +301,8 @@ needs no second dependency.
|
|
|
273
301
|
|
|
274
302
|
| Specifier | Carries |
|
|
275
303
|
|---|---|
|
|
276
|
-
| `polydeukes` | The barrel — `loadConfig`, `runCovenantCheck`,
|
|
277
|
-
| `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 |
|
|
278
306
|
| `polydeukes/schema.json` | The config JSON Schema, copied from the core at build time |
|
|
279
307
|
|
|
280
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,18 +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
183
|
row is the point: a gate that did nothing says so in the data. `pdks explain` shows the
|
|
157
|
-
same fact before any commit:
|
|
158
|
-
`
|
|
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
|
|
159
186
|
really a session-surface tool — on a project that wires only the commit surface, such an
|
|
160
187
|
entry only ever buys telemetry, so declare it where an AI partner's session exists to be
|
|
161
188
|
judged. The row appears only when the entry's scope actually matched, so an unrelated
|
package/dist/explain.d.ts
CHANGED
|
@@ -16,11 +16,9 @@ export type ExplainSpec = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
18
18
|
*
|
|
19
|
-
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
23
|
-
* not the session's.
|
|
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.
|
|
24
22
|
*/
|
|
25
23
|
export declare function explain(spec: ExplainSpec): Promise<{
|
|
26
24
|
text: string;
|
package/dist/explain.js
CHANGED
|
@@ -11,39 +11,45 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { resolveGitAdapterSettings } from '@polydeukes/adapter-git';
|
|
14
|
-
import { noopTranscript } from '@polydeukes/core';
|
|
14
|
+
import { AXIS_NAMES, deriveShape, noopTranscript, RELATION_NAMES } from '@polydeukes/core';
|
|
15
15
|
import { assembleSessionRegistrations } from './claude-code-hook.js';
|
|
16
16
|
import { assembleCommitRegistrations } from './covenant-check.js';
|
|
17
17
|
import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
|
|
18
18
|
import { loadConfig } from './load-config.js';
|
|
19
19
|
/** The three meta-covenant labels: registrations that protect the judging chain itself. */
|
|
20
20
|
const META_LABELS = new Set(['self-mod', 'shell-mod', 'transcript-mod']);
|
|
21
|
-
/** Normalize an optional glob field to an array (absent = empty). */
|
|
22
|
-
function toGlobs(value) {
|
|
23
|
-
if (value === undefined)
|
|
24
|
-
return [];
|
|
25
|
-
return typeof value === 'string' ? [value] : value;
|
|
26
|
-
}
|
|
27
21
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
22
|
+
* The description of a declaration entry: its catalogue coordinate (the mechanism, the axes
|
|
23
|
+
* its sources derive, and the relations its entries decide), then what it routes on, how
|
|
24
|
+
* large its two regex lists are, how many sources it names and how many of those carry
|
|
25
|
+
* each non-file kind, whether it carries a valve, and whether the author left a `why`. An
|
|
26
|
+
* absent scope block admits every world.
|
|
27
|
+
*
|
|
28
|
+
* The axes are derived, never read off the declaration: `loadConfig` has already run the
|
|
29
|
+
* declaration through the validator, so the shape here is the one the catalogue admitted.
|
|
31
30
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
function declareDescription(entry, enforce) {
|
|
32
|
+
const declare = entry.declare;
|
|
33
|
+
const shape = deriveShape(declare);
|
|
34
|
+
const axes = AXIS_NAMES.filter((axis) => shape.axes.has(axis)).join(',');
|
|
35
|
+
const relations = RELATION_NAMES.filter((relation) => shape.relations.has(relation)).join(',');
|
|
36
|
+
const relate = declare.relate.map((relateEntry) => relateEntry.id).join(', ');
|
|
37
|
+
const scope = declare.scope === undefined ? 'scope every world' : `scope ${declare.scope.source}`;
|
|
38
|
+
const include = declare.scope?.include?.length ?? 0;
|
|
39
|
+
const exclude = declare.scope?.exclude?.length ?? 0;
|
|
40
|
+
const bindings = Object.values(declare.sources ?? {});
|
|
41
|
+
// A file binding is the unmarked kind, so only the two the surface has to supply are
|
|
42
|
+
// counted out; a kind nothing binds is left off rather than printed as a zero.
|
|
43
|
+
const kinds = ['sidecar', 'transcript']
|
|
44
|
+
.map((kind) => ({ kind, count: bindings.filter((binding) => kind in binding).length }))
|
|
45
|
+
.filter(({ count }) => count > 0)
|
|
46
|
+
.map(({ kind, count }) => `${kind} ${count}`);
|
|
47
|
+
const counted = kinds.length === 0 ? '' : ` (${kinds.join(', ')})`;
|
|
48
|
+
const sources = `sources ${bindings.length}${counted}`;
|
|
49
|
+
const valve = declare.witness === undefined ? '—' : '✓';
|
|
50
|
+
const why = entry.why === undefined ? '—' : '✓';
|
|
51
|
+
return (`${declare.mechanism} · ${axes} · ${relations} ${relate} · ${scope} · ` +
|
|
52
|
+
`include ${include} · exclude ${exclude} · ${sources} · valve ${valve} · why ${why}${enforce}`);
|
|
47
53
|
}
|
|
48
54
|
/** One rendered line: the kind column, the label column, then the description. */
|
|
49
55
|
function row(kind, label, width, description) {
|
|
@@ -59,8 +65,8 @@ function metaDescription(registration, surface) {
|
|
|
59
65
|
/** Render one surface: its header, its tallies, and one line per registration. */
|
|
60
66
|
function renderSurface(spec) {
|
|
61
67
|
const lines = [];
|
|
62
|
-
const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.
|
|
63
|
-
let
|
|
68
|
+
const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.drafts.map((draft) => draft.id.length));
|
|
69
|
+
let declare = 0;
|
|
64
70
|
let skip = 0;
|
|
65
71
|
let meta = 0;
|
|
66
72
|
for (const registration of spec.registrations) {
|
|
@@ -75,38 +81,35 @@ function renderSurface(spec) {
|
|
|
75
81
|
lines.push(row('skip', registration.label, width, registration.skip.reason));
|
|
76
82
|
continue;
|
|
77
83
|
}
|
|
78
|
-
|
|
84
|
+
// Every non-meta body registration is one config entry's declaration; a label the config
|
|
85
|
+
// does not carry is an assembly the renderer was never told about.
|
|
79
86
|
const entry = spec.disciplines.find((candidate) => candidate.id === registration.label);
|
|
87
|
+
if (entry === undefined) {
|
|
88
|
+
throw new Error(`explain: registration '${registration.label}' matches no config entry`);
|
|
89
|
+
}
|
|
80
90
|
// The DECLARED level is rendered, never the effective one: an omission stays unmarked
|
|
81
91
|
// so the default and an author's explicit choice of it never read alike, and the
|
|
82
92
|
// surface header states what the omission resolves to.
|
|
83
|
-
const level = entry
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: `${scopeOf(entry)} · why ${entry.why === undefined ? '—' : '✓'}${level}`;
|
|
87
|
-
lines.push(row('judge', registration.label, width, description));
|
|
88
|
-
}
|
|
89
|
-
for (const entry of spec.excluded) {
|
|
90
|
-
lines.push(row('excluded', entry.id, width, 'forbidCommand — no shell axis on this surface'));
|
|
93
|
+
const level = entry.enforce === undefined ? '' : ` · enforce: ${entry.enforce}`;
|
|
94
|
+
declare += 1;
|
|
95
|
+
lines.push(row('declare', registration.label, width, declareDescription(entry, level)));
|
|
91
96
|
}
|
|
92
97
|
for (const draft of spec.drafts) {
|
|
93
98
|
lines.push(row('draft', draft.id, width, 'unpromoted — no judgment'));
|
|
94
99
|
}
|
|
95
|
-
const tally = ` registrations ${meta +
|
|
96
|
-
`
|
|
100
|
+
const tally = ` registrations ${meta + declare + skip} · ` +
|
|
101
|
+
`declare ${declare} · skip ${skip} · meta ${meta} · draft ${spec.drafts.length}`;
|
|
97
102
|
return [spec.header, tally, ...lines].join('\n');
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
101
106
|
*
|
|
102
|
-
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
106
|
-
* not the session's.
|
|
107
|
+
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
108
|
+
* exists here exactly as it does under a normal hook payload — the path is never read,
|
|
109
|
+
* because the injected transcript is the no-op one.
|
|
107
110
|
*/
|
|
108
111
|
export async function explain(spec) {
|
|
109
|
-
const { config, configPath } = loadConfig(spec.repoRoot);
|
|
112
|
+
const { config, configPath } = loadConfig({ rootDir: spec.repoRoot });
|
|
110
113
|
// Resolved and imported exactly as the two runners do, so what this renders is the table
|
|
111
114
|
// that would judge: a dist those runners would refuse cannot be rendered as if it worked.
|
|
112
115
|
// The load names the missing module and the recovery command.
|
|
@@ -125,14 +128,13 @@ export async function explain(spec) {
|
|
|
125
128
|
rootDir: spec.repoRoot,
|
|
126
129
|
covenant,
|
|
127
130
|
});
|
|
128
|
-
const gitSettings = resolveGitAdapterSettings(config.adapters?.git);
|
|
131
|
+
const gitSettings = resolveGitAdapterSettings({ namespace: config.adapters?.git });
|
|
129
132
|
const text = [
|
|
130
133
|
`pdks explain — ${configPath}`,
|
|
131
134
|
'',
|
|
132
135
|
renderSurface({
|
|
133
136
|
header: 'surface: session (claude-code hook) · disciplines: advise unless enforce: block · meta: block',
|
|
134
137
|
registrations: session,
|
|
135
|
-
excluded: [],
|
|
136
138
|
drafts,
|
|
137
139
|
disciplines,
|
|
138
140
|
selfModScope: 'common; includes the config file itself',
|
|
@@ -141,7 +143,6 @@ export async function explain(spec) {
|
|
|
141
143
|
renderSurface({
|
|
142
144
|
header: `surface: commit (git pre-commit) · enforce: ${gitSettings.enforce} · disciplines: advise unless enforce: block`,
|
|
143
145
|
registrations: commit,
|
|
144
|
-
excluded: disciplines.filter((entry) => entry.forbidCommand !== undefined),
|
|
145
146
|
drafts,
|
|
146
147
|
disciplines,
|
|
147
148
|
selfModScope: 'common ∪ adapters.git; deduped, includes the config file itself',
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,5 @@
|
|
|
18
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
19
19
|
*/
|
|
20
20
|
export type { ResolvedConfig } from '@polydeukes/core';
|
|
21
|
-
export { type
|
|
22
|
-
export { type
|
|
23
|
-
export { type LoadedConfig, loadConfig } from './load-config.js';
|
|
21
|
+
export { type CheckDomain, type CovenantCheckOutcome, type CovenantCheckSpec, runCovenantCheck, } from './covenant-check.ts';
|
|
22
|
+
export { type LoadConfigSpec, type LoadedConfig, loadConfig } from './load-config.ts';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
*
|
|
18
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
19
19
|
*/
|
|
20
|
-
export {
|
|
21
|
-
export { runCovenantCheck } from './covenant-check.js';
|
|
20
|
+
export { runCovenantCheck, } from './covenant-check.js';
|
|
22
21
|
export { loadConfig } from './load-config.js';
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Nothing existing is overwritten. The settings file in particular is merged, never
|
|
16
16
|
* replaced: a consumer's other PreToolUse registrations and permissions are live
|
|
17
|
-
* configuration, and replacing them would disarm every other tool they wired.
|
|
17
|
+
* configuration, and replacing them would disarm every other tool they wired. A grok JSON
|
|
18
|
+
* whose command still names the grok mjs is rewritten to this hook's command so the two
|
|
19
|
+
* installers do not leave two spawn strings.
|
|
18
20
|
*/
|
|
19
|
-
import { type ScaffoldReport } from './scaffold-project.
|
|
21
|
+
import { type ScaffoldReport } from './scaffold-project.ts';
|
|
20
22
|
/**
|
|
21
23
|
* The generated classification skill — the procedure that turns a described problem into a
|
|
22
24
|
* registered entry. A classification procedure an agent never learns about is one that never
|
|
@@ -26,7 +28,7 @@ import { type ScaffoldReport } from './scaffold-project.js';
|
|
|
26
28
|
* lets an advised call through with exit 0, and the reason never reaches the model at call
|
|
27
29
|
* time — reading the telemetry log at task boundaries is the only way it arrives.
|
|
28
30
|
*/
|
|
29
|
-
export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the current families can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nAsk these questions in order; the first yes decides.\n\n| # | Question | Family | Entry key |\n| --- | --- | --- | --- |\n| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | delta | `forbid` |\n| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | path | `immutable` |\n| 3 | Is it about the shell command line itself, regardless of files? | command | `forbidCommand` |\n| 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | context | `requirePrecedent` |\n| 5 | None of the above | \u2014 | `draft: true` (step 4b) |\n\nExisting occurrences are forgiven by the delta family \u2014 only new additions break the promise.\nThat is usually what you want: a discipline adopted today should not indict yesterday's code.\n\nTwo path-shaped promises take no `disciplines:` entry at all. A path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry\nkey. And a path that must never be CREATED is not expressible today: `immutable` allows\ncreation by design, so register that promise as a draft (step 4b).\n\n### 3. Check the observation boundary\n\nTwo kinds of promise cannot be judged here, whatever their shape:\n\n- **Destruction outside the repository** \u2014 judgment observes the project root only. Register\n nothing; use the agent's own permission deny policy for commands like `rm -rf ~`.\n- **Writes by child processes** \u2014 a test runner or script writing files is invisible to the\n session surface, which judges declared tool calls only. Say so to the user; the commit\n surface will still see the result as a staged diff.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n forbid: '\\.only\\('\n enforce: advise\n```\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow `in:` to the files where any\n match IS a break (`in:`/`except:` scope `forbid` and `requirePrecedent` only), or\n accept \"editing this file at all\" as the trigger.\n- **`^` silently disarms on the delta axis.** `forbid` scans whole file content as one\n string, so a line-start anchor matches the first line only \u2014 write `(^|\\n)` there.\n `forbidCommand` judges per line and the whole string, so `^` is safe on that axis.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`forbid` vs `forbidden`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the SHAPE of the promise inside `why`, so the promotion destination is already\nwritten down when a later engine can express it. Name the shape in these terms:\n\n| Shape | The promise reads like |\n| --- | --- |\n| pairing | every element of set A has a counterpart in set B (translation keys, i18n) |\n| companion | if X appears in a unit, Y must appear with it |\n| ordered | a sequence must keep its order (migration journals, version ladders) |\n| fingerprint | a derived artifact must match the hash/stamp of its source |\n| producer-owned | only a designated generator may write this artifact |\n| self-absolution | the party being judged must not write its own verdict field |\n| actor-scope | the same action is fine for one actor and a break for another |\n| phase-order | several precedents, in a fixed order |\n| turn-locality | the evidence must be in the same turn or time window |\n| stated-ground | the reason must be written down before the action |\n| controlled-vocabulary | only an enumerated set of words/values is allowed |\n| naming-convention | names must match a pattern per kind |\n| irreversible-marker | once present, a marker may never be removed |\n| delegation-scope | a delegated task may touch only its granted scope |\n| scope-valve | a defined exception valve, judged rather than ad hoc |\n| claim-verification | the claim must be re-run/measured, not trusted |\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'locale-files-move-together'\n why: 'pairing \u2014 en.json and ko.json must change in the same commit; one side alone is a break'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its family and\nsurfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run its family can\nactually reach:\n\n| Family | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| `forbid` / `immutable` | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidCommand` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `requirePrecedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 this family judges on the session surface only (the commit surface records it `skipped`) |\n\nThen undo the scratch break, repeat the same run, and confirm silence on the\nmust-NOT-match direction. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
|
|
31
|
+
export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the current families can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nAsk these questions in order; the first yes decides.\n\n| # | Question | Entry key |\n| --- | --- | --- |\n| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | `declare` (mechanism `added-only`) |\n| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | `declare` (mechanism `self-absolution-ban`) |\n| 3 | Is it about the shell command line itself, regardless of files? | `declare` (mechanism `forbidden-command`, reading the `command` source) |\n| 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | `declare` (mechanism `precedent`, reading a `transcript` source) |\n| 5 | None of the above | `draft: true` (step 4b) |\n\nAn `added-only` declaration forgives existing occurrences \u2014 only what the edit adds breaks\nthe promise. That is usually what you want: a discipline adopted today should not indict\nyesterday's code.\n\nOne path-shaped promise takes no `disciplines:` entry at all: a path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry key.\n\n### 3. Check the observation boundary\n\nTwo kinds of promise cannot be judged here, whatever their shape:\n\n- **Destruction outside the repository** \u2014 judgment observes the project root only. Register\n nothing; use the agent's own permission deny policy for commands like `rm -rf ~`.\n- **Writes by child processes** \u2014 a test runner or script writing files is invisible to the\n session surface, which judges declared tool calls only. Say so to the user; the commit\n surface will still see the result as a staged diff.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n declare:\n mechanism: 'added-only'\n scope: { source: 'target.path', include: ['^src/'] }\n supply: { pre: 'empty', post: 'empty' }\n extract:\n before:\n - { op: 'source', of: 'pre' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n after:\n - { op: 'source', of: 'post' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n added:\n - { op: 'onlyIn', of: 'after', notIn: 'before' }\n relate:\n - id: 'nothing-added'\n relation: { op: 'empty', of: 'added' }\n message: 'adds {key}: {value}'\n enforce: advise\n```\n\nA command-line ban reads the fixed source `command` and scopes on it \u2014 the scope is part of\nthe mechanism's shape, so a `forbidden-command` entry without it is refused at load time:\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-force-push'\n why: 'a force push rewrites history nobody reviewed'\n declare:\n mechanism: 'forbidden-command'\n scope: { source: 'command' }\n extract:\n hits:\n - { op: 'source', of: 'command' }\n - { op: 'lines' }\n - { op: 'matches', re: 'git push\\\\b.*--force(?![\\\\w-])' }\n relate:\n - { id: 'no-force', relation: { op: 'empty', of: 'hits' }, message: '{value}' }\n enforce: advise\n```\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow the declaration's own `scope`\n block to the files where any match IS a break, or accept \"editing this file at all\" as\n the trigger.\n- **`^` means what the preceding step left.** After a `lines` step a declaration's\n pattern sees one line at a time, so `^` anchors to that line; over an unsplit source it\n anchors to the whole text and matches the first line only. A ban over the command line\n puts `lines` before its `matches` for exactly that reason.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`only(` vs `only_helper(`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the SHAPE of the promise inside `why`, so the promotion destination is already\nwritten down when a later engine can express it. Name the shape in these terms:\n\n| Shape | The promise reads like |\n| --- | --- |\n| pairing | every element of set A has a counterpart in set B (translation keys, i18n) |\n| companion | if X appears in a unit, Y must appear with it |\n| ordered | a sequence must keep its order (migration journals, version ladders) |\n| fingerprint | a derived artifact must match the hash/stamp of its source |\n| producer-owned | only a designated generator may write this artifact |\n| self-absolution | the party being judged must not write its own verdict field |\n| actor-scope | the same action is fine for one actor and a break for another |\n| phase-order | several precedents, in a fixed order |\n| turn-locality | the evidence must be in the same turn or time window |\n| stated-ground | the reason must be written down before the action |\n| controlled-vocabulary | only an enumerated set of words/values is allowed |\n| naming-convention | names must match a pattern per kind |\n| irreversible-marker | once present, a marker may never be removed |\n| delegation-scope | a delegated task may touch only its granted scope |\n| scope-valve | a defined exception valve, judged rather than ad hoc |\n| claim-verification | the claim must be re-run/measured, not trusted |\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'locale-files-move-together'\n why: 'pairing \u2014 en.json and ko.json must change in the same commit; one side alone is a break'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its mechanism\nand surfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run the declaration's\nown mechanism can actually reach:\n\n| Mechanism | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| a file-reading one (`added-only`, `naming`, \u2026) | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidden-command` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `precedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 a declaration reading the session judges on the session surface only (the commit surface has none, so its `supply` policy records it `skipped`) |\n\nThen undo the scratch break, repeat the same run, and confirm silence on the\nmust-NOT-match direction. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
|
|
30
32
|
/** `initClaudeCode` input — the target tree and the preflight seam. */
|
|
31
33
|
export type InitClaudeCodeSpec = {
|
|
32
34
|
/** Project root to install into — every write below is relative to it. */
|