polydeukes 0.4.0 → 0.5.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 +3 -2
- package/README.md +10 -2
- package/dist/bin.d.ts +5 -6
- package/dist/bin.js +75 -38
- package/dist/claude-code-hook.d.ts +35 -14
- package/dist/claude-code-hook.js +128 -168
- package/dist/covenant-check.d.ts +54 -33
- package/dist/covenant-check.js +135 -193
- package/dist/covenant-module.d.ts +25 -0
- package/dist/covenant-module.js +34 -0
- package/dist/docs/configuration.md +16 -8
- package/dist/docs/installation.md +3 -2
- package/dist/docs/reference/adapter-git.md +16 -5
- package/dist/docs/reference/configuration.md +48 -4
- package/dist/docs/reference/core.md +4 -2
- package/dist/docs/reference/covenant.md +1 -1
- package/dist/docs/reference/polydeukes.md +95 -23
- package/dist/docs/troubleshooting.md +3 -1
- package/dist/docs-query.d.ts +10 -10
- package/dist/docs-query.js +12 -12
- package/dist/explain.d.ts +27 -0
- package/dist/explain.js +152 -0
- package/dist/index.d.ts +10 -14
- package/dist/index.js +9 -13
- package/dist/init-claude-code.d.ts +28 -17
- package/dist/init-claude-code.js +208 -40
- package/dist/load-config.d.ts +12 -14
- package/dist/load-config.js +11 -11
- package/dist/scaffold-project.d.ts +23 -14
- package/dist/scaffold-project.js +51 -26
- package/dist/schema/polydeukes.schema.json +30 -4
- package/package.json +5 -5
|
@@ -21,23 +21,39 @@ you do not install and do not import.
|
|
|
21
21
|
|
|
22
22
|
## Subcommands
|
|
23
23
|
|
|
24
|
-
The bin is `pdks`, with `polydeukes` as an alias.
|
|
25
|
-
|
|
24
|
+
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` is exact,
|
|
26
|
+
`explain` takes one word, and `docs` takes an optional topic.
|
|
26
27
|
|
|
27
28
|
### `pdks covenant check`
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
`pdks covenant check [--worktree | --range <base>..<head>]` — the commit-surface judgment
|
|
31
|
+
runner. It discovers the config at the working directory,
|
|
32
|
+
collects one observation of the repository through the git adapter, translates it into the
|
|
33
|
+
covenant input IR, and dispatches it through the same in-process judges the session hook calls.
|
|
34
|
+
Which observation is the domain flag's choice — the same violation receives the same verdict
|
|
35
|
+
in all three:
|
|
36
|
+
|
|
37
|
+
| Form | Domain | `pre` → `post` |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `pdks covenant check` | The staging area — what a pre-commit hook judges | HEAD blob → staged blob |
|
|
40
|
+
| `pdks covenant check --worktree` | The working tree, untracked (non-ignored) files included | HEAD blob → bytes on disk |
|
|
41
|
+
| `pdks covenant check --range <base>..<head>` | Two refs; `<base>...<head>` reads from their merge-base, the PR reading | base blob → head blob |
|
|
42
|
+
|
|
43
|
+
`--worktree` and `--range` are diagnostic calls — run them after a task, before a PR, or in
|
|
44
|
+
CI. Only the staged form is a gate, so only it can prompt for the witness token; the other
|
|
45
|
+
two report exit `2` without a prompt, since there is no commit for a human to open.
|
|
33
46
|
|
|
34
47
|
| Situation | Result |
|
|
35
48
|
|---|---|
|
|
36
|
-
|
|
|
49
|
+
| The domain's changes break nothing | exit `0` |
|
|
37
50
|
| 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
51
|
| 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
|
-
|
|
|
52
|
+
| A worktree or range change breaks a covenant, `enforce: block` | exit `2`, never a prompt |
|
|
53
|
+
| A change breaks a covenant, `enforce: advise` | One advisory line on stderr, exit `0`, recorded `advised` |
|
|
54
|
+
| Empty domain (nothing staged, a clean tree, identical refs) | exit `0` — an explicit pass, not a skipped run |
|
|
55
|
+
| A range naming a ref git cannot resolve, or two refs with no merge-base | exit `2`, one `blocked` row |
|
|
56
|
+
| `--range` without an argument, an argument without `..`, both flags at once, or an unknown flag | The usage line on stderr, exit `2` |
|
|
41
57
|
| No config, more than one config, or an invalid one | exit `2` |
|
|
42
58
|
| A judge body that cannot be loaded | exit `2` |
|
|
43
59
|
|
|
@@ -48,7 +64,7 @@ permanent condition of the commit surface, not a fault.
|
|
|
48
64
|
### `pdks init claude-code`
|
|
49
65
|
|
|
50
66
|
The session-surface installer. It proves that `polydeukes` resolves from the directory it
|
|
51
|
-
was invoked in **before writing anything**, then creates
|
|
67
|
+
was invoked in **before writing anything**, then creates six artifacts:
|
|
52
68
|
|
|
53
69
|
| Artifact | Kind |
|
|
54
70
|
|---|---|
|
|
@@ -56,6 +72,7 @@ was invoked in **before writing anything**, then creates five artifacts:
|
|
|
56
72
|
| `.claude/settings.json` | Merged — the PreToolUse registration is added to whatever the file already carries |
|
|
57
73
|
| `polydeukes.config.yaml` | Created — the starter policy, with a placeholder `languages` block |
|
|
58
74
|
| `.claude/rules/polydeukes.md` | Created — tells the AI partner to ask [`pdks docs`](#pdks-docs-topic) instead of searching the web |
|
|
75
|
+
| `.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
76
|
| `.gitignore` | Appended — one line for `.polydeukes/` |
|
|
60
77
|
|
|
61
78
|
Nothing existing is overwritten: an artifact already present is reported as skipped and left
|
|
@@ -96,11 +113,59 @@ the package.
|
|
|
96
113
|
`pdks init claude-code` writes a discovery file that points an AI partner at this
|
|
97
114
|
subcommand; see the artifact table above.
|
|
98
115
|
|
|
116
|
+
### `pdks explain`
|
|
117
|
+
|
|
118
|
+
The assembly reader. It loads the config at the working directory, assembles both surfaces'
|
|
119
|
+
registration sets through the same functions the two judgment runners use, and prints them
|
|
120
|
+
without judging — no judge thunk is called, no telemetry row is written, no transcript is
|
|
121
|
+
read.
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
pdks explain — polydeukes.config.yaml
|
|
125
|
+
|
|
126
|
+
surface: session (claude-code hook)
|
|
127
|
+
registrations 23 · judged 11 · skip 9 · meta 3 · excluded 0 · draft 1
|
|
128
|
+
meta self-mod paths 13 (common; includes the config file itself)
|
|
129
|
+
judge covenant-vocabulary forbid · in packages/*/src/** · except … · why ✓
|
|
130
|
+
skip covenant-vocabulary a shell write in scope whose result this layer cannot compute
|
|
131
|
+
draft bilingual-docs-sync unpromoted — no judgment
|
|
132
|
+
...
|
|
133
|
+
surface: commit (git pre-commit) · enforce: advise
|
|
134
|
+
registrations 10 · judged 3 · skip 6 · meta 1 · excluded 3 · draft 1
|
|
135
|
+
skip manifest-needs-npm-view no session transcript to read
|
|
136
|
+
excluded hooks-stay-armed forbidCommand — no shell axis on this surface
|
|
137
|
+
draft bilingual-docs-sync unpromoted — no judgment
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
One line per registration, in the order the surface dispatches them. The kind column has
|
|
141
|
+
five words: `meta` (the registrations protecting the judging chain — `self-mod`,
|
|
142
|
+
`shell-mod`, and on the session surface `transcript-mod`), `judge` (an entry with a judge
|
|
143
|
+
body, with its family, routing scope, and whether it carries a `why`), `skip` (a registration
|
|
144
|
+
that records `skipped` instead of judging, with the reason the compiler gave — the reason
|
|
145
|
+
that otherwise reaches stderr only on a config fault), `excluded` (a `forbidCommand`
|
|
146
|
+
entry on the commit surface, which has no shell axis), and `draft` (an unpromoted
|
|
147
|
+
`draft: true` entry, shown on both surfaces since it belongs to neither). `registrations`
|
|
148
|
+
counts the first three; `excluded` and `draft` are tallied apart because neither ever
|
|
149
|
+
becomes a registration.
|
|
150
|
+
The commit surface's header also names its `adapters.git.enforce` level, since an advising
|
|
151
|
+
surface records the same table but blocks nothing.
|
|
152
|
+
|
|
153
|
+
The session surface is rendered as the hook sees it under a normal payload — with a
|
|
154
|
+
transcript present — so `transcript-mod` and the context family appear as they do in a
|
|
155
|
+
session; the commit surface shows the context family as skips, which is that surface's
|
|
156
|
+
permanent condition.
|
|
157
|
+
|
|
158
|
+
| Call | Result |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `pdks explain` | Both surfaces on stdout, exit `0` |
|
|
161
|
+
| `pdks explain <anything>` | The usage line on stderr, exit `2` |
|
|
162
|
+
| no config, two configs, or an invalid one | `pdks explain: <reason>` on stderr, stdout at zero bytes, exit `2` |
|
|
163
|
+
|
|
99
164
|
### Any other argument form
|
|
100
165
|
|
|
101
|
-
Anything that is not one of these forms writes
|
|
102
|
-
`usage: pdks covenant check |
|
|
103
|
-
exits `2`.
|
|
166
|
+
Anything that is not one of these forms writes the usage line —
|
|
167
|
+
`usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain |
|
|
168
|
+
pdks init claude-code | pdks docs [topic]` — to stderr and exits `2`.
|
|
104
169
|
|
|
105
170
|
## Exit codes
|
|
106
171
|
|
|
@@ -109,16 +174,16 @@ Three codes exist, and they live at two layers. **What a consumer's hook observe
|
|
|
109
174
|
|
|
110
175
|
| Code | Constant | Emitted by | Means |
|
|
111
176
|
|---|---|---|---|
|
|
112
|
-
| `0` | `EXIT_UPHOLD` | Judge
|
|
113
|
-
| `1` | `EXIT_BREAK_NON_BLOCKING` | Judge
|
|
177
|
+
| `0` | `EXIT_UPHOLD` | Judge outcome, wrapper, bin | The promise was upheld — the call or commit proceeds |
|
|
178
|
+
| `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
179
|
| `2` | `EXIT_BREAK_BLOCKING` | Wrapper, bin, fail-closed paths | The call or commit is refused |
|
|
115
180
|
|
|
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.
|
|
181
|
+
The asymmetry is the protocol's responsibility boundary. A judge decides *whether* a
|
|
182
|
+
promise was broken and answers `0` or `1` in its outcome; deciding what a break *costs* belongs
|
|
183
|
+
to the wrapper, and that is the one place `enforce` is read. A judge can therefore be run,
|
|
184
|
+
tested, and reasoned about without knowing whether the surface it runs under blocks or advises.
|
|
185
|
+
Only the verdict relaxes: every unjudgeable outcome — an outcome of `2` or higher, a throw
|
|
186
|
+
from the judge — stays `2` at either level.
|
|
122
187
|
|
|
123
188
|
**Everything unjudgeable resolves to `2`.** A missing config, an invalid one, an
|
|
124
189
|
unparseable payload, a judge body that was never built — each fails closed. The one
|
|
@@ -156,15 +221,22 @@ a silently unprotected project.
|
|
|
156
221
|
function runCovenantCheck(spec: CovenantCheckSpec): Promise<{ exitCode: 0 | 2 }>;
|
|
157
222
|
|
|
158
223
|
type CovenantCheckSpec = {
|
|
159
|
-
repoRoot: string; // config discovery and
|
|
224
|
+
repoRoot: string; // config discovery and collection anchor here
|
|
160
225
|
telemetryPath?: string; // overrides the config's log path
|
|
161
226
|
covenantDist?: string; // overrides the resolved judge directory
|
|
162
227
|
ttyPrompt?: (prompt: string) => string | null; // the TTY valve seam
|
|
228
|
+
domain?: CheckDomain; // which observation to judge; absent = staged
|
|
163
229
|
};
|
|
230
|
+
|
|
231
|
+
type CheckDomain =
|
|
232
|
+
| { kind: 'staged' }
|
|
233
|
+
| { kind: 'worktree' }
|
|
234
|
+
| { kind: 'range'; base: string; head: string; ancestry?: 'merge-base' };
|
|
164
235
|
```
|
|
165
236
|
|
|
166
237
|
The commit surface's composition root — what [`pdks covenant
|
|
167
|
-
check`](#pdks-covenant-check) runs.
|
|
238
|
+
check`](#pdks-covenant-check) runs. `ancestry: 'merge-base'` is the `<base>...<head>`
|
|
239
|
+
reading; the adapter resolves the merge-base.
|
|
168
240
|
|
|
169
241
|
`ttyPrompt` absent means a non-TTY environment, and the valve then has no way to open — an
|
|
170
242
|
agent-spawned commit and a CI run reach the same state. The valve is a human at a terminal
|
|
@@ -153,7 +153,9 @@ assembles as a skip registration: routing intact, no judge body. When its scope
|
|
|
153
153
|
staged change it records `skipped` with the entry's id and proceeds.
|
|
154
154
|
|
|
155
155
|
**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.
|
|
156
|
+
row is the point: a gate that did nothing says so in the data. `pdks explain` shows the
|
|
157
|
+
same fact before any commit: every context entry appears under the commit surface as a
|
|
158
|
+
`skip` line carrying the reason. A context-family entry is
|
|
157
159
|
really a session-surface tool — on a project that wires only the commit surface, such an
|
|
158
160
|
entry only ever buys telemetry, so declare it where an AI partner's session exists to be
|
|
159
161
|
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,27 @@
|
|
|
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 and
|
|
20
|
+
* the context family exist here exactly as they do under a normal hook payload — the path is
|
|
21
|
+
* never read, because the injected transcript is the no-op one. Without a transcript the
|
|
22
|
+
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
23
|
+
* not the session's.
|
|
24
|
+
*/
|
|
25
|
+
export declare function explain(spec: ExplainSpec): Promise<{
|
|
26
|
+
text: string;
|
|
27
|
+
}>;
|
package/dist/explain.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
import { join } from 'node:path';
|
|
13
|
+
import { resolveGitAdapterSettings } from '@polydeukes/adapter-git';
|
|
14
|
+
import { noopTranscript } from '@polydeukes/core';
|
|
15
|
+
import { assembleSessionRegistrations } from './claude-code-hook.js';
|
|
16
|
+
import { assembleCommitRegistrations } from './covenant-check.js';
|
|
17
|
+
import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
|
|
18
|
+
import { loadConfig } from './load-config.js';
|
|
19
|
+
/** The three meta-covenant labels: registrations that protect the judging chain itself. */
|
|
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
|
+
/**
|
|
28
|
+
* The routing scope of a config entry, in its own family's shape: the delta and context
|
|
29
|
+
* families scope by `in`/`except` globs, the path family by its own glob, and the command
|
|
30
|
+
* family by nothing at all — it judges the command line, which no path scopes.
|
|
31
|
+
*/
|
|
32
|
+
function scopeOf(entry) {
|
|
33
|
+
if (entry.immutable !== undefined) {
|
|
34
|
+
return `immutable ${toGlobs(entry.immutable).join(', ')}`;
|
|
35
|
+
}
|
|
36
|
+
if (entry.forbidCommand !== undefined) {
|
|
37
|
+
return 'forbidCommand · (no path scope)';
|
|
38
|
+
}
|
|
39
|
+
const family = entry.requirePrecedent === undefined
|
|
40
|
+
? 'forbid'
|
|
41
|
+
: `requirePrecedent ${Object.keys(entry.requirePrecedent).join(', ')}`;
|
|
42
|
+
const inGlobs = toGlobs(entry.in);
|
|
43
|
+
const scope = inGlobs.length === 0 ? 'every file' : `in ${inGlobs.join(', ')}`;
|
|
44
|
+
const exceptGlobs = toGlobs(entry.except);
|
|
45
|
+
const except = exceptGlobs.length === 0 ? '' : ` · except ${exceptGlobs.join(', ')}`;
|
|
46
|
+
return `${family} · ${scope}${except}`;
|
|
47
|
+
}
|
|
48
|
+
/** One rendered line: the kind column, the label column, then the description. */
|
|
49
|
+
function row(kind, label, width, description) {
|
|
50
|
+
return ` ${kind.padEnd(8)} ${label.padEnd(width)} ${description}`;
|
|
51
|
+
}
|
|
52
|
+
/** The description of a meta-covenant registration — how much surface it covers. */
|
|
53
|
+
function metaDescription(registration, surface) {
|
|
54
|
+
if (registration.label === 'transcript-mod') {
|
|
55
|
+
return 'content predicate · conditional: transcript_path';
|
|
56
|
+
}
|
|
57
|
+
return `paths ${registration.protectedPaths.length} (${surface})`;
|
|
58
|
+
}
|
|
59
|
+
/** Render one surface: its header, its tallies, and one line per registration. */
|
|
60
|
+
function renderSurface(spec) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.excluded.map((entry) => entry.id.length), ...spec.drafts.map((draft) => draft.id.length));
|
|
63
|
+
let judged = 0;
|
|
64
|
+
let skip = 0;
|
|
65
|
+
let meta = 0;
|
|
66
|
+
for (const registration of spec.registrations) {
|
|
67
|
+
if (META_LABELS.has(registration.label)) {
|
|
68
|
+
meta += 1;
|
|
69
|
+
const scope = registration.label === 'self-mod' ? spec.selfModScope : 'common';
|
|
70
|
+
lines.push(row('meta', registration.label, width, metaDescription(registration, scope)));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (registration.skip !== undefined) {
|
|
74
|
+
skip += 1;
|
|
75
|
+
lines.push(row('skip', registration.label, width, registration.skip.reason));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
judged += 1;
|
|
79
|
+
const entry = spec.disciplines.find((candidate) => candidate.id === registration.label);
|
|
80
|
+
// The DECLARED level is rendered, never the effective one: an omission stays unmarked
|
|
81
|
+
// so the default and an author's explicit choice of it never read alike, and the
|
|
82
|
+
// surface header states what the omission resolves to.
|
|
83
|
+
const level = entry?.enforce === undefined ? '' : ` · enforce: ${entry.enforce}`;
|
|
84
|
+
const description = entry === undefined
|
|
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'));
|
|
91
|
+
}
|
|
92
|
+
for (const draft of spec.drafts) {
|
|
93
|
+
lines.push(row('draft', draft.id, width, 'unpromoted — no judgment'));
|
|
94
|
+
}
|
|
95
|
+
const tally = ` registrations ${meta + judged + skip} · judged ${judged} · skip ${skip} · ` +
|
|
96
|
+
`meta ${meta} · excluded ${spec.excluded.length} · draft ${spec.drafts.length}`;
|
|
97
|
+
return [spec.header, tally, ...lines].join('\n');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
101
|
+
*
|
|
102
|
+
* The session assembly is given a transcript path, so its `transcript-mod` registration and
|
|
103
|
+
* the context family exist here exactly as they do under a normal hook payload — the path is
|
|
104
|
+
* never read, because the injected transcript is the no-op one. Without a transcript the
|
|
105
|
+
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
106
|
+
* not the session's.
|
|
107
|
+
*/
|
|
108
|
+
export async function explain(spec) {
|
|
109
|
+
const { config, configPath } = loadConfig(spec.repoRoot);
|
|
110
|
+
// Resolved and imported exactly as the two runners do, so what this renders is the table
|
|
111
|
+
// that would judge: a dist those runners would refuse cannot be rendered as if it worked.
|
|
112
|
+
// The load names the missing module and the recovery command.
|
|
113
|
+
const covenant = await loadCovenantModule(resolveCovenantDist());
|
|
114
|
+
const disciplines = config.disciplines ?? [];
|
|
115
|
+
const drafts = config.drafts ?? [];
|
|
116
|
+
const session = assembleSessionRegistrations({
|
|
117
|
+
config,
|
|
118
|
+
rootDir: spec.repoRoot,
|
|
119
|
+
covenant,
|
|
120
|
+
transcriptPath: join(spec.repoRoot, 'transcript.jsonl'),
|
|
121
|
+
transcript: noopTranscript,
|
|
122
|
+
});
|
|
123
|
+
const commit = assembleCommitRegistrations({
|
|
124
|
+
config,
|
|
125
|
+
rootDir: spec.repoRoot,
|
|
126
|
+
covenant,
|
|
127
|
+
});
|
|
128
|
+
const gitSettings = resolveGitAdapterSettings(config.adapters?.git);
|
|
129
|
+
const text = [
|
|
130
|
+
`pdks explain — ${configPath}`,
|
|
131
|
+
'',
|
|
132
|
+
renderSurface({
|
|
133
|
+
header: 'surface: session (claude-code hook) · disciplines: advise unless enforce: block · meta: block',
|
|
134
|
+
registrations: session,
|
|
135
|
+
excluded: [],
|
|
136
|
+
drafts,
|
|
137
|
+
disciplines,
|
|
138
|
+
selfModScope: 'common; includes the config file itself',
|
|
139
|
+
}),
|
|
140
|
+
'',
|
|
141
|
+
renderSurface({
|
|
142
|
+
header: `surface: commit (git pre-commit) · enforce: ${gitSettings.enforce} · disciplines: advise unless enforce: block`,
|
|
143
|
+
registrations: commit,
|
|
144
|
+
excluded: disciplines.filter((entry) => entry.forbidCommand !== undefined),
|
|
145
|
+
drafts,
|
|
146
|
+
disciplines,
|
|
147
|
+
selfModScope: 'common ∪ adapters.git; deduped, includes the config file itself',
|
|
148
|
+
}),
|
|
149
|
+
'',
|
|
150
|
+
].join('\n');
|
|
151
|
+
return { text };
|
|
152
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,25 +3,21 @@
|
|
|
3
3
|
* coding partner.
|
|
4
4
|
*
|
|
5
5
|
* Pre-alpha. This package reserves the unscoped `polydeukes` name and is the umbrella /
|
|
6
|
-
* `pdks` CLI entry point. It owns the config discovery loader
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* `pdks` CLI entry point. It owns the config discovery loader and both surfaces'
|
|
7
|
+
* composition roots — `runCovenantCheck` for the commit surface and `runClaudeCodeHook`
|
|
8
|
+
* for the session one — because assembly needs an adapter and the covenant package at
|
|
9
|
+
* once, which no sibling is allowed to depend on. The covenant, ledger, memory, and verify
|
|
10
|
+
* modules live in their own `@polydeukes/*` packages.
|
|
11
11
|
*
|
|
12
12
|
* This file is a barrel and nothing more. ESM re-exports are eager, so anything defined
|
|
13
|
-
* here would be instantiated by every consumer of any other export
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* here would be instantiated by every consumer of any other export. Keep definitions in
|
|
14
|
+
* their own modules and let importers reach them directly. A session call enters through
|
|
15
|
+
* the published `./claude-code` subpath instead, which keeps the commit surface and its git
|
|
16
|
+
* adapter off that load path.
|
|
16
17
|
*
|
|
17
|
-
* The mirror of that coupling is closed as of DIST-02. `exports` publishes `./claude-code`
|
|
18
|
-
* alongside `"."`, and both delegators — this repository's and the one `pdks init
|
|
19
|
-
* claude-code` generates — enter through it, so a session call no longer instantiates
|
|
20
|
-
* `covenant-check.js` or `@polydeukes/adapter-git`. The window DIST-01 §3-d declared (a
|
|
21
|
-
* workspace missing only that dist failing closed with no telemetry row) is gone with it.
|
|
22
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
23
19
|
*/
|
|
24
20
|
export type { ResolvedConfig } from '@polydeukes/core';
|
|
25
21
|
export { type ClaudeCodeHookSpec, runClaudeCodeHook } from './claude-code-hook.js';
|
|
26
|
-
export { type CovenantCheckSpec, runCovenantCheck } from './covenant-check.js';
|
|
22
|
+
export { type CheckDomain, type CovenantCheckSpec, runCovenantCheck } from './covenant-check.js';
|
|
27
23
|
export { type LoadedConfig, loadConfig } from './load-config.js';
|
package/dist/index.js
CHANGED
|
@@ -3,22 +3,18 @@
|
|
|
3
3
|
* coding partner.
|
|
4
4
|
*
|
|
5
5
|
* Pre-alpha. This package reserves the unscoped `polydeukes` name and is the umbrella /
|
|
6
|
-
* `pdks` CLI entry point. It owns the config discovery loader
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* `pdks` CLI entry point. It owns the config discovery loader and both surfaces'
|
|
7
|
+
* composition roots — `runCovenantCheck` for the commit surface and `runClaudeCodeHook`
|
|
8
|
+
* for the session one — because assembly needs an adapter and the covenant package at
|
|
9
|
+
* once, which no sibling is allowed to depend on. The covenant, ledger, memory, and verify
|
|
10
|
+
* modules live in their own `@polydeukes/*` packages.
|
|
11
11
|
*
|
|
12
12
|
* This file is a barrel and nothing more. ESM re-exports are eager, so anything defined
|
|
13
|
-
* here would be instantiated by every consumer of any other export
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* here would be instantiated by every consumer of any other export. Keep definitions in
|
|
14
|
+
* their own modules and let importers reach them directly. A session call enters through
|
|
15
|
+
* the published `./claude-code` subpath instead, which keeps the commit surface and its git
|
|
16
|
+
* adapter off that load path.
|
|
16
17
|
*
|
|
17
|
-
* The mirror of that coupling is closed as of DIST-02. `exports` publishes `./claude-code`
|
|
18
|
-
* alongside `"."`, and both delegators — this repository's and the one `pdks init
|
|
19
|
-
* claude-code` generates — enter through it, so a session call no longer instantiates
|
|
20
|
-
* `covenant-check.js` or `@polydeukes/adapter-git`. The window DIST-01 §3-d declared (a
|
|
21
|
-
* workspace missing only that dist failing closed with no telemetry row) is gone with it.
|
|
22
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
23
19
|
*/
|
|
24
20
|
export { runClaudeCodeHook } from './claude-code-hook.js';
|
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `initClaudeCode` — the session-surface installer
|
|
2
|
+
* `initClaudeCode` — the session-surface installer.
|
|
3
3
|
*
|
|
4
4
|
* One command wires a project into the session surface: prove the package resolves, run the
|
|
5
5
|
* shared project-side scaffold ({@link scaffoldProject}), then add what this distribution
|
|
6
|
-
* path owns — the delegator hook file, its `.claude/settings.json` registration,
|
|
7
|
-
* discipline file that tells an agent the docs query exists
|
|
6
|
+
* path owns — the delegator hook file, its `.claude/settings.json` registration, the
|
|
7
|
+
* discipline file that tells an agent the docs query exists, and the classification skill
|
|
8
|
+
* that turns a described problem into a config entry.
|
|
8
9
|
*
|
|
9
|
-
* Preflight comes first and nothing is written before it clears
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Preflight comes first and nothing is written before it clears. A generated hook whose
|
|
11
|
+
* import can never resolve blocks every call through its own fail-closed catch, and a tree
|
|
12
|
+
* that also has no config and no valve to open cannot be edited back into shape from inside
|
|
13
|
+
* the session.
|
|
13
14
|
*
|
|
14
|
-
* Nothing existing is overwritten
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Nothing existing is overwritten. The settings file in particular is merged, never
|
|
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
18
|
*/
|
|
18
19
|
import { type ScaffoldReport } from './scaffold-project.js';
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The generated classification skill — the procedure that turns a described problem into a
|
|
22
|
+
* registered entry. A classification procedure an agent never learns about is one that never
|
|
23
|
+
* runs, so it ships as an artifact of the install rather than as prose in a README.
|
|
24
|
+
*
|
|
25
|
+
* Its advise-consumption section is the delivery path for advised rows: the session surface
|
|
26
|
+
* lets an advised call through with exit 0, and the reason never reaches the model at call
|
|
27
|
+
* time — reading the telemetry log at task boundaries is the only way it arrives.
|
|
28
|
+
*/
|
|
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";
|
|
30
|
+
/** `initClaudeCode` input — the target tree and the preflight seam. */
|
|
20
31
|
export type InitClaudeCodeSpec = {
|
|
21
32
|
/** Project root to install into — every write below is relative to it. */
|
|
22
33
|
projectRoot: string;
|
|
23
34
|
/**
|
|
24
|
-
*
|
|
35
|
+
* Preflight seam: throws when the package cannot be resolved from the given root.
|
|
25
36
|
* ABSENT uses the real resolution, anchored at that root and nowhere else — anchoring it
|
|
26
37
|
* at the installer's own module would answer for the installer's install graph rather
|
|
27
38
|
* than the target project's, which is precisely the case that must fail.
|
|
@@ -29,11 +40,11 @@ export type InitClaudeCodeSpec = {
|
|
|
29
40
|
resolvePolydeukes?: (projectRoot: string) => void;
|
|
30
41
|
};
|
|
31
42
|
/**
|
|
32
|
-
* Install the session surface into `spec.projectRoot
|
|
33
|
-
*
|
|
43
|
+
* Install the session surface into `spec.projectRoot`, skipping whatever is already there
|
|
44
|
+
* and reporting both halves per artifact.
|
|
34
45
|
*
|
|
35
|
-
* Throws before any write when the package cannot be resolved from that root
|
|
36
|
-
*
|
|
37
|
-
*
|
|
46
|
+
* Throws before any write when the package cannot be resolved from that root or when two
|
|
47
|
+
* config spellings already coexist there — both leave zero files. Translating a throw into
|
|
48
|
+
* exit 2 with the install command is the bin's job.
|
|
38
49
|
*/
|
|
39
50
|
export declare function initClaudeCode(spec: InitClaudeCodeSpec): ScaffoldReport;
|