polydeukes 0.6.0 → 0.7.1

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.
Files changed (117) hide show
  1. package/README.ko.md +30 -70
  2. package/README.md +31 -84
  3. package/dist/baseline.d.ts +37 -1
  4. package/dist/baseline.js +68 -1
  5. package/dist/bin.d.ts +3 -4
  6. package/dist/bin.js +79 -93
  7. package/dist/covenant/bash-line.d.ts +130 -0
  8. package/dist/covenant/bash-line.js +566 -0
  9. package/dist/covenant/declaration-engine.d.ts +108 -0
  10. package/dist/covenant/declaration-engine.js +415 -0
  11. package/dist/covenant/discipline.d.ts +103 -0
  12. package/dist/covenant/discipline.js +667 -0
  13. package/dist/covenant/dispatch.d.ts +153 -0
  14. package/dist/covenant/dispatch.js +237 -0
  15. package/dist/covenant/extract-steps.d.ts +122 -0
  16. package/dist/covenant/extract-steps.js +0 -0
  17. package/dist/covenant/mention.d.ts +117 -0
  18. package/dist/covenant/mention.js +209 -0
  19. package/dist/covenant/module.d.ts +25 -0
  20. package/dist/covenant/module.js +23 -0
  21. package/dist/covenant/mutation-rules.d.ts +40 -0
  22. package/dist/covenant/mutation-rules.js +146 -0
  23. package/dist/covenant/relations.d.ts +46 -0
  24. package/dist/covenant/relations.js +68 -0
  25. package/dist/covenant/run-covenant.d.ts +95 -0
  26. package/dist/covenant/run-covenant.js +164 -0
  27. package/dist/covenant/self-mod.d.ts +55 -0
  28. package/dist/covenant/self-mod.js +84 -0
  29. package/dist/covenant/shell-evidence.d.ts +38 -0
  30. package/dist/covenant/shell-evidence.js +247 -0
  31. package/dist/covenant/shell-mod.d.ts +84 -0
  32. package/dist/covenant/shell-mod.js +213 -0
  33. package/dist/covenant/supply.d.ts +54 -0
  34. package/dist/covenant/supply.js +50 -0
  35. package/dist/covenant/transcript-mod.d.ts +60 -0
  36. package/dist/covenant/transcript-mod.js +254 -0
  37. package/dist/covenant/ttl-witness.d.ts +47 -0
  38. package/dist/covenant/ttl-witness.js +80 -0
  39. package/dist/covenant-check.d.ts +72 -39
  40. package/dist/covenant-check.js +260 -121
  41. package/dist/diff-ir.d.ts +28 -0
  42. package/dist/diff-ir.js +310 -0
  43. package/dist/docs/README.ko.md +60 -0
  44. package/dist/docs/README.md +64 -0
  45. package/dist/docs/catalog.json +480 -0
  46. package/dist/docs/concepts/judgment.ko.md +113 -0
  47. package/dist/docs/concepts/judgment.md +113 -0
  48. package/dist/docs/how-to/configure-project.ko.md +99 -0
  49. package/dist/docs/how-to/configure-project.md +98 -0
  50. package/dist/docs/how-to/connect-surfaces.ko.md +120 -0
  51. package/dist/docs/how-to/connect-surfaces.md +121 -0
  52. package/dist/docs/how-to/write-disciplines.ko.md +179 -0
  53. package/dist/docs/how-to/write-disciplines.md +181 -0
  54. package/dist/docs/index.json +2280 -0
  55. package/dist/docs/reference/cli/covenant-check.ko.md +148 -0
  56. package/dist/docs/reference/cli/covenant-check.md +153 -0
  57. package/dist/docs/reference/cli/docs.ko.md +97 -0
  58. package/dist/docs/reference/cli/docs.md +95 -0
  59. package/dist/docs/reference/cli/explain.ko.md +84 -0
  60. package/dist/docs/reference/cli/explain.md +89 -0
  61. package/dist/docs/reference/cli/init.ko.md +119 -0
  62. package/dist/docs/reference/cli/init.md +119 -0
  63. package/dist/docs/reference/configuration/index.ko.md +553 -0
  64. package/dist/docs/reference/{configuration.md → configuration/index.md} +215 -90
  65. package/dist/docs/reference/packages/adapter-claude-code.ko.md +110 -0
  66. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +47 -19
  67. package/dist/docs/reference/packages/adapter-grok.ko.md +77 -0
  68. package/dist/docs/reference/packages/adapter-grok.md +79 -0
  69. package/dist/docs/reference/packages/core.ko.md +174 -0
  70. package/dist/docs/reference/{core.md → packages/core.md} +74 -12
  71. package/dist/docs/reference/packages/polydeukes.ko.md +195 -0
  72. package/dist/docs/reference/packages/polydeukes.md +204 -0
  73. package/dist/docs/reference/packages/sdk-ts.ko.md +161 -0
  74. package/dist/docs/reference/packages/sdk-ts.md +164 -0
  75. package/dist/docs/troubleshooting.ko.md +144 -0
  76. package/dist/docs/troubleshooting.md +103 -151
  77. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  78. package/dist/docs/tutorials/first-judgment.md +81 -0
  79. package/dist/docs-catalog.d.ts +25 -0
  80. package/dist/docs-catalog.js +450 -0
  81. package/dist/docs-library.d.ts +23 -0
  82. package/dist/docs-library.js +347 -0
  83. package/dist/docs-markdown.d.ts +32 -0
  84. package/dist/docs-markdown.js +150 -0
  85. package/dist/docs-query.d.ts +11 -40
  86. package/dist/docs-query.js +28 -122
  87. package/dist/docs-types.d.ts +105 -0
  88. package/dist/docs-types.js +2 -0
  89. package/dist/explain.d.ts +1 -1
  90. package/dist/explain.js +37 -22
  91. package/dist/load-config.d.ts +1 -1
  92. package/dist/load-config.js +1 -1
  93. package/dist/pre-state-reader.d.ts +1 -1
  94. package/dist/pre-state-reader.js +1 -1
  95. package/dist/scaffold-project.d.ts +2 -2
  96. package/dist/scaffold-project.js +9 -3
  97. package/dist/schema/polydeukes.schema.json +12 -0
  98. package/dist/worktree-reader.d.ts +19 -0
  99. package/dist/worktree-reader.js +30 -0
  100. package/package.json +3 -17
  101. package/dist/claude-code-hook.d.ts +0 -79
  102. package/dist/claude-code-hook.js +0 -372
  103. package/dist/claude-code.d.ts +0 -6
  104. package/dist/claude-code.js +0 -6
  105. package/dist/covenant-module.d.ts +0 -25
  106. package/dist/covenant-module.js +0 -42
  107. package/dist/docs/configuration.md +0 -103
  108. package/dist/docs/installation.md +0 -241
  109. package/dist/docs/reference/adapter-git.md +0 -100
  110. package/dist/docs/reference/covenant.md +0 -116
  111. package/dist/docs/reference/polydeukes.md +0 -315
  112. package/dist/index.d.ts +0 -22
  113. package/dist/index.js +0 -21
  114. package/dist/init-claude-code.d.ts +0 -52
  115. package/dist/init-claude-code.js +0 -469
  116. package/dist/init-grok.d.ts +0 -51
  117. package/dist/init-grok.js +0 -242
