polydeukes 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +11 -4
- package/README.md +21 -5
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +5 -6
- package/dist/bin.js +92 -38
- package/dist/claude-code-hook.d.ts +40 -17
- package/dist/claude-code-hook.js +187 -175
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +50 -36
- package/dist/covenant-check.js +167 -193
- package/dist/covenant-module.d.ts +25 -0
- package/dist/covenant-module.js +42 -0
- package/dist/docs/configuration.md +17 -9
- package/dist/docs/installation.md +42 -12
- package/dist/docs/reference/adapter-claude-code.md +6 -4
- package/dist/docs/reference/adapter-git.md +23 -10
- package/dist/docs/reference/configuration.md +265 -103
- package/dist/docs/reference/core.md +15 -7
- package/dist/docs/reference/covenant.md +32 -24
- package/dist/docs/reference/polydeukes.md +132 -32
- package/dist/docs/troubleshooting.md +37 -8
- package/dist/docs-query.d.ts +10 -10
- package/dist/docs-query.js +12 -12
- package/dist/explain.d.ts +25 -0
- package/dist/explain.js +153 -0
- package/dist/index.d.ts +11 -16
- package/dist/index.js +10 -15
- package/dist/init-claude-code.d.ts +31 -18
- package/dist/init-claude-code.js +254 -40
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +17 -15
- package/dist/load-config.js +13 -12
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.d.ts +23 -14
- package/dist/scaffold-project.js +97 -32
- package/dist/schema/polydeukes.schema.json +54 -81
- package/package.json +7 -7
package/dist/init-claude-code.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
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. 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.
|
|
17
20
|
*/
|
|
18
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
22
|
import { findPackageJSON } from 'node:module';
|
|
@@ -21,26 +24,27 @@ import { dirname, join } from 'node:path';
|
|
|
21
24
|
import { MUTATING_TOOLS, SHELL_TOOLS } from '@polydeukes/adapter-claude-code';
|
|
22
25
|
import { isPlainObject } from '@polydeukes/core';
|
|
23
26
|
import { TOPICS } from './docs-query.js';
|
|
27
|
+
import { retargetGrokHookCommandToClaude } from './init-grok.js';
|
|
24
28
|
import { CONFIG_FILENAMES } from './load-config.js';
|
|
25
29
|
import { scaffoldProject } from './scaffold-project.js';
|
|
26
|
-
/** The published entry point the generated hook loads the judge through
|
|
30
|
+
/** The published entry point the generated hook loads the judge through. */
|
|
27
31
|
const HOOK_SPECIFIER = 'polydeukes/claude-code';
|
|
28
32
|
/** The registration artifacts, as `projectRoot`-relative paths (the report vocabulary). */
|
|
29
33
|
const HOOK_RELATIVE = '.claude/hooks/covenant-pretooluse.mjs';
|
|
30
34
|
const SETTINGS_RELATIVE = '.claude/settings.json';
|
|
31
35
|
const DISCOVERY_RELATIVE = '.claude/rules/polydeukes.md';
|
|
36
|
+
const SKILL_RELATIVE = '.claude/skills/discipline-draft/SKILL.md';
|
|
32
37
|
/**
|
|
33
38
|
* The command the host spawns, and the string our registration is recognized by: the same
|
|
34
|
-
* command already present means already registered
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* command already present means already registered. A registration keyed on anything else
|
|
40
|
+
* would be re-added on every run, and the host would then spawn the judge twice per call —
|
|
41
|
+
* every verdict and every telemetry row doubled.
|
|
37
42
|
*/
|
|
38
43
|
const HOOK_COMMAND = `node "$CLAUDE_PROJECT_DIR"/${HOOK_RELATIVE}`;
|
|
39
44
|
/** Which calls reach the judge — the adapter's own vocabulary, never a copy of it. */
|
|
40
45
|
const HOOK_MATCHER = [...MUTATING_TOOLS, ...SHELL_TOOLS].join('|');
|
|
41
46
|
/**
|
|
42
|
-
* The generated hook
|
|
43
|
-
* narrative removed. It carries no assembly at all, so upgrading the package upgrades the
|
|
47
|
+
* The generated hook. It carries no assembly at all, so upgrading the package upgrades the
|
|
44
48
|
* judge without regenerating this file.
|
|
45
49
|
*/
|
|
46
50
|
const GENERATED_HOOK = `#!/usr/bin/env node
|
|
@@ -71,13 +75,16 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
71
75
|
try {
|
|
72
76
|
const { runClaudeCodeHook } = await import('${HOOK_SPECIFIER}');
|
|
73
77
|
const { exitCode } = await runClaudeCodeHook({ repoRoot });
|
|
74
|
-
process.exit(
|
|
78
|
+
// Assign and let the process end naturally instead of process.exit(): an explicit exit
|
|
79
|
+
// can preempt a buffered stderr write on platforms with async pipes, dropping the break
|
|
80
|
+
// reason the agent needs to read.
|
|
81
|
+
process.exitCode = exitCode;
|
|
75
82
|
} catch (error) {
|
|
76
83
|
console.error(\`covenant hook failed closed: \${error?.message ?? error}\`);
|
|
77
|
-
process.
|
|
84
|
+
process.exitCode = 2;
|
|
78
85
|
}
|
|
79
86
|
`;
|
|
80
|
-
/** What a session is about to do, per topic — the correspondence
|
|
87
|
+
/** What a session is about to do, per topic — the correspondence the generated file carries. */
|
|
81
88
|
const DOCS_TOPIC_PURPOSE = {
|
|
82
89
|
install: 'install Polydeukes, or wire another surface into this project',
|
|
83
90
|
config: 'edit `polydeukes.config.*` — every key and what reads it',
|
|
@@ -86,11 +93,10 @@ const DOCS_TOPIC_PURPOSE = {
|
|
|
86
93
|
witness: 'open a blocked call in person',
|
|
87
94
|
};
|
|
88
95
|
/**
|
|
89
|
-
* The generated discipline file
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* it out of context until a Polydeukes path is in play.
|
|
96
|
+
* The generated discipline file — the discovery path that gets the query surface called. A
|
|
97
|
+
* query an agent never learns about is a query that does not exist. One scoped file costs
|
|
98
|
+
* nothing while it waits: `paths` frontmatter keeps it out of context until a Polydeukes
|
|
99
|
+
* path is in play.
|
|
94
100
|
*
|
|
95
101
|
* Both the command forms and the topic names come from the shipped surface itself — a file
|
|
96
102
|
* naming a query that exits 2 fails the agent once, and it never calls the command again.
|
|
@@ -117,12 +123,218 @@ manager's exec form — from the project root.
|
|
|
117
123
|
| --- | --- |
|
|
118
124
|
${TOPICS.map((topic) => `| ${DOCS_TOPIC_PURPOSE[topic]} | \`pdks docs ${topic}\` |`).join('\n')}
|
|
119
125
|
`;
|
|
126
|
+
/**
|
|
127
|
+
* The generated classification skill — the procedure that turns a described problem into a
|
|
128
|
+
* registered entry. A classification procedure an agent never learns about is one that never
|
|
129
|
+
* runs, so it ships as an artifact of the install rather than as prose in a README.
|
|
130
|
+
*
|
|
131
|
+
* Its advise-consumption section is the delivery path for advised rows: the session surface
|
|
132
|
+
* lets an advised call through with exit 0, and the reason never reaches the model at call
|
|
133
|
+
* time — reading the telemetry log at task boundaries is the only way it arrives.
|
|
134
|
+
*/
|
|
135
|
+
export const GENERATED_SKILL = `---
|
|
136
|
+
name: discipline-draft
|
|
137
|
+
description: Turn a described discipline problem into a registered entry in polydeukes.config — 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").
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
# discipline-draft — from a problem description to a registered discipline
|
|
141
|
+
|
|
142
|
+
This project is judged by Polydeukes. A discipline starts as prose and climbs a ladder —
|
|
143
|
+
\`draft\` (registered, read, never judged) → \`advise\` (judged, recorded, never stops a call) →
|
|
144
|
+
\`block\` (stops the call; the user's explicit choice, never the default). This skill walks a
|
|
145
|
+
problem description down to the right first rung and registers it.
|
|
146
|
+
|
|
147
|
+
## Procedure
|
|
148
|
+
|
|
149
|
+
### 1. Restate the problem as a promise
|
|
150
|
+
|
|
151
|
+
Rewrite the description as one sentence of the form "X must not happen" or "when A happens,
|
|
152
|
+
B must also happen". If the sentence needs "unless" more than once, split it into two
|
|
153
|
+
promises and classify each separately.
|
|
154
|
+
|
|
155
|
+
### 2. Classify the shape
|
|
156
|
+
|
|
157
|
+
Ask these questions in order; the first yes decides.
|
|
158
|
+
|
|
159
|
+
| # | Question | Entry key |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | \`declare\` (mechanism \`added-only\`) |
|
|
162
|
+
| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | \`declare\` (mechanism \`self-absolution-ban\`) |
|
|
163
|
+
| 3 | Is it about the shell command line itself, regardless of files? | \`declare\` (mechanism \`forbidden-command\`, reading the \`command\` source) |
|
|
164
|
+
| 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) |
|
|
165
|
+
| 5 | None of the above | \`draft: true\` (step 4b) |
|
|
166
|
+
|
|
167
|
+
An \`added-only\` declaration forgives existing occurrences — only what the edit adds breaks
|
|
168
|
+
the promise. That is usually what you want: a discipline adopted today should not indict
|
|
169
|
+
yesterday's code.
|
|
170
|
+
|
|
171
|
+
One path-shaped promise takes no \`disciplines:\` entry at all: a path nobody may touch
|
|
172
|
+
belongs in the top-level \`protectedPaths:\` list — its own config block, never an entry key.
|
|
173
|
+
|
|
174
|
+
### 3. Check the observation boundary
|
|
175
|
+
|
|
176
|
+
Two kinds of promise cannot be judged here, whatever their shape:
|
|
177
|
+
|
|
178
|
+
- **Destruction outside the repository** — judgment observes the project root only. Register
|
|
179
|
+
nothing; use the agent's own permission deny policy for commands like \`rm -rf ~\`.
|
|
180
|
+
- **Writes by child processes** — a test runner or script writing files is invisible to the
|
|
181
|
+
session surface, which judges declared tool calls only. Say so to the user; the commit
|
|
182
|
+
surface will still see the result as a staged diff.
|
|
183
|
+
|
|
184
|
+
### 4a. Expressible now — register a judged entry
|
|
185
|
+
|
|
186
|
+
Add the entry to the \`disciplines:\` array in \`polydeukes.config.yaml\`. Advise is the default
|
|
187
|
+
landing — a break is recorded as \`advised\` and the call goes on — and the \`enforce: advise\`
|
|
188
|
+
line below only spells that default out. NEVER write \`enforce: block\` from this skill:
|
|
189
|
+
promotion to block is the user's own choice, made after the advise measurements have been
|
|
190
|
+
read.
|
|
191
|
+
|
|
192
|
+
The examples below are whole documents, so \`languages:\` — the schema's one required block —
|
|
193
|
+
appears alongside the entry; in a config that already has one, copy the entry only.
|
|
194
|
+
|
|
195
|
+
\`\`\`yaml
|
|
196
|
+
languages:
|
|
197
|
+
placeholder:
|
|
198
|
+
productionGlob: 'src/**'
|
|
199
|
+
testCmd: 'echo "set a verification command for {scope}"'
|
|
200
|
+
disciplines:
|
|
201
|
+
- id: 'no-focused-tests'
|
|
202
|
+
why: 'a committed .only silently shrinks the suite to one test'
|
|
203
|
+
declare:
|
|
204
|
+
mechanism: 'added-only'
|
|
205
|
+
scope: { source: 'target.path', include: ['^src/'] }
|
|
206
|
+
supply: { pre: 'empty', post: 'empty' }
|
|
207
|
+
extract:
|
|
208
|
+
before:
|
|
209
|
+
- { op: 'source', of: 'pre' }
|
|
210
|
+
- { op: 'lines' }
|
|
211
|
+
- { op: 'keyByPattern', re: '(\\.only\\()' }
|
|
212
|
+
after:
|
|
213
|
+
- { op: 'source', of: 'post' }
|
|
214
|
+
- { op: 'lines' }
|
|
215
|
+
- { op: 'keyByPattern', re: '(\\.only\\()' }
|
|
216
|
+
added:
|
|
217
|
+
- { op: 'onlyIn', of: 'after', notIn: 'before' }
|
|
218
|
+
relate:
|
|
219
|
+
- id: 'nothing-added'
|
|
220
|
+
relation: { op: 'empty', of: 'added' }
|
|
221
|
+
message: 'adds {key}: {value}'
|
|
222
|
+
enforce: advise
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
A command-line ban reads the fixed source \`command\` and scopes on it — the scope is part of
|
|
226
|
+
the mechanism's shape, so a \`forbidden-command\` entry without it is refused at load time:
|
|
227
|
+
|
|
228
|
+
\`\`\`yaml
|
|
229
|
+
languages:
|
|
230
|
+
placeholder:
|
|
231
|
+
productionGlob: 'src/**'
|
|
232
|
+
testCmd: 'echo "set a verification command for {scope}"'
|
|
233
|
+
disciplines:
|
|
234
|
+
- id: 'no-force-push'
|
|
235
|
+
why: 'a force push rewrites history nobody reviewed'
|
|
236
|
+
declare:
|
|
237
|
+
mechanism: 'forbidden-command'
|
|
238
|
+
scope: { source: 'command' }
|
|
239
|
+
extract:
|
|
240
|
+
hits:
|
|
241
|
+
- { op: 'source', of: 'command' }
|
|
242
|
+
- { op: 'lines' }
|
|
243
|
+
- { op: 'matches', re: 'git push\\\\b.*--force(?![\\\\w-])' }
|
|
244
|
+
relate:
|
|
245
|
+
- { id: 'no-force', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
|
|
246
|
+
enforce: advise
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
**Write the regex yourself — the user states the promise, you author the pattern.** The
|
|
250
|
+
pattern is the part users find hardest, so never hand the prose back and ask for one. Three
|
|
251
|
+
authoring traps, each measured on a live config:
|
|
252
|
+
|
|
253
|
+
- **A pattern answers a syntactic question only.** "Is this string a forbidden word" is
|
|
254
|
+
syntax; "is this a new dependency version" is meaning, and a regex leaks both ways on a
|
|
255
|
+
semantic question. When the question is semantic, narrow the declaration's own \`scope\`
|
|
256
|
+
block to the files where any match IS a break, or accept "editing this file at all" as
|
|
257
|
+
the trigger.
|
|
258
|
+
- **\`^\` means what the preceding step left.** After a \`lines\` step a declaration's
|
|
259
|
+
pattern sees one line at a time, so \`^\` anchors to that line; over an unsplit source it
|
|
260
|
+
anchors to the whole text and matches the first line only. A ban over the command line
|
|
261
|
+
puts \`lines\` before its \`matches\` for exactly that reason.
|
|
262
|
+
- **Author both directions.** Before registering, write down one string the pattern must
|
|
263
|
+
match and one nearby string it must not (\`only(\` vs \`only_helper(\`, a flag vs its
|
|
264
|
+
substring). A pattern checked in only the breaking direction over-fires in review-proof
|
|
265
|
+
ways.
|
|
266
|
+
|
|
267
|
+
### 4b. Not expressible yet — register a draft
|
|
268
|
+
|
|
269
|
+
A draft is prose with a handle: \`id\`, \`why\`, and the literal marker \`draft: true\` — no other
|
|
270
|
+
keys. It produces no judgment and no telemetry; \`pdks explain\` lists it as unpromoted.
|
|
271
|
+
Record the SHAPE of the promise inside \`why\`, so the promotion destination is already
|
|
272
|
+
written down when a later engine can express it. Name the shape in these terms:
|
|
273
|
+
|
|
274
|
+
| Shape | The promise reads like |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| pairing | every element of set A has a counterpart in set B (translation keys, i18n) |
|
|
277
|
+
| companion | if X appears in a unit, Y must appear with it |
|
|
278
|
+
| ordered | a sequence must keep its order (migration journals, version ladders) |
|
|
279
|
+
| fingerprint | a derived artifact must match the hash/stamp of its source |
|
|
280
|
+
| producer-owned | only a designated generator may write this artifact |
|
|
281
|
+
| self-absolution | the party being judged must not write its own verdict field |
|
|
282
|
+
| actor-scope | the same action is fine for one actor and a break for another |
|
|
283
|
+
| phase-order | several precedents, in a fixed order |
|
|
284
|
+
| turn-locality | the evidence must be in the same turn or time window |
|
|
285
|
+
| stated-ground | the reason must be written down before the action |
|
|
286
|
+
| controlled-vocabulary | only an enumerated set of words/values is allowed |
|
|
287
|
+
| naming-convention | names must match a pattern per kind |
|
|
288
|
+
| irreversible-marker | once present, a marker may never be removed |
|
|
289
|
+
| delegation-scope | a delegated task may touch only its granted scope |
|
|
290
|
+
| scope-valve | a defined exception valve, judged rather than ad hoc |
|
|
291
|
+
| claim-verification | the claim must be re-run/measured, not trusted |
|
|
292
|
+
|
|
293
|
+
\`\`\`yaml
|
|
294
|
+
languages:
|
|
295
|
+
placeholder:
|
|
296
|
+
productionGlob: 'src/**'
|
|
297
|
+
testCmd: 'echo "set a verification command for {scope}"'
|
|
298
|
+
disciplines:
|
|
299
|
+
- id: 'locale-files-move-together'
|
|
300
|
+
why: 'pairing — en.json and ko.json must change in the same commit; one side alone is a break'
|
|
301
|
+
draft: true
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
### 5. Prove it fires, then close
|
|
305
|
+
|
|
306
|
+
Run \`pdks explain\` and confirm the new entry is listed (a judged entry with its mechanism
|
|
307
|
+
and surfaces; a draft as unpromoted).
|
|
308
|
+
|
|
309
|
+
For a judged entry, registration is not the finish — a pattern that never fires protects
|
|
310
|
+
nothing while looking installed. Fire it once for real, with the proof run the declaration's
|
|
311
|
+
own mechanism can actually reach:
|
|
312
|
+
|
|
313
|
+
| Mechanism | Break it once | The entry's id shows up in |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| a file-reading one (\`added-only\`, \`naming\`, …) | one scratch edit matching the must-match direction | \`pdks covenant check --worktree\` output — the exit stays 0 at advise, the id is the proof |
|
|
316
|
+
| \`forbidden-command\` | run one harmless command matching the pattern | the telemetry log tail — at advise the call proceeds and its row records the id |
|
|
317
|
+
| \`precedent\` | one in-scope edit made without the required precedent | the telemetry log tail — a declaration reading the session judges on the session surface only (the commit surface has none, so its \`supply\` policy records it \`skipped\`) |
|
|
318
|
+
|
|
319
|
+
Then undo the scratch break, repeat the same run, and confirm silence on the
|
|
320
|
+
must-NOT-match direction. Close by telling the user which rung the entry landed on and
|
|
321
|
+
that \`enforce: block\` is theirs to add later if the advise record earns it.
|
|
322
|
+
|
|
323
|
+
## Reading the advise record
|
|
324
|
+
|
|
325
|
+
An \`advised\` row means a promise was broken and the call went through anyway. Rows land in
|
|
326
|
+
the telemetry log at the path configured by \`telemetry.logPath\` (default
|
|
327
|
+
\`.polydeukes/roi.log\`). The hook's stderr note is not shown to you, so consult the log at
|
|
328
|
+
task boundaries: before committing, or after a batch of edits, read the tail and act on any
|
|
329
|
+
\`advised\` row — fix the break, or tell the user why it should stand. An advisory nobody
|
|
330
|
+
reads measures nothing.
|
|
331
|
+
`;
|
|
120
332
|
/**
|
|
121
333
|
* The default preflight: is `polydeukes` installed where `projectRoot` can reach it?
|
|
122
334
|
*
|
|
123
335
|
* ESM resolution specifically, because that is what the generated hook's `await import(...)`
|
|
124
|
-
* runs; the CJS alternatives were measured disagreeing in both directions
|
|
125
|
-
*
|
|
336
|
+
* runs; the CJS alternatives were measured disagreeing with it in both directions.
|
|
337
|
+
* `findPackageJSON` is experimental in Node 24, so its behaviour can still change.
|
|
126
338
|
*/
|
|
127
339
|
function resolveFromProjectRoot(projectRoot) {
|
|
128
340
|
// Absence throws here rather than returning undefined (Node 24.18); the branch guards the
|
|
@@ -135,7 +347,7 @@ function resolveFromProjectRoot(projectRoot) {
|
|
|
135
347
|
// map, so a version predating the session subpath, or one whose dist was never built,
|
|
136
348
|
// passes a bare-name check while the generated hook fails on every call. That tree cannot
|
|
137
349
|
// be reopened with the witness token either, because an assembly crash lands before any
|
|
138
|
-
// verdict
|
|
350
|
+
// verdict.
|
|
139
351
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
140
352
|
const subpath = isPlainObject(manifest) && isPlainObject(manifest.exports)
|
|
141
353
|
? manifest.exports[`./${HOOK_SPECIFIER.split('/')[1]}`]
|
|
@@ -209,9 +421,9 @@ function mergeSettings(projectRoot, settings, report) {
|
|
|
209
421
|
// Read the registration back rather than assuming the write carried it. The one outcome
|
|
210
422
|
// this installer must never produce is a successful-looking run whose judge never spawns,
|
|
211
423
|
// and the merge can drop the entry without failing — a settings file whose root is an
|
|
212
|
-
// array takes the assignment as a non-index property and `JSON.stringify` discards it
|
|
213
|
-
//
|
|
214
|
-
//
|
|
424
|
+
// array takes the assignment as a non-index property and `JSON.stringify` discards it.
|
|
425
|
+
// Checking the file instead of the shapes that reach it keeps the question finite: one
|
|
426
|
+
// code path, asked after every write, whatever arrived.
|
|
215
427
|
if (!carriesRegistration(JSON.parse(readFileSync(settingsPath, 'utf-8')))) {
|
|
216
428
|
throw new Error(`${SETTINGS_RELATIVE} in ${projectRoot} did not take the PreToolUse registration — ` +
|
|
217
429
|
'the judge would never be spawned. Fix that file and re-run');
|
|
@@ -219,12 +431,12 @@ function mergeSettings(projectRoot, settings, report) {
|
|
|
219
431
|
report.created.push(SETTINGS_RELATIVE);
|
|
220
432
|
}
|
|
221
433
|
/**
|
|
222
|
-
* Install the session surface into `spec.projectRoot
|
|
223
|
-
*
|
|
434
|
+
* Install the session surface into `spec.projectRoot`, skipping whatever is already there
|
|
435
|
+
* and reporting both halves per artifact.
|
|
224
436
|
*
|
|
225
|
-
* Throws before any write when the package cannot be resolved from that root
|
|
226
|
-
*
|
|
227
|
-
*
|
|
437
|
+
* Throws before any write when the package cannot be resolved from that root or when two
|
|
438
|
+
* config spellings already coexist there — both leave zero files. Translating a throw into
|
|
439
|
+
* exit 2 with the install command is the bin's job.
|
|
228
440
|
*/
|
|
229
441
|
export function initClaudeCode(spec) {
|
|
230
442
|
const resolvePolydeukes = spec.resolvePolydeukes ?? resolveFromProjectRoot;
|
|
@@ -235,21 +447,23 @@ export function initClaudeCode(spec) {
|
|
|
235
447
|
// The message names the package because the user's next action is installing it — the
|
|
236
448
|
// seam's own message cannot be relied on to say so. The original is carried through
|
|
237
449
|
// rather than discarded: "not exposed" and "not installed" need different actions, and
|
|
238
|
-
// an experimental resolver can fail for reasons that are neither
|
|
450
|
+
// an experimental resolver can fail for reasons that are neither.
|
|
239
451
|
throw new Error(`cannot use 'polydeukes' from ${spec.projectRoot} — install or update it there first ` +
|
|
240
452
|
"(e.g. 'npm install --save-dev polydeukes'), then run this command again: " +
|
|
241
453
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
242
454
|
}
|
|
243
|
-
// Every read that can fail is settled before the first write
|
|
455
|
+
// Every read that can fail is settled before the first write.
|
|
244
456
|
const settings = readSettings(spec.projectRoot);
|
|
245
457
|
const report = scaffoldProject(spec.projectRoot);
|
|
246
458
|
writeIfAbsent(spec.projectRoot, HOOK_RELATIVE, GENERATED_HOOK, report);
|
|
247
459
|
mergeSettings(spec.projectRoot, settings, report);
|
|
248
460
|
// Written last, after the registration the hook needs to ever be spawned. Every write
|
|
249
461
|
// between the hook file and that registration widens the window where a throw leaves a
|
|
250
|
-
// delegator nothing invokes — a tree that looks installed and is judged by nothing.
|
|
251
|
-
//
|
|
252
|
-
// failure costs least.
|
|
462
|
+
// delegator nothing invokes — a tree that looks installed and is judged by nothing. These
|
|
463
|
+
// two artifacts are the ones whose absence costs only discoverability, so they go where
|
|
464
|
+
// a failure costs least.
|
|
253
465
|
writeIfAbsent(spec.projectRoot, DISCOVERY_RELATIVE, GENERATED_DISCOVERY, report);
|
|
466
|
+
writeIfAbsent(spec.projectRoot, SKILL_RELATIVE, GENERATED_SKILL, report);
|
|
467
|
+
retargetGrokHookCommandToClaude(spec.projectRoot);
|
|
254
468
|
return report;
|
|
255
469
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `initGrok` — the Grok session-surface installer.
|
|
3
|
+
*
|
|
4
|
+
* Preflight first, then the shared project-side scaffold, then the generated hook and the
|
|
5
|
+
* `.grok/hooks` JSON registration. When a Claude delegator is already on disk, the JSON
|
|
6
|
+
* command names that file instead of planting a second one — two command strings would
|
|
7
|
+
* spawn two judges per call.
|
|
8
|
+
*
|
|
9
|
+
* Nothing existing is overwritten, with one command-field exception: a grok JSON whose
|
|
10
|
+
* `command` is still the grok-mjs string is rewritten to the Claude-hook command when that
|
|
11
|
+
* Claude file is on disk — otherwise grok-then-claude leaves two command strings. Any entry
|
|
12
|
+
* naming the Claude-hook command also takes the matcher of the `.claude/settings.json` entry
|
|
13
|
+
* carrying that command, because the host collapses the two registrations only when command
|
|
14
|
+
* and matcher are byte-identical; with no such entry the matcher stays. Timeout stays either
|
|
15
|
+
* way, and a command that is not the grok-mjs string is left byte-identical.
|
|
16
|
+
*
|
|
17
|
+
* Rules and skills are not copied; `.claude/settings.json` is not written.
|
|
18
|
+
*/
|
|
19
|
+
import { type ScaffoldReport } from './scaffold-project.ts';
|
|
20
|
+
/** `initGrok` input — the target tree and the preflight seam. */
|
|
21
|
+
export type InitGrokSpec = {
|
|
22
|
+
/** Project root to install into — every write below is relative to it. */
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
/**
|
|
25
|
+
* Preflight seam: throws when the package cannot be resolved from the given root.
|
|
26
|
+
* ABSENT uses the real resolution, anchored at that root and nowhere else — anchoring it
|
|
27
|
+
* at the installer's own module would answer for the installer's install graph rather
|
|
28
|
+
* than the target project's, which is precisely the case that must fail.
|
|
29
|
+
*/
|
|
30
|
+
resolvePolydeukes?: (projectRoot: string) => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Rewrite the grok JSON `command` from the grok-mjs string to the Claude-hook string.
|
|
34
|
+
*
|
|
35
|
+
* The Claude installer calls this after writing its delegator; this installer calls it on
|
|
36
|
+
* re-run when that file is already on disk. Only the installer-generated grok-mjs command
|
|
37
|
+
* is rewritten; any other string is the consumer's spawn target and the file is not touched.
|
|
38
|
+
* Every entry naming the Claude-hook command — rewritten now or by an earlier install — takes
|
|
39
|
+
* the matcher the Claude settings file registered that command under, so the host sees one
|
|
40
|
+
* pair rather than two and a re-run converges. Parse failure leaves the file as it was —
|
|
41
|
+
* existence is presence, not parse success.
|
|
42
|
+
*/
|
|
43
|
+
export declare function retargetGrokHookCommandToClaude(projectRoot: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Install the Grok session surface into `spec.projectRoot`, skipping whatever is already
|
|
46
|
+
* there and reporting both halves per artifact.
|
|
47
|
+
*
|
|
48
|
+
* Throws before any write when the package cannot be resolved from that root — that leaves
|
|
49
|
+
* zero files. Translating a throw into exit 2 with the install command is the bin's job.
|
|
50
|
+
*/
|
|
51
|
+
export declare function initGrok(spec: InitGrokSpec): ScaffoldReport;
|