polydeukes 0.6.1 → 0.8.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 +16 -28
- package/README.md +16 -28
- package/dist/baseline.d.ts +37 -1
- package/dist/baseline.js +68 -1
- package/dist/bin.d.ts +3 -4
- package/dist/bin.js +73 -90
- package/dist/covenant/bash-line.d.ts +130 -0
- package/dist/covenant/bash-line.js +566 -0
- package/dist/covenant/declaration-engine.d.ts +108 -0
- package/dist/covenant/declaration-engine.js +415 -0
- package/dist/covenant/discipline.d.ts +109 -0
- package/dist/covenant/discipline.js +667 -0
- package/dist/covenant/dispatch.d.ts +156 -0
- package/dist/covenant/dispatch.js +250 -0
- package/dist/covenant/extract-steps.d.ts +122 -0
- package/dist/covenant/extract-steps.js +0 -0
- package/dist/covenant/mention.d.ts +117 -0
- package/dist/covenant/mention.js +209 -0
- package/dist/covenant/module.d.ts +25 -0
- package/dist/covenant/module.js +23 -0
- package/dist/covenant/mutation-rules.d.ts +40 -0
- package/dist/covenant/mutation-rules.js +146 -0
- package/dist/covenant/relations.d.ts +46 -0
- package/dist/covenant/relations.js +68 -0
- package/dist/covenant/run-covenant.d.ts +95 -0
- package/dist/covenant/run-covenant.js +164 -0
- package/dist/covenant/self-mod.d.ts +55 -0
- package/dist/covenant/self-mod.js +84 -0
- package/dist/covenant/shell-evidence.d.ts +38 -0
- package/dist/covenant/shell-evidence.js +247 -0
- package/dist/covenant/shell-mod.d.ts +84 -0
- package/dist/covenant/shell-mod.js +213 -0
- package/dist/covenant/supply.d.ts +54 -0
- package/dist/covenant/supply.js +50 -0
- package/dist/covenant/transcript-mod.d.ts +60 -0
- package/dist/covenant/transcript-mod.js +254 -0
- package/dist/covenant/ttl-witness.d.ts +47 -0
- package/dist/covenant/ttl-witness.js +80 -0
- package/dist/covenant-check.d.ts +74 -41
- package/dist/covenant-check.js +372 -129
- package/dist/diff-ir.d.ts +28 -0
- package/dist/diff-ir.js +310 -0
- package/dist/docs/README.ko.md +10 -9
- package/dist/docs/README.md +10 -9
- package/dist/docs/catalog.json +76 -28
- package/dist/docs/concepts/judgment.ko.md +2 -2
- package/dist/docs/concepts/judgment.md +2 -2
- package/dist/docs/how-to/configure-project.ko.md +16 -16
- package/dist/docs/how-to/configure-project.md +15 -12
- package/dist/docs/how-to/connect-surfaces.ko.md +67 -34
- package/dist/docs/how-to/connect-surfaces.md +67 -34
- package/dist/docs/how-to/write-disciplines.ko.md +64 -7
- package/dist/docs/how-to/write-disciplines.md +65 -7
- package/dist/docs/index.json +744 -362
- package/dist/docs/reference/cli/covenant-check.ko.md +112 -65
- package/dist/docs/reference/cli/covenant-check.md +114 -59
- package/dist/docs/reference/cli/docs.ko.md +2 -2
- package/dist/docs/reference/cli/docs.md +2 -2
- package/dist/docs/reference/cli/explain.ko.md +20 -15
- package/dist/docs/reference/cli/explain.md +22 -15
- package/dist/docs/reference/cli/init.ko.md +91 -52
- package/dist/docs/reference/cli/init.md +81 -55
- package/dist/docs/reference/configuration/index.ko.md +182 -71
- package/dist/docs/reference/configuration/index.md +188 -76
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +39 -11
- package/dist/docs/reference/packages/adapter-claude-code.md +40 -14
- package/dist/docs/reference/packages/adapter-codex.ko.md +99 -0
- package/dist/docs/reference/packages/adapter-codex.md +100 -0
- package/dist/docs/reference/packages/adapter-grok.ko.md +81 -0
- package/dist/docs/reference/packages/adapter-grok.md +83 -0
- package/dist/docs/reference/packages/core.ko.md +50 -4
- package/dist/docs/reference/packages/core.md +55 -6
- package/dist/docs/reference/packages/polydeukes.ko.md +139 -77
- package/dist/docs/reference/packages/polydeukes.md +147 -81
- package/dist/docs/reference/packages/sdk-ts.ko.md +164 -0
- package/dist/docs/reference/packages/sdk-ts.md +167 -0
- package/dist/docs/troubleshooting.ko.md +51 -19
- package/dist/docs/troubleshooting.md +53 -17
- package/dist/docs/tutorials/first-judgment.ko.md +6 -4
- package/dist/docs/tutorials/first-judgment.md +6 -4
- package/dist/explain.d.ts +1 -1
- package/dist/explain.js +37 -22
- package/dist/load-config.d.ts +25 -1
- package/dist/load-config.js +21 -3
- package/dist/pre-state-reader.d.ts +1 -1
- package/dist/pre-state-reader.js +1 -1
- package/dist/scaffold-project.d.ts +2 -2
- package/dist/scaffold-project.js +12 -4
- package/dist/schema/polydeukes.schema.json +12 -0
- package/dist/worktree-reader.d.ts +19 -0
- package/dist/worktree-reader.js +30 -0
- package/package.json +5 -18
- package/dist/claude-code-hook.d.ts +0 -79
- package/dist/claude-code-hook.js +0 -372
- package/dist/claude-code.d.ts +0 -6
- package/dist/claude-code.js +0 -6
- package/dist/covenant-module.d.ts +0 -25
- package/dist/covenant-module.js +0 -42
- package/dist/docs/reference/packages/adapter-git.ko.md +0 -101
- package/dist/docs/reference/packages/adapter-git.md +0 -108
- package/dist/docs/reference/packages/covenant.ko.md +0 -115
- package/dist/docs/reference/packages/covenant.md +0 -123
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
- package/dist/init-claude-code.d.ts +0 -52
- package/dist/init-claude-code.js +0 -586
- package/dist/init-grok.d.ts +0 -51
- package/dist/init-grok.js +0 -242
package/dist/claude-code-hook.js
DELETED
|
@@ -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
|
-
}
|
package/dist/claude-code.d.ts
DELETED
|
@@ -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';
|
package/dist/claude-code.js
DELETED
|
@@ -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>;
|
package/dist/covenant-module.js
DELETED
|
@@ -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,101 +0,0 @@
|
|
|
1
|
-
# `@polydeukes/adapter-git`
|
|
2
|
-
|
|
3
|
-
[English](adapter-git.md) · **한국어**
|
|
4
|
-
|
|
5
|
-
> **커밋 표면의 번역기**입니다. 스테이징된 diff가 약속(covenant) 입력 IR이 되고,
|
|
6
|
-
> `adapters.git` 설정 네임스페이스가 여기서 정의됩니다.
|
|
7
|
-
>
|
|
8
|
-
> 알파입니다. 통합 패키지의 의존성으로 설치되므로 일반 사용자가 따로 설치하거나 불러올 필요는 없습니다. 커밋 표면은
|
|
9
|
-
> [`pdks covenant check`](polydeukes.ko.md#polydeukes-bin)를 통해 닿습니다.
|
|
10
|
-
|
|
11
|
-
<a id="ownership"></a>
|
|
12
|
-
## 담당하는 기능
|
|
13
|
-
|
|
14
|
-
Git의 변경 정보를 세션 표면과 같은 공통 입력 형식으로 번역합니다. 작성자가 AI인지 사람인지에 따라 판정 방식을 달리하지 않습니다.
|
|
15
|
-
|
|
16
|
-
| 단위 | 하는 일 |
|
|
17
|
-
|---|---|
|
|
18
|
-
| 스테이징 변경 수집 | 스테이징 영역을 내용 기준선을 갖춘 변경 목록으로 읽습니다 |
|
|
19
|
-
| 순수 번역 | 변경 목록을 하나의 `CovenantInput`으로 바꿉니다 |
|
|
20
|
-
| 설정 어휘 | 이 어댑터 자신의 설정 네임스페이스를 검증합니다 |
|
|
21
|
-
|
|
22
|
-
순수 라이브러리입니다. 스테이징된 diff의 형태를 알 뿐 설치나 훅 러너, 밸브는 알지
|
|
23
|
-
못합니다. pre-commit 훅에 배선하는 것은 우산이 맡는 배포 행위입니다.
|
|
24
|
-
`@polydeukes/core`는 `peerDependency`로 부릅니다. 판정기와 같은 어휘를 공유하며 별도 사본을 설치하지 않습니다.
|
|
25
|
-
|
|
26
|
-
<a id="collection"></a>
|
|
27
|
-
## 수집과 `adapters.git` 네임스페이스
|
|
28
|
-
|
|
29
|
-
**세 수집기가 같은 형식으로 결과를 반환합니다.** `collectStagedChanges` · `collectWorktreeChanges` ·
|
|
30
|
-
`collectRangeChanges({ repoRoot, range: '<base>..<head>' | '<base>...<head>' })`는 모두 같은
|
|
31
|
-
`StagedChange[]`를 반환하므로 이후에는 같은 번역기와 판정 경로를 사용합니다.
|
|
32
|
-
|
|
33
|
-
| 수집기 | `pre` | `post` | 그 밖에 |
|
|
34
|
-
|---|---|---|---|
|
|
35
|
-
| staged | HEAD blob | **스테이징된** blob. 워크트리는 쓰지 않습니다. `git add` 뒤에 갈라졌을 수 있습니다 | |
|
|
36
|
-
| worktree | HEAD blob | 디스크의 바이트 | 추적하지 않는 파일도 무시 대상이 아니면 `added`로 포함하며, 디스크에서 사라진 파일은 HEAD에 있었든 인덱스에만 있었든 `deleted`입니다. 읽을 수 없는 경로(깨진 심볼릭 링크)는 내용 null로 경로만 판정합니다 |
|
|
37
|
-
| range | base blob | head blob | `...`는 base를 `git merge-base`로 해소합니다. git이 해소하지 못하는 ref나 merge-base 없는 두 ref는 예외를 던집니다 |
|
|
38
|
-
|
|
39
|
-
**수집기는 증거로 사용할 수 있는 내용을 제한합니다.**
|
|
40
|
-
|
|
41
|
-
| 결정 | 이유 |
|
|
42
|
-
|---|---|
|
|
43
|
-
| 모든 수집기에서 `--no-renames` 강제 | 이름 변경을 삭제 더하기 추가로 판정합니다. 보호 파일의 `git mv`가 불투명한 이름 변경 항목 하나로 빠져나가면 안 됩니다 |
|
|
44
|
-
| 바이너리 blob이나 파일은 내용을 null로 | 손실 있게 디코딩된 바이트를 싣지 않습니다 |
|
|
45
|
-
| 첫 커밋 이전 상태는 전부 추가로 좁힘 | 예외를 던지지 않습니다. staged와 worktree가 같습니다 |
|
|
46
|
-
| 타입 변경(`T`)은 `pre`를 유지 | 심볼릭 링크가 파일로 바뀐 것은 수정이라, delta 판정이 사라진 내용을 봅니다 |
|
|
47
|
-
| 모든 목록 명령은 `--`로 끝남 | 파일과 이름이 같은 브랜치도 ref이지 모호한 인자가 아닙니다 |
|
|
48
|
-
|
|
49
|
-
번역은 변경 하나마다 도구 호출 하나를 만듭니다. 이름은 어댑터가 소유한 `staged-write`와
|
|
50
|
-
`staged-delete`입니다. 삭제는 언제나 증거를 답니다. 쓰기는 스테이징된 blob이 바이너리인
|
|
51
|
-
경우를 빼고 증거를 답니다. 그때는 견줄 텍스트가 없으므로 `fileChange` 없이 도착하고,
|
|
52
|
-
증명되지 않은 다른 호출과 똑같이 경로만으로 판정됩니다. **세션 컬렉션은 정직하게
|
|
53
|
-
비어 있습니다.** 커밋 표면에는 세션이 없고, 있는 것처럼 보이게 키를 지어내지 않습니다.
|
|
54
|
-
`actor`도 없습니다. 훅은 사람의 `git commit`과 에이전트의 것을 구별할 수 없으므로 아무것도
|
|
55
|
-
증명하지 않습니다.
|
|
56
|
-
|
|
57
|
-
**네임스페이스는 이 어댑터 자신의 어휘입니다.** 코어는 컨테이너 형태만 검증합니다.
|
|
58
|
-
어댑터마다 설정 객체 하나라는 것까지이고 내용은 그대로 통과시킵니다. 그래서 어휘와 그
|
|
59
|
-
검증기와 기본값이 전부 여기 있습니다.
|
|
60
|
-
|
|
61
|
-
| 키 | 값 | 기본값 | 뜻 |
|
|
62
|
-
|---|---|---|---|
|
|
63
|
-
| `adapters.git.enforce` | `block` \| `advise` | `block` | 커밋 표면의 판정이 무엇을 **하는지** |
|
|
64
|
-
| `adapters.git.protectedPaths` | string[] | `[]` | 커밋 표면의 **가산** 보호 범위입니다. 공통 목록 위에 얹혀 판정됩니다 |
|
|
65
|
-
|
|
66
|
-
알 수 없는 키, 허용된 두 값 이외의 `enforce`, 문자열 배열이 아닌 `protectedPaths`는
|
|
67
|
-
즉시 거부하며 오류에 전체 필드 경로를 표시합니다. 작성 레퍼런스는
|
|
68
|
-
[설정 레퍼런스의 `adapters` 절](../configuration/index.ko.md#adapters)입니다.
|
|
69
|
-
|
|
70
|
-
**추가 보호 범위는 관측자가 정합니다.** 강제 수준과 마찬가지로 해당 표면의 설정입니다. 여기 적힌 항목은 작업이 역사가 되는 시점에 판정되고 세션 표면은 그것을 읽지
|
|
71
|
-
않습니다. 저장소가 판정기 **소스**를 세션에서 편집할 수 있게 두면서도 그것을 올리는
|
|
72
|
-
커밋은 멈출 수 있는 이유가 이것입니다.
|
|
73
|
-
|
|
74
|
-
`enforce: advise`에서는 밸브를 구성하지 않습니다. 위반은 `advised`로 기록하고 권고를
|
|
75
|
-
stderr에 출력한 뒤 커밋을 계속합니다. 달라지는 것은 위반 뒤의 처리이지 판정 기준이 아닙니다.
|
|
76
|
-
판정할 수 없는 실행은 어느 강제 수준에서도 종료 코드 `2`로 차단합니다(fail-closed).
|
|
77
|
-
|
|
78
|
-
<a id="consumer-contract"></a>
|
|
79
|
-
## 사용자와의 접점
|
|
80
|
-
|
|
81
|
-
- **설정의 `adapters.git` 블록.**
|
|
82
|
-
- **`pdks covenant check`를 실행하는 pre-commit 훅.** 손으로 배선하며, 훅 관리자 셋의
|
|
83
|
-
수동 절차는
|
|
84
|
-
[설치](../../how-to/connect-surfaces.ko.md#commit-surface)에 있습니다.
|
|
85
|
-
|
|
86
|
-
일반 사용자가 직접 불러올 필요는 없습니다.
|
|
87
|
-
|
|
88
|
-
<a id="limits"></a>
|
|
89
|
-
## 선언된 한계
|
|
90
|
-
|
|
91
|
-
- **세션을 읽는 선언은 여기서 판정할 수 없습니다.** `precedent`는 세션 이력이 필요한데 커밋에는
|
|
92
|
-
없습니다. 항목이 `skipped`와 `supply-pass`를 남기는 것은 그 선언의 `supply`가 `pass`일
|
|
93
|
-
때뿐입니다. 정책이 없으면 없는 세션은 판정 불가(exit 2)입니다. 항목의 결함이 아니라 이
|
|
94
|
-
표면이 제공하는 증거의 한계입니다.
|
|
95
|
-
- **추적하지 않는 파일이 무시 대상이면 이 표면에서 관측하지 않습니다. 이미 추적 중인 파일은
|
|
96
|
-
`.gitignore`와 무관하게 관측합니다.** 무시 대상이며 한 번도 추가하지 않은 `dist`는 보이지
|
|
97
|
-
않지만, 추적 중인 파일은 나중에 무시 패턴에 해당하더라도 `git diff`에 남습니다. 세션 표면이 생성 경로를 공통 목록에
|
|
98
|
-
싣는 이유입니다.
|
|
99
|
-
- **밸브에는 터미널 앞의 사람이 필요합니다.** TTY가 없으면 프롬프트를 표시하지 못하므로
|
|
100
|
-
증언으로 차단을 해제할 수 없습니다. CI 실행과 에이전트가 시작한 `git commit`에도 같은 조건을 적용합니다. 무엇도
|
|
101
|
-
저장되지 않으므로 한 번의 답이 나중 커밋을 덮지 않습니다.
|