@@ -1,79 +0,0 @@
1
- /**
2
- * `runClaudeCodeHook` — the assembled session-surface judgment runner.
3
- *
4
- * The session counterpart of {@link runCovenantCheck}, and the one place where the Claude
5
- * Code adapter (tool vocabulary, up-translation) and the covenant package (dispatcher +
6
- * judge bodies) meet. Packages stay one-way — each depends only on core — so their
7
- * composition lives here, in the umbrella, and the repository's PreToolUse hook shrinks to
8
- * a delegator that calls this function. That is what makes the session surface installable:
9
- * a consumer registers a hook that resolves this package instead of copying assembly.
10
- *
11
- * The protection-policy data (protectedPaths / disciplines / witness) is read from the root
12
- * data config through {@link loadConfig}, which also attaches the config file to its own
13
- * surface.
14
- *
15
- * The valve is the TTL witness, judged over the JSONL transcript provider. The judge body
16
- * always spawns, and only an outcome that translated to blocked consults the witness —
17
- * `witnessed` rows are would-block only. Its defence is provenance rather than secrecy: only
18
- * a real human utterance carries the transcript marking `findUserMessages()` admits.
19
- *
20
- * fail-closed: ANY failure — an unbuilt judge body, an unreadable stdin, a missing or
21
- * invalid config file — resolves to `{ exitCode: 2 }` with one `blocked` record under the
22
- * `hook` label. Nothing throws: an uncaught rejection would exit the delegator non-blocking,
23
- * the cheapest bypass vector there is. Recovery from an unbuilt clone is `pnpm build` (it
24
- * mentions no protected path, so it is never blocked).
25
- */
26
- import { type CanonicalTranscript } from '@polydeukes/core';
27
- import { type CovenantRegistration } from '@polydeukes/covenant';
28
- import { type CovenantModule } from './covenant-module.ts';
29
- import { loadConfig } from './load-config.ts';
30
- /** {@link runClaudeCodeHook} result — the exit code the hook process leaves with. */
31
- export type ClaudeCodeHookOutcome = {
32
- exitCode: 0 | 2;
33
- };
34
- /** `runClaudeCodeHook` input — the `CovenantCheckSpec` shape, session side. */
35
- export type ClaudeCodeHookSpec = {
36
- /** Repository root — config discovery and discipline glob scoping both anchor here. */
37
- repoRoot: string;
38
- /** One raw PreToolUse payload. ABSENT means read fd 0 (the hook's real stdin). */
39
- rawPayload?: string;
40
- /** Overrides the config's telemetry log path (tests and assembly injection). */
41
- telemetryPath?: string;
42
- /** Overrides the resolved covenant dist directory (tests and assembly injection). */
43
- covenantDist?: string;
44
- };
45
- /** {@link assembleSessionRegistrations} input — what the session surface's assembly needs. */
46
- export type SessionAssemblySpec = {
47
- config: ReturnType<typeof loadConfig>['config'];
48
- rootDir: string;
49
- /**
50
- * The covenant surface the registrations are built from — the module the caller loaded
51
- * from the resolved dist, so what judges a call is what that dist carries, and what
52
- * `explain` renders is what would judge it.
53
- */
54
- covenant: CovenantModule;
55
- /** The payload's transcript path. ABSENT leaves the transcript-mod registration out. */
56
- transcriptPath?: string;
57
- transcript?: CanonicalTranscript;
58
- witness?: CovenantRegistration['witness'];
59
- };
60
- /**
61
- * The session surface's registration set. One assembly, two consumers: the runner below
62
- * dispatches it, `explain` renders it — so what a reader is shown is the table the judgment
63
- * actually uses, never a second opinion about it.
64
- */
65
- export declare function assembleSessionRegistrations(spec: SessionAssemblySpec): CovenantRegistration[];
66
- /**
67
- * The session-surface entry point: the post-hoc state comparison wrapped around the judgment.
68
- *
69
- * The comparison sits OUTSIDE {@link judgeHookCall}'s fail-closed try on both ends. Inside
70
- * it, a comparison failure would become a blocked call — the opposite of a mechanism whose
71
- * whole purpose is to record rather than stop — so each side carries its own catch and
72
- * neither can reach the verdict. Observation is fail-open, the direction
73
- * `appendRecordFailOpen` already established: the worst outcome is a missing datum.
74
- *
75
- * Order is the contract. The comparison runs first, so it reads the window the previous call
76
- * left and its rows land ahead of this call's judgment; the re-establishment runs last, so
77
- * this call's own judged writes are folded in rather than alarmed on next time.
78
- */
79
- export declare function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<ClaudeCodeHookOutcome>;
@@ -1,372 +0,0 @@
1
- /**
2
- * `runClaudeCodeHook` — the assembled session-surface judgment runner.
3
- *
4
- * The session counterpart of {@link runCovenantCheck}, and the one place where the Claude
5
- * Code adapter (tool vocabulary, up-translation) and the covenant package (dispatcher +
6
- * judge bodies) meet. Packages stay one-way — each depends only on core — so their
7
- * composition lives here, in the umbrella, and the repository's PreToolUse hook shrinks to
8
- * a delegator that calls this function. That is what makes the session surface installable:
9
- * a consumer registers a hook that resolves this package instead of copying assembly.
10
- *
11
- * The protection-policy data (protectedPaths / disciplines / witness) is read from the root
12
- * data config through {@link loadConfig}, which also attaches the config file to its own
13
- * surface.
14
- *
15
- * The valve is the TTL witness, judged over the JSONL transcript provider. The judge body
16
- * always spawns, and only an outcome that translated to blocked consults the witness —
17
- * `witnessed` rows are would-block only. Its defence is provenance rather than secrecy: only
18
- * a real human utterance carries the transcript marking `findUserMessages()` admits.
19
- *
20
- * fail-closed: ANY failure — an unbuilt judge body, an unreadable stdin, a missing or
21
- * invalid config file — resolves to `{ exitCode: 2 }` with one `blocked` record under the
22
- * `hook` label. Nothing throws: an uncaught rejection would exit the delegator non-blocking,
23
- * the cheapest bypass vector there is. Recovery from an unbuilt clone is `pnpm build` (it
24
- * mentions no protected path, so it is never blocked).
25
- */
26
- import { mkdirSync, readFileSync } from 'node:fs';
27
- import { homedir } from 'node:os';
28
- import { join, resolve } from 'node:path';
29
- import { COMMAND_ARGS, MUTATING_TOOLS, runAdapterPath, SHELL_TOOLS, sessionChannelReader, sessionSourceReader, transcriptFromJsonlFile, transcriptPathFromPayload, } from '@polydeukes/adapter-claude-code';
30
- import { appendRecordFailOpen, DEFAULT_TELEMETRY_LOG_PATH, isPlainObject, normalizeProtectedPaths, readRecords, } from '@polydeukes/core';
31
- import { ttlWitness } from '@polydeukes/covenant';
32
- import { findUnattributed, readBaseline, snapshotBaseline, writeBaseline } from './baseline.js';
33
- import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
34
- import { loadConfig } from './load-config.js';
35
- import { sessionPreStateReader } from './pre-state-reader.js';
36
- /** The label every post-hoc state comparison row carries. */
37
- const BASELINE_LABEL = 'baseline';
38
- /**
39
- * Compare the protected entries' on-disk state against the stored baseline and record what
40
- * moved with no judgment explaining it.
41
- *
42
- * Runs at hook call START, before this call's own judgment rows land, so the window it reads
43
- * is the one the previous comparison left open. Returns the record count as of right now —
44
- * where the NEXT window opens, which {@link updateBaseline} persists at call end.
45
- *
46
- * The comparison records, it never blocks: no row it writes and no failure it hits changes
47
- * a verdict or an exit code, which is why every caller keeps it outside the judgment path.
48
- */
49
- function compareBaseline(spec) {
50
- const baselinePath = join(spec.repoRoot, '.polydeukes', 'baseline.json');
51
- // Read before any row of this comparison lands, so the rows this call is about to write
52
- // cannot fall inside the window they would then explain away.
53
- const { records } = readRecords(spec.telemetryPath);
54
- const stored = readBaseline(baselinePath);
55
- if (stored === null) {
56
- // Absence and corruption are the same signal. The baseline file is NOT on the protection
57
- // list — protecting it would need a comparison of its own — so its disappearance has to
58
- // stay legible in the log instead.
59
- appendRecordFailOpen(spec.telemetryPath, {
60
- event: 'unattributed',
61
- label: BASELINE_LABEL,
62
- subject: baselinePath,
63
- });
64
- return;
65
- }
66
- const changed = findUnattributed({
67
- previous: stored.entries,
68
- current: snapshotBaseline({ rootDir: spec.repoRoot, entries: spec.entries }),
69
- records,
70
- // The cut travels with the hashes it belongs to, from the one read above. Rows older
71
- // than it were already spent explaining the state that snapshot recorded.
72
- cutAt: stored.cutAt,
73
- });
74
- // One row per changed entry — an aggregate row could not say WHICH gate definition moved.
75
- for (const entry of changed) {
76
- appendRecordFailOpen(spec.telemetryPath, {
77
- event: 'unattributed',
78
- label: BASELINE_LABEL,
79
- subject: entry,
80
- });
81
- }
82
- }
83
- /**
84
- * Re-establish the baseline at hook call END.
85
- *
86
- * At call end rather than right after the comparison: refreshing at comparison time would
87
- * miss whatever this call's own judged writes changed, leaving detection permanently one
88
- * call behind.
89
- *
90
- * The cut is stamped HERE, beside the snapshot, not at the comparison that opened the call.
91
- * Both describe the same instant — everything this call did is already folded into the
92
- * hashes — so the rows explaining it belong before the cut. Stamping the earlier instant
93
- * instead would re-admit this call's own judgment rows into the next window, where they
94
- * would attribute a change they had nothing to do with: a call that merely MENTIONED a
95
- * protected entry would then absolve any tamper that followed it.
96
- */
97
- function updateBaseline(spec) {
98
- const dotDir = join(spec.repoRoot, '.polydeukes');
99
- mkdirSync(dotDir, { recursive: true });
100
- writeBaseline(join(dotDir, 'baseline.json'), snapshotBaseline({ rootDir: spec.repoRoot, entries: spec.entries }), new Date().toISOString());
101
- }
102
- /**
103
- * Where the comparison writes and what it observes, or `undefined`.
104
- *
105
- * The domain is derived from config rather than enumerated here, and the telemetry path is
106
- * resolved by the same precedence the judgment uses so both land in one log. A config that
107
- * does not load leaves NO domain, so there is nothing to compare and nothing to re-establish
108
- * — the judgment path already answers that failure fail-closed, and a comparison row on top
109
- * of it would report the same absence twice under a label that judges nothing.
110
- */
111
- function comparisonSpec(spec) {
112
- let config;
113
- try {
114
- config = loadConfig({ rootDir: spec.repoRoot }).config;
115
- }
116
- catch {
117
- return undefined;
118
- }
119
- return {
120
- repoRoot: spec.repoRoot,
121
- telemetryPath: spec.telemetryPath ??
122
- process.env.POLYDEUKES_TELEMETRY_PATH ??
123
- resolve(spec.repoRoot, config.telemetry.logPath),
124
- entries: normalizeProtectedPaths({ protectedPaths: config.protectedPaths }),
125
- };
126
- }
127
- /**
128
- * The session surface's registration set. One assembly, two consumers: the runner below
129
- * dispatches it, `explain` renders it — so what a reader is shown is the table the judgment
130
- * actually uses, never a second opinion about it.
131
- */
132
- export function assembleSessionRegistrations(spec) {
133
- const { config, rootDir, covenant, transcriptPath, transcript, witness } = spec;
134
- // The live transcript is the session a history declaration reads AND the one the witness
135
- // reads, so erasing or forging it disables every history discipline while opening or
136
- // shutting the human valve on the same file. It must NOT join this list: it lives deep
137
- // under HOME, and a path entry makes every ancestor protected — which measured as an
138
- // over-block refusing `cd /home/<user>`, `echo $HOME`, and every edit whose content
139
- // carried a bare `~`. The dedicated `transcript-mod` registration below covers that one
140
- // file instead: equality-only, never an ancestor. Ancestor destruction outside the
141
- // repository is out of observation scope — the agent's own deny policy owns what no
142
- // repo-scoped judge can reach.
143
- const protectedPaths = normalizeProtectedPaths({
144
- protectedPaths: config.protectedPaths ?? [],
145
- });
146
- const disciplines = config.disciplines ?? [];
147
- const registrations = [
148
- covenant.selfModRegistration({
149
- protectedPaths,
150
- mutatingToolNames: MUTATING_TOOLS,
151
- witness,
152
- }),
153
- covenant.shellModRegistration({
154
- protectedPaths,
155
- shellTools: SHELL_TOOLS,
156
- commandArgs: COMMAND_ARGS,
157
- witness,
158
- }),
159
- // Routing is the matches predicate, never path mention, so the home directory cannot
160
- // become a protected ancestor. No transcript in the payload means nothing to protect —
161
- // the valve and every history declaration already forfeited on the same absence.
162
- ...(transcriptPath === undefined
163
- ? []
164
- : [
165
- covenant.transcriptModRegistration({
166
- transcriptPath,
167
- // The env value first, since that is what the judged shell expands `~` and
168
- // `$HOME` from. `homedir()` reads the same passwd entry bash falls back to when
169
- // HOME is unset, so a hook spawned without an environment (a service manager,
170
- // `env -i`) keeps judging the home spellings instead of silently going
171
- // absolute-only — an inert spelling closure looks identical to a passing call.
172
- home: process.env.HOME ?? homedir(),
173
- shellTools: SHELL_TOOLS,
174
- commandArgs: COMMAND_ARGS,
175
- mutatingTools: MUTATING_TOOLS,
176
- witness,
177
- }),
178
- ]),
179
- ...covenant.compileDisciplineRegistrations({
180
- disciplines,
181
- rootDir,
182
- shellTools: SHELL_TOOLS,
183
- commandArgs: COMMAND_ARGS,
184
- readPreState: sessionPreStateReader,
185
- // One PreToolUse call is the whole observation, so the derived change set is a
186
- // singleton and a change-set declaration cannot be judged here — it records `skipped`,
187
- // the shape the commit surface gives a history declaration.
188
- observesChangeSet: false,
189
- witness,
190
- // The session itself, injected rather than its path: a declaration reading a
191
- // `transcript` binding sees it flattened, and passing a path would leak JSONL
192
- // knowledge into covenant.
193
- transcript,
194
- }),
195
- ];
196
- return registrations;
197
- }
198
- /**
199
- * This runtime's mutating+shell roster, rewritten onto the Claude vocabulary the adapter
200
- * already judges. Claude names are not keys, so an existing Write/Edit/Bash envelope
201
- * passes through. A name outside the table is left alone — that is a declared limit,
202
- * recorded as the adapter's funnel pass, never a parse fault. The map lives here, not in
203
- * the adapter, so the adapter stays Claude-vocabulary-only.
204
- */
205
- const GROK_TOOL_NAME_MAP = {
206
- write: 'Write',
207
- search_replace: 'Edit',
208
- run_terminal_command: 'Bash',
209
- };
210
- /**
211
- * Rewrite Grok tool names in a raw PreToolUse payload. Invalid JSON is left as the original
212
- * string so the existing fail-closed path still runs — this function must not throw.
213
- */
214
- function rewriteGrokToolNames(rawPayload) {
215
- let parsed;
216
- try {
217
- parsed = JSON.parse(rawPayload);
218
- }
219
- catch {
220
- return rawPayload;
221
- }
222
- if (!isPlainObject(parsed))
223
- return rawPayload;
224
- for (const key of ['tool_name', 'toolName']) {
225
- const value = parsed[key];
226
- if (typeof value !== 'string')
227
- continue;
228
- const mapped = GROK_TOOL_NAME_MAP[value];
229
- if (mapped !== undefined)
230
- parsed[key] = mapped;
231
- }
232
- return JSON.stringify(parsed);
233
- }
234
- /**
235
- * Judge one declared tool call before it runs. Async because the dispatcher spawns covenant
236
- * bodies — a synchronous runner would mean reimplementing the judge, which the
237
- * single-dispatcher principle forbids.
238
- */
239
- async function judgeHookCall(spec) {
240
- // Env-first telemetry precedence, settled BEFORE any failure branch: a config that never
241
- // loads still has somewhere to write its one blocked row. The config value applies after
242
- // the load succeeds.
243
- //
244
- // Computed INSIDE the try even though it must run first, because `join` throws on a
245
- // non-string repoRoot and this function's contract is that nothing escapes it — a rejection
246
- // would exit a delegator non-blocking, which is the cheapest bypass there is. A throw here
247
- // leaves `telemetryPath` undefined, which the catch tolerates: there is no root to write a
248
- // row under anyway.
249
- let telemetryPath;
250
- try {
251
- const envTelemetryPath = process.env.POLYDEUKES_TELEMETRY_PATH;
252
- telemetryPath =
253
- spec.telemetryPath ?? envTelemetryPath ?? join(spec.repoRoot, DEFAULT_TELEMETRY_LOG_PATH);
254
- // Discovery + parse + validation are the loader's job; a throw here (absent, ambiguous,
255
- // unparseable, or invalid config) falls into the fail-closed catch.
256
- const { config } = loadConfig({ rootDir: spec.repoRoot });
257
- telemetryPath =
258
- spec.telemetryPath ?? envTelemetryPath ?? resolve(spec.repoRoot, config.telemetry.logPath);
259
- // Settled for the rest of the happy path. The `let` above exists so the catch can still
260
- // record when a failure lands before this point; a closure cannot narrow it, so the
261
- // dispatch seam below takes this const instead.
262
- const logPath = telemetryPath;
263
- const rawPayload = rewriteGrokToolNames(spec.rawPayload ?? readFileSync(0, 'utf-8'));
264
- // The transcript path travels in the raw payload only — up-translation drops it, so the
265
- // adapter reads it from the string. Every failure narrows to `undefined`, which leaves
266
- // the dispatcher on its `noopTranscript` default: lost evidence closes the valve rather
267
- // than opening it.
268
- const transcriptPath = transcriptPathFromPayload({ rawPayload });
269
- const transcript = transcriptPath === undefined ? undefined : transcriptFromJsonlFile({ path: transcriptPath });
270
- // One witness predicate shared by every registration: a witness is a session-wide
271
- // permission the human granted, not a per-covenant one. Absent `witness` config leaves
272
- // this undefined, and no verdict can be witnessed open at all. The predicate receives
273
- // the transcript as its second argument from the dispatcher, which is why the transcript
274
- // is injected below rather than captured here.
275
- const witness = config.witness === undefined
276
- ? undefined
277
- : ttlWitness({
278
- token: config.witness.token,
279
- // Minutes are the human-facing unit in config; the predicate takes milliseconds.
280
- // Core passes the value through verbatim, so the conversion belongs to assembly.
281
- ttlMs: config.witness.ttlMinutes * 60_000,
282
- });
283
- // The judges are the covenant package's built barrel — resolved through the real
284
- // package (never a test alias), so the session surface runs the same judges the commit
285
- // surface does. An injected directory overrides that resolution, which is how a fixture
286
- // reaches a dist that real Node resolution would never land on. Awaited HERE, before
287
- // any registration is composed: a dist the barrel cannot load throws now, into the
288
- // fail-closed catch, instead of leaving a half-judged table behind.
289
- const covenantDist = spec.covenantDist ?? resolveCovenantDist();
290
- const covenant = await loadCovenantModule(covenantDist);
291
- // Assembled HERE, outside the dispatch seam: a judge takes its call set as an argument,
292
- // so assembly needs no payload, and an assembly throw belongs to this function's own
293
- // fail-closed catch — `hook` label, `covenant hook failed closed:` on stderr. Composed
294
- // inside the dispatch closure it would land in `runAdapterPath`'s catch instead, which
295
- // records the adapter's label and says nothing about what broke.
296
- const registrations = assembleSessionRegistrations({
297
- config,
298
- rootDir: spec.repoRoot,
299
- covenant,
300
- transcriptPath,
301
- transcript,
302
- witness,
303
- });
304
- // The world axis: files read from disk under the repository root, channels read beside
305
- // the session's transcript. The disk is the pre-edit state on this surface; the rule
306
- // that the judged change's own `post` overrides it belongs to the judge, so the root
307
- // supplies what it read and nothing more. No `changes` list either — one PreToolUse call
308
- // is the whole observation, and the judge derives that set from the input.
309
- const { files, channels } = covenant.supplySources({
310
- plan: covenant.planSources({ registrations }),
311
- read: sessionSourceReader({ repoRoot: spec.repoRoot }),
312
- readChannel: sessionChannelReader({ transcriptPath }),
313
- });
314
- return await runAdapterPath({
315
- rawPayload,
316
- telemetryPath: logPath,
317
- dispatch: (stdinPayload) => covenant.dispatchCovenants({
318
- stdinPayload,
319
- registrations,
320
- telemetryPath: logPath,
321
- transcript,
322
- world: { files, channels },
323
- }),
324
- });
325
- }
326
- catch (error) {
327
- process.stderr.write(`covenant hook failed closed: ${error instanceof Error ? error.message : String(error)}\n`);
328
- // Honor the one-call-one-record invariant with a blocked record under the assembly's own
329
- // label — never a judge's, since no judge answered. `undefined` means the failure landed
330
- // before a path could even be composed (a non-string repoRoot), where there is nowhere
331
- // to write and nothing to attribute the row to.
332
- if (telemetryPath !== undefined) {
333
- appendRecordFailOpen(telemetryPath, { event: 'blocked', label: 'hook', subject: '-' });
334
- }
335
- return { exitCode: 2 };
336
- }
337
- }
338
- /**
339
- * The session-surface entry point: the post-hoc state comparison wrapped around the judgment.
340
- *
341
- * The comparison sits OUTSIDE {@link judgeHookCall}'s fail-closed try on both ends. Inside
342
- * it, a comparison failure would become a blocked call — the opposite of a mechanism whose
343
- * whole purpose is to record rather than stop — so each side carries its own catch and
344
- * neither can reach the verdict. Observation is fail-open, the direction
345
- * `appendRecordFailOpen` already established: the worst outcome is a missing datum.
346
- *
347
- * Order is the contract. The comparison runs first, so it reads the window the previous call
348
- * left and its rows land ahead of this call's judgment; the re-establishment runs last, so
349
- * this call's own judged writes are folded in rather than alarmed on next time.
350
- */
351
- export async function runClaudeCodeHook(spec) {
352
- let comparison;
353
- try {
354
- comparison = comparisonSpec(spec);
355
- if (comparison !== undefined) {
356
- compareBaseline(comparison);
357
- }
358
- }
359
- catch {
360
- // fail-open: a comparison that could not run leaves the judgment exactly as it was.
361
- }
362
- const result = await judgeHookCall(spec);
363
- try {
364
- if (comparison !== undefined) {
365
- updateBaseline(comparison);
366
- }
367
- }
368
- catch {
369
- // fail-open: an unwritable baseline costs the next call's detection, never this verdict.
370
- }
371
- return result;
372
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * `polydeukes/claude-code` — the session surface's entry point.
3
- *
4
- * The delegator a consumer project's PreToolUse hook imports: one verb and its spec type.
5
- */
6
- export { type ClaudeCodeHookOutcome, type ClaudeCodeHookSpec, runClaudeCodeHook, } from './claude-code-hook.ts';
@@ -1,6 +0,0 @@
1
- /**
2
- * `polydeukes/claude-code` — the session surface's entry point.
3
- *
4
- * The delegator a consumer project's PreToolUse hook imports: one verb and its spec type.
5
- */
6
- export { runClaudeCodeHook, } from './claude-code-hook.js';
@@ -1,25 +0,0 @@
1
- /**
2
- * The covenant package as a resolved artifact — the existence proof both composition roots
3
- * share.
4
- *
5
- * What the roots prove is the package IMPORT itself. The barrel is eager: a dist missing one
6
- * of the modules it references throws on import, before any assembly can compose a
7
- * registration, and the surface's own fail-closed catch records that as one `blocked` row. A
8
- * partially loaded judge set has no representation here — an ESM import either fully succeeds
9
- * or throws.
10
- *
11
- * The `covenantDist` seam selects WHICH dist is imported, so a fixture can inject a gutted
12
- * mirror where real Node resolution would always land on the healthy build.
13
- */
14
- import type * as covenant from '@polydeukes/covenant';
15
- /** The covenant surface both roots assemble against — the members they call, and no more. */
16
- export type CovenantModule = Pick<typeof covenant, 'dispatchCovenants' | 'compileDisciplineRegistrations' | 'selfModRegistration' | 'shellModRegistration' | 'transcriptModRegistration' | 'planSources' | 'supplySources'>;
17
- /** Where real Node resolution puts the covenant package's built barrel. */
18
- export declare function resolveCovenantDist(): string;
19
- /**
20
- * Import the covenant barrel from `distDir`, naming the recovery command when it will not
21
- * load. The message carries the loader's own text, which names the module that is missing;
22
- * a reader locked out by an unbuilt or half-built dist needs both that name and the one
23
- * command that fixes it.
24
- */
25
- export declare function loadCovenantModule(distDir: string): Promise<CovenantModule>;
@@ -1,42 +0,0 @@
1
- /**
2
- * The covenant package as a resolved artifact — the existence proof both composition roots
3
- * share.
4
- *
5
- * What the roots prove is the package IMPORT itself. The barrel is eager: a dist missing one
6
- * of the modules it references throws on import, before any assembly can compose a
7
- * registration, and the surface's own fail-closed catch records that as one `blocked` row. A
8
- * partially loaded judge set has no representation here — an ESM import either fully succeeds
9
- * or throws.
10
- *
11
- * The `covenantDist` seam selects WHICH dist is imported, so a fixture can inject a gutted
12
- * mirror where real Node resolution would always land on the healthy build.
13
- */
14
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
15
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
16
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
17
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
18
- });
19
- }
20
- return path;
21
- };
22
- import { createRequire } from 'node:module';
23
- import { join } from 'node:path';
24
- import { pathToFileURL } from 'node:url';
25
- /** Where real Node resolution puts the covenant package's built barrel. */
26
- export function resolveCovenantDist() {
27
- return join(createRequire(import.meta.url).resolve('@polydeukes/covenant'), '..');
28
- }
29
- /**
30
- * Import the covenant barrel from `distDir`, naming the recovery command when it will not
31
- * load. The message carries the loader's own text, which names the module that is missing;
32
- * a reader locked out by an unbuilt or half-built dist needs both that name and the one
33
- * command that fixes it.
34
- */
35
- export async function loadCovenantModule(distDir) {
36
- try {
37
- return (await import(__rewriteRelativeImportExtension(pathToFileURL(join(distDir, 'index.js')).href)));
38
- }
39
- catch (error) {
40
- throw new Error(`the covenant judges could not be loaded from ${distDir} — run 'pnpm build' to rebuild them: ${error instanceof Error ? error.message : String(error)}`);
41
- }
42
- }
@@ -1,103 +0,0 @@
1
- # Configuring Polydeukes
2
-
3
- **English** · [한국어](./configuration.ko.md)
4
-
5
- > Alpha. This guide covers the config surface as shipped today (schema v2, loader, and
6
- > the declaration grammar). Fields and steps will grow; what is
7
- > written here is tested and enforced now.
8
-
9
- `polydeukes.config.yaml` is the one file where a project declares its disciplines — the
10
- promises the human and the AI partner both agree to be bound by. It is **data, not code**:
11
- nothing in it can compute, so nothing in it can lie. The core validates it, the covenant
12
- package enforces it, and every judgment it causes is measured.
13
-
14
- This is the guide layer: the file itself, how discovery fails, the IDE wiring, and what
15
- enforcement looks like. Every key, with its full rules and pitfalls, lives in
16
- [the configuration reference](./reference/configuration.md).
17
-
18
- ## The file
19
-
20
- Put exactly one of these at the project root:
21
-
22
- | Filename | Note |
23
- |---|---|
24
- | `polydeukes.config.yaml` | canonical |
25
- | `polydeukes.config.yml` | accepted variant |
26
- | `polydeukes.config.json` | accepted variant (read by the same parser — YAML is a JSON superset) |
27
-
28
- Discovery is deliberately strict, and every failure refuses loudly instead of guessing:
29
-
30
- - **No config found** → error naming all three candidate filenames. A missing config never
31
- silently loads defaults — silent defaults would mean silently unprotected.
32
- - **More than one found** → error naming the collisions. Ambiguity never picks a winner.
33
- - **Parse error, or a custom YAML tag** → error naming the file. Custom tags are rejected
34
- even though the parser cannot execute them — config data stays uncomputable by contract.
35
- - **Schema violation** → error naming the key and the file. Unknown keys are rejected
36
- wherever the core owns the vocabulary — the top level, and the fixed keys inside a
37
- discipline entry — so `protectedPath:` for `protectedPaths:`, or `adaptors:` for
38
- `adapters:`, is caught here. Two maps stay open, because their keys are your values
39
- rather than the core's: language names under `languages`, and adapter names under
40
- `adapters`. A misspelt adapter name is accepted and its block simply goes unread, which
41
- leaves that adapter on its defaults — check the name against the adapter's own reference.
42
- Inside a namespace the vocabulary belongs to that adapter: the core passes contents
43
- through verbatim, and the adapter's own validator rejects what it does not recognise,
44
- naming the full field path (see
45
- [the `adapters` reference](./reference/configuration.md#adapters)).
46
-
47
- ## IDE support
48
-
49
- The JSON Schema gives autocompletion and validation in editors. It ships inside the
50
- `polydeukes` package, so the line names a path into your own `node_modules`:
51
-
52
- ```yaml
53
- # yaml-language-server: $schema=node_modules/polydeukes/dist/schema/polydeukes.schema.json
54
- ```
55
-
56
- For a JSON config, use the standard top-level key instead. The loader accepts it and drops
57
- it from the resolved config:
58
-
59
- ```json
60
- { "$schema": "node_modules/polydeukes/dist/schema/polydeukes.schema.json" }
61
- ```
62
-
63
- **The path is resolved against the directory your config sits in**, not against a project
64
- root the editor infers. The spelling above is right when the two are the same place. When
65
- they are not — a config in a monorepo sub-package whose dependencies installed at the
66
- workspace root — count the levels up yourself:
67
-
68
- ```yaml
69
- # yaml-language-server: $schema=../../node_modules/polydeukes/dist/schema/polydeukes.schema.json
70
- ```
71
-
72
- `pdks init claude-code` writes the line only when the schema is where the plain spelling
73
- names it. If the generated config has no such line, that is the case above, and the prefix
74
- is yours to add — an unresolvable path costs you validation without reporting anything.
75
-
76
- If you installed `@polydeukes/core` directly rather than the umbrella, name its own copy:
77
-
78
- ```yaml
79
- # yaml-language-server: $schema=node_modules/@polydeukes/core/schema/polydeukes.schema.json
80
- ```
81
-
82
- Every value here is a **file path**, not a module specifier: `$schema` is a static string an
83
- editor reads, so no module resolver runs on it. Code that reads the schema at runtime uses
84
- the package subpath `polydeukes/schema.json` instead.
85
-
86
- ## What enforcement looks like
87
-
88
- A `disciplines:` entry lands at **advise** by default: a break is recorded as `advised`
89
- with the discipline's `id` in the telemetry record, the break message with its `why` goes
90
- to stderr, and the call proceeds (exit 0) — the judgment measures instead of stopping.
91
- Writing `enforce: block` on an entry is the promotion: that entry then **blocks (exit 2)**
92
- before the call runs. The sanctioned valve on a block is the witness — a human supplying
93
- the pass condition on a judgment that actually blocked, recorded as `witnessed` — never
94
- silent.
95
-
96
- What blocks without being asked is the judging chain's own protection, a finite list: the
97
- `protectedPaths` entries (tool-axis and shell-axis mutations, and mentions without a
98
- read-only head), the session transcript, and the assembly itself — a missing, ambiguous, or
99
- invalid config, an unbuilt judge, an unparseable payload, or a routing that could not
100
- answer. At either level the system fails closed on these, because a dead gate that waves
101
- things through is the cheapest bypass of all. On the commit surface `adapters.git.enforce: advise` relaxes
102
- the protected-path verdicts to `advised` as well — it is the observer's setting — while an
103
- assembly that cannot judge still fails closed.