polydeukes 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +3 -2
- package/README.md +10 -2
- package/dist/bin.d.ts +5 -6
- package/dist/bin.js +75 -38
- package/dist/claude-code-hook.d.ts +45 -15
- package/dist/claude-code-hook.js +245 -158
- package/dist/covenant-check.d.ts +58 -31
- package/dist/covenant-check.js +145 -179
- package/dist/covenant-module.d.ts +25 -0
- package/dist/covenant-module.js +34 -0
- package/dist/docs/configuration.md +44 -302
- package/dist/docs/installation.md +7 -3
- package/dist/docs/reference/adapter-git.md +17 -6
- package/dist/docs/reference/configuration.md +338 -0
- package/dist/docs/reference/core.md +8 -6
- package/dist/docs/reference/covenant.md +13 -5
- package/dist/docs/reference/polydeukes.md +103 -26
- package/dist/docs/troubleshooting.md +12 -2
- package/dist/docs-query.d.ts +10 -10
- package/dist/docs-query.js +20 -20
- package/dist/explain.d.ts +27 -0
- package/dist/explain.js +152 -0
- package/dist/index.d.ts +10 -14
- package/dist/index.js +9 -13
- package/dist/init-claude-code.d.ts +28 -17
- package/dist/init-claude-code.js +208 -40
- package/dist/load-config.d.ts +15 -16
- package/dist/load-config.js +21 -14
- package/dist/scaffold-project.d.ts +20 -20
- package/dist/scaffold-project.js +69 -30
- package/dist/schema/polydeukes.schema.json +229 -0
- package/package.json +8 -7
package/README.ko.md
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
표면에 편입시킵니다.
|
|
19
19
|
- **`pdks covenant check`.** `pdks` bin의 첫 실물 서브커맨드입니다(`polydeukes`는 별칭).
|
|
20
20
|
pre-commit 판정 러너로, 스테이징 영역의 변경을 `@polydeukes/adapter-git`이 수집해
|
|
21
|
-
약속(covenant) 입력 IR로 번역하고, 세션 훅이
|
|
22
|
-
판정기는 하나, 표면은 여럿입니다.
|
|
21
|
+
약속(covenant) 입력 IR로 번역하고, 세션 훅이 부르는 바로 그 in-process 판정기로 보냅니다.
|
|
22
|
+
판정기는 하나, 표면은 여럿입니다. `--worktree`와 `--range <base>..<head>`는 같은 판정을
|
|
23
|
+
작업 트리나 ref 범위에 진단 호출로 돌리며, 증인 프롬프트는 뜨지 않습니다. 맥락족 규율(discipline, `requirePrecedent`)도 다른 규율과
|
|
23
24
|
똑같이 조립되지만, 읽을 세션이 없으므로 스킵 등록이 됩니다. 스테이징 변경과 매치하면 자기
|
|
24
25
|
id와 그 변경을 담은 `skipped` 이벤트를 남기고 커밋은 진행됩니다. 여기서 판정하면 걸리는
|
|
25
26
|
커밋마다 정당하게 통과할 길 없이 막히고, 아예 걸러내면 게이트가 물러섰다는 사실이 가려집니다.
|
package/README.md
CHANGED
|
@@ -18,14 +18,22 @@ one-way, through the core alone.
|
|
|
18
18
|
itself to its own protection surface.
|
|
19
19
|
- **`pdks covenant check`** — the first real subcommand of the `pdks` bin (`polydeukes` is an
|
|
20
20
|
alias). A pre-commit judgment runner: staged changes are collected by `@polydeukes/adapter-git`,
|
|
21
|
-
translated into the covenant input IR, and dispatched through the very
|
|
22
|
-
hook
|
|
21
|
+
translated into the covenant input IR, and dispatched through the very in-process judges the
|
|
22
|
+
session hook calls — one judge, every surface. `--worktree` and `--range <base>..<head>` run the same
|
|
23
|
+
judgment over the working tree or a ref range as a diagnostic call, with no witness prompt.
|
|
24
|
+
Context-family disciplines (`requirePrecedent`) assemble
|
|
23
25
|
here like any other, but with no session to read they become skip registrations: when one
|
|
24
26
|
matches a staged change it records a `skipped` event carrying its id and that change, and the
|
|
25
27
|
commit proceeds. Judging them would block every matching commit with no legitimate pass path;
|
|
26
28
|
filtering them out would hide that a gate stood down. It is the same disposition the session
|
|
27
29
|
surface uses when it has no transcript. An empty staging area is an explicit pass; a missing or
|
|
28
30
|
invalid config fails closed.
|
|
31
|
+
- **`pdks explain`** — the assembly reader. It assembles both surfaces' registration sets through
|
|
32
|
+
the same functions the two runners call and prints them without judging: every registration
|
|
33
|
+
with its kind (`meta` / `judge` / `skip` / `excluded`), the routing scope of each entry, whether
|
|
34
|
+
it carries a `why`, and for each skip the compiler's reason — the one that otherwise reaches
|
|
35
|
+
stderr only when the cause is a config fault. No judge thunk is called and no telemetry row is
|
|
36
|
+
written. A config that cannot be loaded fails at exit `2` with stdout at zero bytes.
|
|
29
37
|
- **`pdks init claude-code`** — the session-surface installer. It proves `polydeukes` resolves
|
|
30
38
|
from the target project before writing anything, then creates what every distribution path
|
|
31
39
|
shares (the data config and its `.polydeukes/` ignore line) and what this path owns (a delegator
|
package/dist/bin.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `pdks` / `polydeukes` — the umbrella bin
|
|
3
|
+
* `pdks` / `polydeukes` — the umbrella bin.
|
|
4
4
|
*
|
|
5
|
-
* A thin argv shim
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (fail-closed, the same posture as an unjudgeable payload).
|
|
5
|
+
* A thin argv shim: four subcommands, each matched by direct comparison against a finite
|
|
6
|
+
* table. Anything else prints usage and exits 2 — an unknown argument must never pass
|
|
7
|
+
* silently (fail-closed, the same posture as an unjudgeable payload).
|
|
9
8
|
*
|
|
10
9
|
* The real TTY is wired HERE, not in the library: the runner receives an injectable
|
|
11
10
|
* seam, and this shim binds it to /dev/tty. When /dev/tty cannot be opened (git run by
|
|
12
11
|
* CI or by an agent-spawned shell — no controlling terminal), the seam stays absent and
|
|
13
|
-
* the valve is structurally unreachable
|
|
12
|
+
* the witness valve is structurally unreachable, so only a human at a terminal can arm it.
|
|
14
13
|
*/
|
|
15
14
|
export {};
|
package/dist/bin.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `pdks` / `polydeukes` — the umbrella bin
|
|
3
|
+
* `pdks` / `polydeukes` — the umbrella bin.
|
|
4
4
|
*
|
|
5
|
-
* A thin argv shim
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (fail-closed, the same posture as an unjudgeable payload).
|
|
5
|
+
* A thin argv shim: four subcommands, each matched by direct comparison against a finite
|
|
6
|
+
* table. Anything else prints usage and exits 2 — an unknown argument must never pass
|
|
7
|
+
* silently (fail-closed, the same posture as an unjudgeable payload).
|
|
9
8
|
*
|
|
10
9
|
* The real TTY is wired HERE, not in the library: the runner receives an injectable
|
|
11
10
|
* seam, and this shim binds it to /dev/tty. When /dev/tty cannot be opened (git run by
|
|
12
11
|
* CI or by an agent-spawned shell — no controlling terminal), the seam stays absent and
|
|
13
|
-
* the valve is structurally unreachable
|
|
12
|
+
* the witness valve is structurally unreachable, so only a human at a terminal can arm it.
|
|
14
13
|
*/
|
|
15
14
|
import { closeSync, openSync, readSync, writeSync } from 'node:fs';
|
|
16
15
|
import { dirname, join } from 'node:path';
|
|
@@ -46,20 +45,32 @@ function openTtyPrompt() {
|
|
|
46
45
|
closeSync(fd);
|
|
47
46
|
}
|
|
48
47
|
catch {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// seam (PR #41 review). The valve caches its verdict, so this is defensive.
|
|
48
|
+
// An EBADF thrown from this finally would override the `return null` above and
|
|
49
|
+
// escape the seam.
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
};
|
|
55
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Write `text` to stdout and end the process — exit 0 once the write drains, exit 2 when
|
|
56
|
+
* the reader went away. A piped write is asynchronous, so the exit waits for the flush; a
|
|
57
|
+
* reader that closes mid-write makes the stream emit `error` outside any try frame, and
|
|
58
|
+
* this handler is what keeps that off node's default exit 1 with a stack trace.
|
|
59
|
+
*/
|
|
60
|
+
async function emitAndExit(text) {
|
|
61
|
+
process.stdout.on('error', () => process.exit(2));
|
|
62
|
+
await new Promise((settle) => {
|
|
63
|
+
process.stdout.write(text, () => settle());
|
|
64
|
+
});
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
56
67
|
const args = process.argv.slice(2);
|
|
57
68
|
if (args.length === 2 && args[0] === 'init' && args[1] === 'claude-code') {
|
|
58
69
|
try {
|
|
59
70
|
// Imported inside the try, not above it: ESM imports are eager, so the installer stays
|
|
60
|
-
// off `covenant check`'s load path
|
|
61
|
-
//
|
|
62
|
-
//
|
|
71
|
+
// off `covenant check`'s load path, which a pre-commit hook spawns on every commit. A
|
|
72
|
+
// rejected import outside the try would reach node's unhandled-rejection exit 1, the
|
|
73
|
+
// exact crash this bin refuses to make.
|
|
63
74
|
const { initClaudeCode } = await import('./init-claude-code.js');
|
|
64
75
|
const { created, skipped } = initClaudeCode({ projectRoot: process.cwd() });
|
|
65
76
|
for (const path of created) {
|
|
@@ -71,8 +82,8 @@ if (args.length === 2 && args[0] === 'init' && args[1] === 'claude-code') {
|
|
|
71
82
|
process.exit(0);
|
|
72
83
|
}
|
|
73
84
|
catch (error) {
|
|
74
|
-
// A precondition failure leaves zero files
|
|
75
|
-
//
|
|
85
|
+
// A precondition failure leaves zero files; the message names what the user has to do
|
|
86
|
+
// before running this again.
|
|
76
87
|
process.stderr.write(`pdks init claude-code failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
77
88
|
process.exit(2);
|
|
78
89
|
}
|
|
@@ -80,40 +91,65 @@ if (args.length === 2 && args[0] === 'init' && args[1] === 'claude-code') {
|
|
|
80
91
|
if (args[0] === 'docs' && args.length <= 2) {
|
|
81
92
|
try {
|
|
82
93
|
// Imported inside the try for the same reason `init` is: the query core and the
|
|
83
|
-
// markdown behind it have no business on `covenant check`'s load path
|
|
84
|
-
// spawns on every commit.
|
|
94
|
+
// markdown behind it have no business on `covenant check`'s load path.
|
|
85
95
|
const { queryDocs } = await import('./docs-query.js');
|
|
86
96
|
// The bundle ships beside this file, so the docs root comes from the module's own
|
|
87
97
|
// location — never from the working directory, which is whatever shell invoked us.
|
|
88
98
|
const docsRoot = join(dirname(fileURLToPath(import.meta.url)), 'docs');
|
|
89
99
|
const { text } = queryDocs({ docsRoot, topic: args[1] });
|
|
90
|
-
|
|
91
|
-
// capture) makes the stream emit `error` — an EventEmitter event, so it fires outside
|
|
92
|
-
// the frame this try guards and would reach node's default handler: exit 1 and a raw
|
|
93
|
-
// stack trace, the one disposition this bin never produces. The docs answer is not a
|
|
94
|
-
// verdict, so a reader that stopped listening is not something to report; end at the
|
|
95
|
-
// same code an unanswerable query uses.
|
|
96
|
-
process.stdout.on('error', () => process.exit(2));
|
|
97
|
-
// stdout is a pipe whenever this is captured or redirected, and a piped write is
|
|
98
|
-
// asynchronous — exiting on the next line would discard whatever is still buffered.
|
|
99
|
-
// The whole answer IS the deliverable here (a truncated document is one an agent
|
|
100
|
-
// quotes onward as if complete), so the exit waits for the flush. Awaiting rather
|
|
101
|
-
// than exiting from the callback also keeps this branch from falling through into
|
|
102
|
-
// the covenant runner below while the write drains.
|
|
103
|
-
await new Promise((settle) => {
|
|
104
|
-
process.stdout.write(text, () => settle());
|
|
105
|
-
});
|
|
106
|
-
process.exit(0);
|
|
100
|
+
await emitAndExit(text);
|
|
107
101
|
}
|
|
108
102
|
catch (error) {
|
|
109
|
-
// stdout stays at zero bytes on this path
|
|
110
|
-
//
|
|
103
|
+
// stdout stays at zero bytes on this path: what cannot be answered is never answered
|
|
104
|
+
// halfway.
|
|
111
105
|
process.stderr.write(`pdks docs: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
112
106
|
process.exit(2);
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
|
-
if (args.length
|
|
116
|
-
|
|
109
|
+
if (args.length === 1 && args[0] === 'explain') {
|
|
110
|
+
try {
|
|
111
|
+
// Imported inside the try for the same reason `docs` is: the renderer pulls in both
|
|
112
|
+
// composition roots, and neither belongs on `covenant check`'s load path.
|
|
113
|
+
const { explain } = await import('./explain.js');
|
|
114
|
+
const { text } = await explain({ repoRoot: process.cwd() });
|
|
115
|
+
await emitAndExit(text);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// stdout stays at zero bytes on this path: what cannot be answered is never answered
|
|
119
|
+
// halfway.
|
|
120
|
+
process.stderr.write(`pdks explain: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Read the `covenant check` flags as a domain, or null when the argv is not one of the
|
|
126
|
+
* three recognized forms: no flags is the staged diff, `--worktree` is the working tree,
|
|
127
|
+
* and `--range <base>..<head>` (or `...` for the merge-base reading) is a ref range.
|
|
128
|
+
*/
|
|
129
|
+
function parseCheckDomain(flags) {
|
|
130
|
+
if (flags.length === 0)
|
|
131
|
+
return { kind: 'staged' };
|
|
132
|
+
if (flags.length === 1 && flags[0] === '--worktree')
|
|
133
|
+
return { kind: 'worktree' };
|
|
134
|
+
if (flags.length !== 2 || flags[0] !== '--range')
|
|
135
|
+
return null;
|
|
136
|
+
const range = flags[1];
|
|
137
|
+
if (range.startsWith('--'))
|
|
138
|
+
return null;
|
|
139
|
+
const mergeBase = range.includes('...');
|
|
140
|
+
const separator = mergeBase ? '...' : '..';
|
|
141
|
+
const at = range.indexOf(separator);
|
|
142
|
+
if (at === -1)
|
|
143
|
+
return null;
|
|
144
|
+
const base = range.slice(0, at);
|
|
145
|
+
const head = range.slice(at + separator.length);
|
|
146
|
+
if (base === '' || head === '')
|
|
147
|
+
return null;
|
|
148
|
+
return { kind: 'range', base, head, ...(mergeBase && { ancestry: 'merge-base' }) };
|
|
149
|
+
}
|
|
150
|
+
const domain = args[0] === 'covenant' && args[1] === 'check' ? parseCheckDomain(args.slice(2)) : null;
|
|
151
|
+
if (domain === null) {
|
|
152
|
+
process.stderr.write('usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain | pdks init claude-code | pdks docs [topic]\n');
|
|
117
153
|
process.exit(2);
|
|
118
154
|
}
|
|
119
155
|
try {
|
|
@@ -127,12 +163,13 @@ try {
|
|
|
127
163
|
const { exitCode } = await runCovenantCheck({
|
|
128
164
|
repoRoot: process.cwd(),
|
|
129
165
|
ttyPrompt: openTtyPrompt(),
|
|
166
|
+
domain,
|
|
130
167
|
});
|
|
131
168
|
process.exit(exitCode);
|
|
132
169
|
}
|
|
133
170
|
catch (error) {
|
|
134
171
|
// Any failure the runner did not already translate is unjudgeable — block, never
|
|
135
|
-
// crash into node's exit 1
|
|
172
|
+
// crash into node's exit 1.
|
|
136
173
|
process.stderr.write(`covenant check failed closed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
137
174
|
process.exit(2);
|
|
138
175
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `runClaudeCodeHook` — the assembled session-surface judgment runner
|
|
2
|
+
* `runClaudeCodeHook` — the assembled session-surface judgment runner.
|
|
3
3
|
*
|
|
4
4
|
* The session counterpart of {@link runCovenantCheck}, and the one place where the Claude
|
|
5
5
|
* Code adapter (tool vocabulary, up-translation) and the covenant package (dispatcher +
|
|
@@ -8,17 +8,14 @@
|
|
|
8
8
|
* a delegator that calls this function. That is what makes the session surface installable:
|
|
9
9
|
* a consumer registers a hook that resolves this package instead of copying assembly.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* root data config through {@link loadConfig} (CONFIG-03), which also attaches the config
|
|
15
|
-
* file to its own surface.
|
|
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.
|
|
16
14
|
*
|
|
17
|
-
* The valve is the TTL witness
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* utterance carries the transcript marking `findUserMessages()` admits.
|
|
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.
|
|
22
19
|
*
|
|
23
20
|
* fail-closed: ANY failure — an unbuilt judge body, an unreadable stdin, a missing or
|
|
24
21
|
* invalid config file — resolves to `{ exitCode: 2 }` with one `blocked` record under the
|
|
@@ -26,7 +23,11 @@
|
|
|
26
23
|
* the cheapest bypass vector there is. Recovery from an unbuilt clone is `pnpm build` (it
|
|
27
24
|
* mentions no protected path, so it is never blocked).
|
|
28
25
|
*/
|
|
29
|
-
|
|
26
|
+
import { type CanonicalTranscript } from '@polydeukes/core';
|
|
27
|
+
import { type CovenantRegistration } from '@polydeukes/covenant';
|
|
28
|
+
import { type CovenantModule } from './covenant-module.js';
|
|
29
|
+
import { loadConfig } from './load-config.js';
|
|
30
|
+
/** `runClaudeCodeHook` input — the `CovenantCheckSpec` shape, session side. */
|
|
30
31
|
export type ClaudeCodeHookSpec = {
|
|
31
32
|
/** Repository root — config discovery and discipline glob scoping both anchor here. */
|
|
32
33
|
repoRoot: string;
|
|
@@ -37,10 +38,39 @@ export type ClaudeCodeHookSpec = {
|
|
|
37
38
|
/** Overrides the resolved covenant dist directory (tests and assembly injection). */
|
|
38
39
|
covenantDist?: string;
|
|
39
40
|
};
|
|
41
|
+
/** {@link assembleSessionRegistrations} input — what the session surface's assembly needs. */
|
|
42
|
+
export type SessionAssemblySpec = {
|
|
43
|
+
config: ReturnType<typeof loadConfig>['config'];
|
|
44
|
+
rootDir: string;
|
|
45
|
+
/**
|
|
46
|
+
* The covenant surface the registrations are built from — the module the caller loaded
|
|
47
|
+
* from the resolved dist, so what judges a call is what that dist carries, and what
|
|
48
|
+
* `explain` renders is what would judge it.
|
|
49
|
+
*/
|
|
50
|
+
covenant: CovenantModule;
|
|
51
|
+
/** The payload's transcript path. ABSENT leaves the transcript-mod registration out. */
|
|
52
|
+
transcriptPath?: string;
|
|
53
|
+
transcript?: CanonicalTranscript;
|
|
54
|
+
witness?: CovenantRegistration['witness'];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The session surface's registration set. One assembly, two consumers: the runner below
|
|
58
|
+
* dispatches it, `explain` renders it — so what a reader is shown is the table the judgment
|
|
59
|
+
* actually uses, never a second opinion about it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function assembleSessionRegistrations(spec: SessionAssemblySpec): CovenantRegistration[];
|
|
40
62
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
63
|
+
* The session-surface entry point: the post-hoc state comparison wrapped around the judgment.
|
|
64
|
+
*
|
|
65
|
+
* The comparison sits OUTSIDE {@link judgeHookCall}'s fail-closed try on both ends. Inside
|
|
66
|
+
* it, a comparison failure would become a blocked call — the opposite of a mechanism whose
|
|
67
|
+
* whole purpose is to record rather than stop — so each side carries its own catch and
|
|
68
|
+
* neither can reach the verdict. Observation is fail-open, the direction
|
|
69
|
+
* `appendRecordFailOpen` already established: the worst outcome is a missing datum.
|
|
70
|
+
*
|
|
71
|
+
* Order is the contract. The comparison runs first, so it reads the window the previous call
|
|
72
|
+
* left and its rows land ahead of this call's judgment; the re-establishment runs last, so
|
|
73
|
+
* this call's own judged writes are folded in rather than alarmed on next time.
|
|
44
74
|
*/
|
|
45
75
|
export declare function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<{
|
|
46
76
|
exitCode: 0 | 2;
|