polydeukes 0.0.1 → 0.4.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/LICENSE +21 -0
- package/README.ko.md +73 -0
- package/README.md +67 -12
- package/dist/bin.d.ts +15 -0
- package/dist/bin.js +138 -0
- package/dist/claude-code-hook.d.ts +56 -0
- package/dist/claude-code-hook.js +360 -0
- package/dist/covenant-check.d.ts +58 -0
- package/dist/covenant-check.js +262 -0
- package/dist/docs/configuration.md +95 -0
- package/dist/docs/installation.md +211 -0
- package/dist/docs/reference/adapter-claude-code.md +82 -0
- package/dist/docs/reference/adapter-git.md +87 -0
- package/dist/docs/reference/configuration.md +294 -0
- package/dist/docs/reference/core.md +111 -0
- package/dist/docs/reference/covenant.md +108 -0
- package/dist/docs/reference/polydeukes.md +215 -0
- package/dist/docs/troubleshooting.md +160 -0
- package/dist/docs-query.d.ts +46 -0
- package/dist/docs-query.js +138 -0
- package/dist/index.d.ts +23 -4
- package/dist/index.js +22 -4
- package/dist/init-claude-code.d.ts +39 -0
- package/dist/init-claude-code.js +255 -0
- package/dist/load-config.d.ts +43 -0
- package/dist/load-config.js +90 -0
- package/dist/scaffold-project.d.ts +28 -0
- package/dist/scaffold-project.js +137 -0
- package/dist/schema/polydeukes.schema.json +203 -0
- package/package.json +31 -11
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# `polydeukes` — the umbrella
|
|
2
|
+
|
|
3
|
+
**English** · [한국어](./polydeukes.ko.md)
|
|
4
|
+
|
|
5
|
+
> Alpha. Everything below is read from the shipped package — the `pdks` bin, the barrel, and
|
|
6
|
+
> the exports map. For the procedures that use these, see [installation](../installation.md)
|
|
7
|
+
> and [troubleshooting](../troubleshooting.md).
|
|
8
|
+
|
|
9
|
+
The umbrella is the only package you install. It carries the core, the judge, and both
|
|
10
|
+
adapters as its own dependencies, and it is the only package allowed to assemble them —
|
|
11
|
+
every other dependency in this repository runs one way, through the core alone. That makes
|
|
12
|
+
this page the consumer-facing surface: the four scoped packages are transitive dependencies
|
|
13
|
+
you do not install and do not import.
|
|
14
|
+
|
|
15
|
+
| Package | Reference | Owns |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `@polydeukes/core` | [core](./core.md) | The protocol, the config schema, telemetry |
|
|
18
|
+
| `@polydeukes/covenant` | [covenant](./covenant.md) | The judge — dispatcher, disciplines, meta-covenants, the valve |
|
|
19
|
+
| `@polydeukes/adapter-claude-code` | [adapter-claude-code](./adapter-claude-code.md) | Session surface — PreToolUse payloads → input IR |
|
|
20
|
+
| `@polydeukes/adapter-git` | [adapter-git](./adapter-git.md) | Commit surface — staged diffs → input IR |
|
|
21
|
+
|
|
22
|
+
## Subcommands
|
|
23
|
+
|
|
24
|
+
The bin is `pdks`, with `polydeukes` as an alias. There are no flags and no options
|
|
25
|
+
anywhere: two subcommands take an exact two-word form, and `docs` takes an optional topic.
|
|
26
|
+
|
|
27
|
+
### `pdks covenant check`
|
|
28
|
+
|
|
29
|
+
The commit-surface judgment runner, invoked from a pre-commit hook. It discovers the config
|
|
30
|
+
at the working directory, collects the staging area through the git adapter, translates it
|
|
31
|
+
into the covenant input IR, and dispatches it through the same judge bodies the session hook
|
|
32
|
+
spawns.
|
|
33
|
+
|
|
34
|
+
| Situation | Result |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Staged changes break nothing | exit `0` |
|
|
37
|
+
| A staged change breaks a covenant, `enforce: block`, config has a `witness` block | Prompts once on `/dev/tty` for the witness token; an unanswered or wrong answer exits `2` |
|
|
38
|
+
| The same, with no `witness` block in the config | exit `2` with no prompt — the valve is built from that block, so without it nothing can open a block |
|
|
39
|
+
| A staged change breaks a covenant, `enforce: advise` | One advisory line on stderr, exit `0`, recorded `advised` |
|
|
40
|
+
| Empty staging area | exit `0` — an explicit pass, not a skipped run |
|
|
41
|
+
| No config, more than one config, or an invalid one | exit `2` |
|
|
42
|
+
| A judge body that cannot be loaded | exit `2` |
|
|
43
|
+
|
|
44
|
+
Context-family disciplines (`requirePrecedent`) assemble here like any other entry, but this
|
|
45
|
+
surface has no session to read: a match records `skipped` and the commit proceeds. That is a
|
|
46
|
+
permanent condition of the commit surface, not a fault.
|
|
47
|
+
|
|
48
|
+
### `pdks init claude-code`
|
|
49
|
+
|
|
50
|
+
The session-surface installer. It proves that `polydeukes` resolves from the directory it
|
|
51
|
+
was invoked in **before writing anything**, then creates five artifacts:
|
|
52
|
+
|
|
53
|
+
| Artifact | Kind |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `.claude/hooks/covenant-pretooluse.mjs` | Created — a delegator that loads the judge from the installed package |
|
|
56
|
+
| `.claude/settings.json` | Merged — the PreToolUse registration is added to whatever the file already carries |
|
|
57
|
+
| `polydeukes.config.yaml` | Created — the starter policy, with a placeholder `languages` block |
|
|
58
|
+
| `.claude/rules/polydeukes.md` | Created — tells the AI partner to ask [`pdks docs`](#pdks-docs-topic) instead of searching the web |
|
|
59
|
+
| `.gitignore` | Appended — one line for `.polydeukes/` |
|
|
60
|
+
|
|
61
|
+
Nothing existing is overwritten: an artifact already present is reported as skipped and left
|
|
62
|
+
alone, so a re-run is a no-op. A precondition failure — the package not resolving, two
|
|
63
|
+
coexisting config spellings, an unparseable settings file — writes zero files and exits `2`,
|
|
64
|
+
never a half-wired tree.
|
|
65
|
+
|
|
66
|
+
### `pdks docs [topic]`
|
|
67
|
+
|
|
68
|
+
The offline documentation reader. The guides and this reference layer ship inside the
|
|
69
|
+
package, so the answer comes from the installed version rather than from the network.
|
|
70
|
+
|
|
71
|
+
| Call | Result |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `pdks docs` | The topic list on stdout, exit `0` |
|
|
74
|
+
| `pdks docs <topic>` | That topic's section, followed by a `See also:` line, exit `0` |
|
|
75
|
+
| `pdks docs <unknown>` | The known topics named on stderr, exit `2` |
|
|
76
|
+
| `pdks docs a b` | The usage line on stderr, exit `2` |
|
|
77
|
+
|
|
78
|
+
| Topic | Answers from |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `install` | [installation](../installation.md), in full |
|
|
81
|
+
| `config` | [the configuration reference](./configuration.md), in full |
|
|
82
|
+
| `discipline` | [the configuration reference](./configuration.md) — the `disciplines` section |
|
|
83
|
+
| `covenant` | [configuration](../configuration.md) — What enforcement looks like |
|
|
84
|
+
| `witness` | [the configuration reference](./configuration.md)'s `witness` section, then [troubleshooting](../troubleshooting.md)'s valve section |
|
|
85
|
+
|
|
86
|
+
**Every failure leaves stdout at zero bytes.** A missing bundled document, a heading the
|
|
87
|
+
document no longer carries, an unknown topic — each names what was missing on stderr and
|
|
88
|
+
exits `2`. A partially written answer is one an agent reads as the document and quotes
|
|
89
|
+
onward, so there is no such state.
|
|
90
|
+
|
|
91
|
+
The bundle carries the English text only. Answers are returned verbatim, so the
|
|
92
|
+
`[한국어](./X.ko.md)` link at the top of each document points at a mirror that lives in the
|
|
93
|
+
[repository](https://github.com/huskyhoochu/polydeukes/tree/main/docs) rather than inside
|
|
94
|
+
the package.
|
|
95
|
+
|
|
96
|
+
`pdks init claude-code` writes a discovery file that points an AI partner at this
|
|
97
|
+
subcommand; see the artifact table above.
|
|
98
|
+
|
|
99
|
+
### Any other argument form
|
|
100
|
+
|
|
101
|
+
Anything that is not one of these forms writes
|
|
102
|
+
`usage: pdks covenant check | pdks init claude-code | pdks docs [topic]` to stderr and
|
|
103
|
+
exits `2`.
|
|
104
|
+
|
|
105
|
+
## Exit codes
|
|
106
|
+
|
|
107
|
+
Three codes exist, and they live at two layers. **What a consumer's hook observes is only
|
|
108
|
+
`0` or `2`** — both composition roots return `Promise<{ exitCode: 0 | 2 }>`.
|
|
109
|
+
|
|
110
|
+
| Code | Constant | Emitted by | Means |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `0` | `EXIT_UPHOLD` | Judge body, wrapper, bin | The promise was upheld — the call or commit proceeds |
|
|
113
|
+
| `1` | `EXIT_BREAK_NON_BLOCKING` | Judge body only | A break reported as a signal. The wrapper translates it — into `2` under `enforce: block`, into `0` + an `advised` row under `advise`. It never reaches the surface either way |
|
|
114
|
+
| `2` | `EXIT_BREAK_BLOCKING` | Wrapper, bin, fail-closed paths | The call or commit is refused |
|
|
115
|
+
|
|
116
|
+
The asymmetry is the protocol's responsibility boundary. A covenant body decides *whether* a
|
|
117
|
+
promise was broken and says so with `0` or `1`; deciding what a break *costs* belongs to the
|
|
118
|
+
wrapper, and that is the one place `enforce` is read. A body can therefore be run, tested, and
|
|
119
|
+
reasoned about without knowing whether the surface it runs under blocks or advises. Only the
|
|
120
|
+
verdict relaxes: every unjudgeable outcome — a body exit of `2` or higher, a signal death —
|
|
121
|
+
stays `2` at either level.
|
|
122
|
+
|
|
123
|
+
**Everything unjudgeable resolves to `2`.** A missing config, an invalid one, an
|
|
124
|
+
unparseable payload, a judge body that was never built — each fails closed. The one
|
|
125
|
+
direction that stays open is measurement: a telemetry write that fails never changes a
|
|
126
|
+
verdict.
|
|
127
|
+
|
|
128
|
+
## Programmatic surface
|
|
129
|
+
|
|
130
|
+
The barrel (`import … from 'polydeukes'`) exports six symbols plus one re-exported type.
|
|
131
|
+
This is the whole public API; the scoped packages are not part of it.
|
|
132
|
+
|
|
133
|
+
### `loadConfig`
|
|
134
|
+
|
|
135
|
+
**Type signature:**
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
function loadConfig(rootDir: string): LoadedConfig;
|
|
139
|
+
|
|
140
|
+
type LoadedConfig = {
|
|
141
|
+
config: ResolvedConfig; // protectedPaths already includes the config file itself
|
|
142
|
+
configPath: string; // rootDir-relative path of the discovered file
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Discovers exactly one `polydeukes.config` file (`.yaml`, `.yml`, or `.json`) directly under
|
|
147
|
+
`rootDir`. **Every failure branch throws** — none found, more than one found, a parse error,
|
|
148
|
+
a schema violation. There are no silent defaults, because a silently defaulted config means
|
|
149
|
+
a silently unprotected project.
|
|
150
|
+
|
|
151
|
+
### `runCovenantCheck`
|
|
152
|
+
|
|
153
|
+
**Type signature:**
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
function runCovenantCheck(spec: CovenantCheckSpec): Promise<{ exitCode: 0 | 2 }>;
|
|
157
|
+
|
|
158
|
+
type CovenantCheckSpec = {
|
|
159
|
+
repoRoot: string; // config discovery and staged collection anchor here
|
|
160
|
+
telemetryPath?: string; // overrides the config's log path
|
|
161
|
+
covenantDist?: string; // overrides the resolved judge directory
|
|
162
|
+
ttyPrompt?: (prompt: string) => string | null; // the TTY valve seam
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The commit surface's composition root — what [`pdks covenant
|
|
167
|
+
check`](#pdks-covenant-check) runs.
|
|
168
|
+
|
|
169
|
+
`ttyPrompt` absent means a non-TTY environment, and the valve then has no way to open — an
|
|
170
|
+
agent-spawned commit and a CI run reach the same state. The valve is a human at a terminal
|
|
171
|
+
or nothing.
|
|
172
|
+
|
|
173
|
+
### `runClaudeCodeHook`
|
|
174
|
+
|
|
175
|
+
**Type signature:**
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<{ exitCode: 0 | 2 }>;
|
|
179
|
+
|
|
180
|
+
type ClaudeCodeHookSpec = {
|
|
181
|
+
repoRoot: string; // config discovery and discipline glob scoping anchor here
|
|
182
|
+
rawPayload?: string; // absent means read fd 0 — the hook's real stdin
|
|
183
|
+
telemetryPath?: string;
|
|
184
|
+
covenantDist?: string;
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The session surface's composition root — what the generated hook delegator calls. Reach it
|
|
189
|
+
through the [`polydeukes/claude-code`](#subpaths) subpath rather than the barrel.
|
|
190
|
+
|
|
191
|
+
**Neither composition root throws.** An uncaught rejection would exit the delegator
|
|
192
|
+
non-blocking, which is the cheapest bypass there is, so both resolve their failures into
|
|
193
|
+
`{ exitCode: 2 }` with a telemetry record instead.
|
|
194
|
+
|
|
195
|
+
### `ResolvedConfig`
|
|
196
|
+
|
|
197
|
+
Re-exported from [`@polydeukes/core`](./core.md) so a consumer reading `loadConfig`'s result
|
|
198
|
+
needs no second dependency.
|
|
199
|
+
|
|
200
|
+
## Subpaths
|
|
201
|
+
|
|
202
|
+
| Specifier | Carries |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `polydeukes` | The barrel — `loadConfig`, `runCovenantCheck`, `runClaudeCodeHook`, their spec types, `ResolvedConfig` |
|
|
205
|
+
| `polydeukes/claude-code` | `runClaudeCodeHook` and `ClaudeCodeHookSpec` alone |
|
|
206
|
+
| `polydeukes/schema.json` | The config JSON Schema, copied from the core at build time |
|
|
207
|
+
|
|
208
|
+
The generated hook delegator imports the subpath, not the barrel. ESM imports are eager, so
|
|
209
|
+
importing the barrel would load the commit-surface runner — and the git adapter behind it —
|
|
210
|
+
on every session tool call that will never use them. The subpath is the session surface's
|
|
211
|
+
own entry point, and the barrel is for programmatic consumers.
|
|
212
|
+
|
|
213
|
+
`polydeukes/schema.json` is for code that reads the schema. A `$schema` line names the file
|
|
214
|
+
path instead — an editor reads that string statically, so no module resolver runs on it. Both
|
|
215
|
+
spellings are in [configuration.md's IDE section](../configuration.md#ide-support).
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Troubleshooting Polydeukes
|
|
2
|
+
|
|
3
|
+
**English** · [한국어](./troubleshooting.ko.md)
|
|
4
|
+
|
|
5
|
+
> Alpha. Nine states cover what ships today — five ways a fail-closed system refuses to
|
|
6
|
+
> proceed, and four things worth knowing when a judgment surprises you. Each entry is
|
|
7
|
+
> symptom → cause → recovery.
|
|
8
|
+
|
|
9
|
+
This is the guide layer for recovery: the fail-closed states, reading verdicts, and the
|
|
10
|
+
witness valve.
|
|
11
|
+
|
|
12
|
+
The one principle behind half of this page: **a gate that cannot judge blocks rather than
|
|
13
|
+
guesses.** A missing config, an ambiguous config, an invalid config, an installer that
|
|
14
|
+
cannot prove resolution, and a judge that cannot be loaded all fail closed, because a dead
|
|
15
|
+
gate that waves things through is the cheapest bypass of all. The recovery is never to
|
|
16
|
+
disable the gate — it is to give it back what it needs to judge. And run that recovery
|
|
17
|
+
**from your own terminal**: inside a session the repair commands are judged by the very
|
|
18
|
+
gate they repair, and while no config is loaded there is no witness valve to open.
|
|
19
|
+
|
|
20
|
+
## Every call is blocked and there is no config
|
|
21
|
+
|
|
22
|
+
**Symptom.** On the session surface, every editing tool call and shell command exits 2;
|
|
23
|
+
on the commit surface, every `pdks covenant check` run does. The error says no Polydeukes
|
|
24
|
+
config was found and names the three candidate filenames.
|
|
25
|
+
|
|
26
|
+
**Cause.** The surface is wired but config discovery found nothing at the project root.
|
|
27
|
+
Discovery looks for exactly these, in this order: `polydeukes.config.yaml`,
|
|
28
|
+
`polydeukes.config.yml`, `polydeukes.config.json`. A missing config never silently loads
|
|
29
|
+
defaults — silent defaults would mean silently unprotected.
|
|
30
|
+
|
|
31
|
+
**Recovery.** Restore the file from git. On the session path,
|
|
32
|
+
`pnpm exec pdks init claude-code` recreates the missing artifacts; on the commit path the
|
|
33
|
+
config is hand-written — the [install guide](./installation.md)'s commit-surface section
|
|
34
|
+
has a starting point.
|
|
35
|
+
|
|
36
|
+
## More than one config file
|
|
37
|
+
|
|
38
|
+
**Symptom.** Every call exits 2 with an error naming two (or three) config files at once.
|
|
39
|
+
|
|
40
|
+
**Cause.** Two spellings coexist — say a `polydeukes.config.yaml` created next to a
|
|
41
|
+
project's existing `.yml`. Ambiguity never picks a winner.
|
|
42
|
+
|
|
43
|
+
**Recovery.** Keep exactly one file and delete the others. If both have content, merge by
|
|
44
|
+
hand first — the loader will not choose for you.
|
|
45
|
+
|
|
46
|
+
## The config is invalid
|
|
47
|
+
|
|
48
|
+
**Symptom.** Every call exits 2 with an error naming the offending file — and, for schema
|
|
49
|
+
violations, the exact key.
|
|
50
|
+
|
|
51
|
+
**Cause.** One of: a YAML parse error; a custom YAML tag (rejected even though the parser
|
|
52
|
+
cannot execute it — config data stays uncomputable by contract); an unknown key (a typo
|
|
53
|
+
like `protectedPath:` is rejected with the full field path — with one open ground: an
|
|
54
|
+
adapter namespace's *name* is not validated, so `adapters.gti:` for `adapters.git:` loads
|
|
55
|
+
clean and its entries are simply never read; that one spelling you check yourself); or an
|
|
56
|
+
empty `languages` block, the schema's one required entry.
|
|
57
|
+
|
|
58
|
+
**Recovery.** Fix the named key in the named file. The error is specific on purpose — no
|
|
59
|
+
rewrite-and-hope needed.
|
|
60
|
+
|
|
61
|
+
## `pdks init claude-code` refuses to run
|
|
62
|
+
|
|
63
|
+
**Symptom.** The installer prints an install command and exits 2 without creating anything.
|
|
64
|
+
|
|
65
|
+
**Cause.** Preflight: before writing any file, the installer proves the `polydeukes`
|
|
66
|
+
package resolves from the target project root. A hook generated without that would block
|
|
67
|
+
every call through its own fail-closed catch — an uneditable tree. The usual trigger is
|
|
68
|
+
running via a one-off `npx` without installing, or running in the wrong directory (it
|
|
69
|
+
installs where it is invoked).
|
|
70
|
+
|
|
71
|
+
**Recovery.** `pnpm add -D polydeukes` in the project you meant, then re-run from that
|
|
72
|
+
root. Zero files were written, so there is no partial state to clean up.
|
|
73
|
+
|
|
74
|
+
## The judge cannot be loaded
|
|
75
|
+
|
|
76
|
+
**Symptom.** Every call exits 2 with `covenant hook failed closed: Cannot find package
|
|
77
|
+
'polydeukes'` — or an error naming a judge-body file that does not exist.
|
|
78
|
+
|
|
79
|
+
**Cause.** The hook is wired but the package it delegates to is gone or incomplete: the
|
|
80
|
+
dependency was removed, the tree is a fresh clone that was never installed, or (in a
|
|
81
|
+
source clone of this repository) the judge's build output is missing. The installer's
|
|
82
|
+
preflight prevents *wiring* a project into this state, but nothing prevents a wired
|
|
83
|
+
project from entering it later.
|
|
84
|
+
|
|
85
|
+
**Recovery.** From your own terminal, reinstall the dependency (`pnpm install`, or
|
|
86
|
+
`pnpm add -D polydeukes` if it was removed). In a source clone, run the build. The hook
|
|
87
|
+
file itself needs no repair — it is a delegator, and it recovers the moment the package
|
|
88
|
+
resolves again.
|
|
89
|
+
|
|
90
|
+
## Reading a verdict
|
|
91
|
+
|
|
92
|
+
**Symptom.** Something was blocked (or passed) and you want to know what the record says.
|
|
93
|
+
|
|
94
|
+
**Cause.** Not a failure — this is the measurement working. Every judgment appends exactly
|
|
95
|
+
one record to the telemetry log (`.polydeukes/roi.log` by default, `telemetry.logPath` to
|
|
96
|
+
move it).
|
|
97
|
+
|
|
98
|
+
**Recovery.** Read the last lines and the six-word vocabulary:
|
|
99
|
+
|
|
100
|
+
| Word | Means |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `passed` | Judged, upheld the covenant. |
|
|
103
|
+
| `blocked` | Judged, broke it. The call did not run. |
|
|
104
|
+
| `witnessed` | A blocked verdict a human opened in person. Never silent. |
|
|
105
|
+
| `advised` | Commit surface at `advise` level: a break recorded without stopping the commit. |
|
|
106
|
+
| `skipped` | A registration matched but could not judge — **the recorded absence of a judgment, not a pass.** |
|
|
107
|
+
| `unattributed` | A protected entry changed on disk and no judgment explains it — **an observation, not a verdict.** Nothing was blocked; the write already happened. |
|
|
108
|
+
|
|
109
|
+
An `unattributed` row names the entry, not the file inside it. Rebuilding a protected `dist`
|
|
110
|
+
without a judged call producing one is expected — it says a write reached that entry outside
|
|
111
|
+
the session's view, which is exactly what the row is for.
|
|
112
|
+
|
|
113
|
+
## Opening a blocked call — the witness
|
|
114
|
+
|
|
115
|
+
**Symptom.** A call you and your agent agree should proceed was blocked, and you want it
|
|
116
|
+
through without editing the policy.
|
|
117
|
+
|
|
118
|
+
**Cause.** The valve exists for exactly this, and it sits *after* the verdict — only a
|
|
119
|
+
judgment that actually blocked can be witnessed open.
|
|
120
|
+
|
|
121
|
+
**Recovery.** Type the token from your config's `witness:` block so it stands **alone on
|
|
122
|
+
the first line** of a conversation message. The window holds for `ttlMinutes`, then
|
|
123
|
+
blocking resumes on its own. Three things that do not work, by design: quoting or
|
|
124
|
+
mentioning the token mid-sentence (invocation is first-line-standalone only); witnessing a
|
|
125
|
+
call that was never blocked (the valve is consulted only after a block); and the agent
|
|
126
|
+
typing the token for itself (only human-authored messages count — the defence is
|
|
127
|
+
provenance, not secrecy). Every allowance lands as one `witnessed` row.
|
|
128
|
+
|
|
129
|
+
## A blocked commit
|
|
130
|
+
|
|
131
|
+
**Symptom.** `git commit` stops at a prompt asking a human to witness a staged protected
|
|
132
|
+
change — or, from an agent, the commit simply fails with exit 2.
|
|
133
|
+
|
|
134
|
+
**Cause.** The commit surface at the default `block` level judges the staged diff, and its
|
|
135
|
+
valve is a TTY prompt. An agent-spawned commit has no TTY, so for it the valve is not even
|
|
136
|
+
assembled — a terminal-holding human is the pass condition, not a workaround.
|
|
137
|
+
|
|
138
|
+
**Recovery.** Run the commit from your own terminal and answer the prompt with the full
|
|
139
|
+
token — one answer covers that whole commit. If you want the commit surface to measure
|
|
140
|
+
without stopping, set `adapters.git.enforce: advise`: verdicts are then recorded as
|
|
141
|
+
`advised` and the commit proceeds with one advisory line on stderr. At either level a run
|
|
142
|
+
that *cannot judge* (missing or invalid config, an unresolvable judge) still exits 2 —
|
|
143
|
+
`advise` relaxes the verdict, never the gate's integrity.
|
|
144
|
+
|
|
145
|
+
## `skipped` rows on the commit surface
|
|
146
|
+
|
|
147
|
+
**Symptom.** A `requirePrecedent` discipline that judges normally in sessions always lands
|
|
148
|
+
as `skipped` on commits.
|
|
149
|
+
|
|
150
|
+
**Cause.** Context-family disciplines judge *session history* — was the required step
|
|
151
|
+
actually executed before this change. A commit has no session to read, so the entry
|
|
152
|
+
assembles as a skip registration: routing intact, no judge body. When its scope matches a
|
|
153
|
+
staged change it records `skipped` with the entry's id and proceeds.
|
|
154
|
+
|
|
155
|
+
**Recovery.** None needed — this is a declared condition of the surface, not a defect. The
|
|
156
|
+
row is the point: a gate that did nothing says so in the data. A context-family entry is
|
|
157
|
+
really a session-surface tool — on a project that wires only the commit surface, such an
|
|
158
|
+
entry only ever buys telemetry, so declare it where an AI partner's session exists to be
|
|
159
|
+
judged. The row appears only when the entry's scope actually matched, so an unrelated
|
|
160
|
+
commit records nothing.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryDocs` — the offline documentation query (DOCS-02 §3-b/§3-c/§3-d).
|
|
3
|
+
*
|
|
4
|
+
* The bundled English guides, answered from the installed version. An AI partner that
|
|
5
|
+
* searches the web gets whatever release the internet indexed; this returns the document
|
|
6
|
+
* that shipped with the code doing the judging, with no network at all.
|
|
7
|
+
*
|
|
8
|
+
* The domain is the five topics below and nothing else (§3-c). An unknown topic throws
|
|
9
|
+
* instead of resolving to something near it: an answer to a question we never mapped is
|
|
10
|
+
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
|
+
*
|
|
12
|
+
* Every failure throws (§3-b) so the bin can leave stdout at zero bytes and exit 2. Text
|
|
13
|
+
* written halfway is read as the document and quoted as the document — the same direction
|
|
14
|
+
* the judging surface fails in, for the same reason.
|
|
15
|
+
*/
|
|
16
|
+
/** The finite query domain (§3-c) — the topic list `pdks docs` prints with no argument. */
|
|
17
|
+
export declare const TOPICS: readonly ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
18
|
+
/**
|
|
19
|
+
* The body of one section of `markdown`: from the line equal to `heading` up to just before
|
|
20
|
+
* the next heading of the same or a higher level, returned verbatim (§3-d).
|
|
21
|
+
*
|
|
22
|
+
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
23
|
+
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
24
|
+
* with full confidence.
|
|
25
|
+
*
|
|
26
|
+
* Both scans — for the start and for the boundary — run outside code fences. `#` lines
|
|
27
|
+
* inside a fence are content: the guides really carry them, and a fence-blind scanner cuts
|
|
28
|
+
* the answer at one of those lines while still looking like a success.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractSection(markdown: string, heading: string): string;
|
|
31
|
+
/** `queryDocs` input — the bundle to read from, and which topic to answer. */
|
|
32
|
+
export type QueryDocsSpec = {
|
|
33
|
+
/** Root of the bundled documents (`dist/docs`), the only tree read here. */
|
|
34
|
+
docsRoot: string;
|
|
35
|
+
/** ABSENT lists the topics; anything not in {@link TOPICS} throws, empty string included. */
|
|
36
|
+
topic?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Answer one documentation query (§3-b).
|
|
40
|
+
*
|
|
41
|
+
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
42
|
+
* With one, it is the §3-c section body followed by the bundled reference to read next.
|
|
43
|
+
*/
|
|
44
|
+
export declare function queryDocs(spec: QueryDocsSpec): {
|
|
45
|
+
text: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `queryDocs` — the offline documentation query (DOCS-02 §3-b/§3-c/§3-d).
|
|
3
|
+
*
|
|
4
|
+
* The bundled English guides, answered from the installed version. An AI partner that
|
|
5
|
+
* searches the web gets whatever release the internet indexed; this returns the document
|
|
6
|
+
* that shipped with the code doing the judging, with no network at all.
|
|
7
|
+
*
|
|
8
|
+
* The domain is the five topics below and nothing else (§3-c). An unknown topic throws
|
|
9
|
+
* instead of resolving to something near it: an answer to a question we never mapped is
|
|
10
|
+
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
|
+
*
|
|
12
|
+
* Every failure throws (§3-b) so the bin can leave stdout at zero bytes and exit 2. Text
|
|
13
|
+
* written halfway is read as the document and quoted as the document — the same direction
|
|
14
|
+
* the judging surface fails in, for the same reason.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/** The finite query domain (§3-c) — the topic list `pdks docs` prints with no argument. */
|
|
19
|
+
export const TOPICS = ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
20
|
+
/** The §3-c mapping, as data: which document answers a topic, and what to read next. */
|
|
21
|
+
const TOPIC_MAP = {
|
|
22
|
+
install: {
|
|
23
|
+
sections: [{ file: 'installation.md' }],
|
|
24
|
+
seeAlso: 'reference/polydeukes.md',
|
|
25
|
+
},
|
|
26
|
+
config: {
|
|
27
|
+
sections: [{ file: 'reference/configuration.md' }],
|
|
28
|
+
seeAlso: 'reference/core.md',
|
|
29
|
+
},
|
|
30
|
+
discipline: {
|
|
31
|
+
sections: [{ file: 'reference/configuration.md', heading: '## `disciplines`' }],
|
|
32
|
+
seeAlso: 'reference/covenant.md',
|
|
33
|
+
},
|
|
34
|
+
covenant: {
|
|
35
|
+
sections: [{ file: 'configuration.md', heading: '## What enforcement looks like' }],
|
|
36
|
+
seeAlso: 'reference/polydeukes.md',
|
|
37
|
+
},
|
|
38
|
+
witness: {
|
|
39
|
+
sections: [
|
|
40
|
+
{ file: 'reference/configuration.md', heading: '## `witness`' },
|
|
41
|
+
{ file: 'troubleshooting.md', heading: '## Opening a blocked call — the witness' },
|
|
42
|
+
],
|
|
43
|
+
seeAlso: 'reference/covenant.md',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/** A fenced block opens and closes on a line whose trimmed form starts with the marker. */
|
|
47
|
+
const FENCE = /^(?:`{3,}|~{3,})/;
|
|
48
|
+
/** An ATX heading, and its level in the capture. */
|
|
49
|
+
const HEADING = /^(#{1,6}) /;
|
|
50
|
+
function isTopic(value) {
|
|
51
|
+
return TOPICS.includes(value);
|
|
52
|
+
}
|
|
53
|
+
function headingLevel(line) {
|
|
54
|
+
return HEADING.exec(line)?.[1].length ?? 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The body of one section of `markdown`: from the line equal to `heading` up to just before
|
|
58
|
+
* the next heading of the same or a higher level, returned verbatim (§3-d).
|
|
59
|
+
*
|
|
60
|
+
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
61
|
+
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
62
|
+
* with full confidence.
|
|
63
|
+
*
|
|
64
|
+
* Both scans — for the start and for the boundary — run outside code fences. `#` lines
|
|
65
|
+
* inside a fence are content: the guides really carry them, and a fence-blind scanner cuts
|
|
66
|
+
* the answer at one of those lines while still looking like a success.
|
|
67
|
+
*/
|
|
68
|
+
export function extractSection(markdown, heading) {
|
|
69
|
+
const lines = markdown.split('\n');
|
|
70
|
+
const level = headingLevel(heading);
|
|
71
|
+
let openMarker;
|
|
72
|
+
let start = -1;
|
|
73
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (FENCE.test(trimmed)) {
|
|
77
|
+
// Opener and closer are both compared trimmed. An indented fence closed by a strict
|
|
78
|
+
// bare-marker test would stay open to end of file, and every heading after it would
|
|
79
|
+
// silently stop being a heading — the guides carry a two-space-indented one.
|
|
80
|
+
if (openMarker === undefined) {
|
|
81
|
+
openMarker = trimmed[0];
|
|
82
|
+
}
|
|
83
|
+
else if (trimmed[0] === openMarker) {
|
|
84
|
+
openMarker = undefined;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (openMarker !== undefined) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (start === -1) {
|
|
92
|
+
if (line === heading) {
|
|
93
|
+
start = i;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (headingLevel(line) > 0 && headingLevel(line) <= level) {
|
|
98
|
+
return lines.slice(start, i).join('\n');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (start === -1) {
|
|
102
|
+
throw new Error(`heading not found: ${heading}`);
|
|
103
|
+
}
|
|
104
|
+
// A section that closes the document ends at end of file; the §3-c map points at one.
|
|
105
|
+
return lines.slice(start).join('\n');
|
|
106
|
+
}
|
|
107
|
+
function readSection(docsRoot, section) {
|
|
108
|
+
const path = join(docsRoot, section.file);
|
|
109
|
+
if (!existsSync(path)) {
|
|
110
|
+
// Named, never swallowed into empty text: a silently incomplete bundle would otherwise
|
|
111
|
+
// reach a reader as the document itself.
|
|
112
|
+
throw new Error(`bundled document missing: ${section.file}`);
|
|
113
|
+
}
|
|
114
|
+
const markdown = readFileSync(path, 'utf-8');
|
|
115
|
+
return section.heading === undefined ? markdown : extractSection(markdown, section.heading);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Answer one documentation query (§3-b).
|
|
119
|
+
*
|
|
120
|
+
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
121
|
+
* With one, it is the §3-c section body followed by the bundled reference to read next.
|
|
122
|
+
*/
|
|
123
|
+
export function queryDocs(spec) {
|
|
124
|
+
if (spec.topic === undefined) {
|
|
125
|
+
return { text: `Polydeukes docs:\n${TOPICS.map((t) => ` pdks docs ${t}`).join('\n')}\n` };
|
|
126
|
+
}
|
|
127
|
+
if (!isTopic(spec.topic)) {
|
|
128
|
+
throw new Error(`unknown docs topic '${spec.topic}' — known topics: ${TOPICS.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
const entry = TOPIC_MAP[spec.topic];
|
|
131
|
+
const body = entry.sections.map((section) => readSection(spec.docsRoot, section)).join('\n');
|
|
132
|
+
// Resolved against the bundle, not printed as the bare relative name. A reader given
|
|
133
|
+
// `reference/core.md` has to guess where the bundle lives before it can open anything, and
|
|
134
|
+
// this line is the only way most of the reference layer is reached at all. A path a file-read
|
|
135
|
+
// tool can take is the difference between a pointer and a dead end, and a dead end sends the
|
|
136
|
+
// reader back to the web search this command replaces.
|
|
137
|
+
return { text: `${body}\nSee also: ${join(spec.docsRoot, entry.seeAlso)}\n` };
|
|
138
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Polydeukes — a
|
|
2
|
+
* Polydeukes — a development discipline framework for building alongside an AI
|
|
3
|
+
* coding partner.
|
|
3
4
|
*
|
|
4
|
-
* Pre-alpha. This package
|
|
5
|
-
*
|
|
5
|
+
* Pre-alpha. This package reserves the unscoped `polydeukes` name and is the umbrella /
|
|
6
|
+
* `pdks` CLI entry point. It owns the config discovery loader (CONFIG-03) and both
|
|
7
|
+
* surfaces' composition roots — `runCovenantCheck` for the commit surface and
|
|
8
|
+
* `runClaudeCodeHook` for the session one — because assembly needs an adapter and the
|
|
9
|
+
* covenant package at once, which no sibling is allowed to depend on. The covenant,
|
|
10
|
+
* ledger, memory, and verify modules live in their own `@polydeukes/*` packages.
|
|
11
|
+
*
|
|
12
|
+
* This file is a barrel and nothing more. ESM re-exports are eager, so anything defined
|
|
13
|
+
* here would be instantiated by every consumer of any other export — which is exactly how
|
|
14
|
+
* the session adapter ended up on the commit surface's load path (PR #46 review). Keep
|
|
15
|
+
* definitions in their own modules and let importers reach them directly.
|
|
16
|
+
*
|
|
17
|
+
* The mirror of that coupling is closed as of DIST-02. `exports` publishes `./claude-code`
|
|
18
|
+
* alongside `"."`, and both delegators — this repository's and the one `pdks init
|
|
19
|
+
* claude-code` generates — enter through it, so a session call no longer instantiates
|
|
20
|
+
* `covenant-check.js` or `@polydeukes/adapter-git`. The window DIST-01 §3-d declared (a
|
|
21
|
+
* workspace missing only that dist failing closed with no telemetry row) is gone with it.
|
|
6
22
|
* See https://github.com/huskyhoochu/polydeukes
|
|
7
23
|
*/
|
|
8
|
-
export
|
|
24
|
+
export type { ResolvedConfig } from '@polydeukes/core';
|
|
25
|
+
export { type ClaudeCodeHookSpec, runClaudeCodeHook } from './claude-code-hook.js';
|
|
26
|
+
export { type CovenantCheckSpec, runCovenantCheck } from './covenant-check.js';
|
|
27
|
+
export { type LoadedConfig, loadConfig } from './load-config.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Polydeukes — a
|
|
2
|
+
* Polydeukes — a development discipline framework for building alongside an AI
|
|
3
|
+
* coding partner.
|
|
3
4
|
*
|
|
4
|
-
* Pre-alpha. This package
|
|
5
|
-
*
|
|
5
|
+
* Pre-alpha. This package reserves the unscoped `polydeukes` name and is the umbrella /
|
|
6
|
+
* `pdks` CLI entry point. It owns the config discovery loader (CONFIG-03) and both
|
|
7
|
+
* surfaces' composition roots — `runCovenantCheck` for the commit surface and
|
|
8
|
+
* `runClaudeCodeHook` for the session one — because assembly needs an adapter and the
|
|
9
|
+
* covenant package at once, which no sibling is allowed to depend on. The covenant,
|
|
10
|
+
* ledger, memory, and verify modules live in their own `@polydeukes/*` packages.
|
|
11
|
+
*
|
|
12
|
+
* This file is a barrel and nothing more. ESM re-exports are eager, so anything defined
|
|
13
|
+
* here would be instantiated by every consumer of any other export — which is exactly how
|
|
14
|
+
* the session adapter ended up on the commit surface's load path (PR #46 review). Keep
|
|
15
|
+
* definitions in their own modules and let importers reach them directly.
|
|
16
|
+
*
|
|
17
|
+
* The mirror of that coupling is closed as of DIST-02. `exports` publishes `./claude-code`
|
|
18
|
+
* alongside `"."`, and both delegators — this repository's and the one `pdks init
|
|
19
|
+
* claude-code` generates — enter through it, so a session call no longer instantiates
|
|
20
|
+
* `covenant-check.js` or `@polydeukes/adapter-git`. The window DIST-01 §3-d declared (a
|
|
21
|
+
* workspace missing only that dist failing closed with no telemetry row) is gone with it.
|
|
6
22
|
* See https://github.com/huskyhoochu/polydeukes
|
|
7
23
|
*/
|
|
8
|
-
export
|
|
24
|
+
export { runClaudeCodeHook } from './claude-code-hook.js';
|
|
25
|
+
export { runCovenantCheck } from './covenant-check.js';
|
|
26
|
+
export { loadConfig } from './load-config.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `initClaudeCode` — the session-surface installer (DIST-02 §3-a/§3-b/§3-g).
|
|
3
|
+
*
|
|
4
|
+
* One command wires a project into the session surface: prove the package resolves, run the
|
|
5
|
+
* shared project-side scaffold ({@link scaffoldProject}), then add what this distribution
|
|
6
|
+
* path owns — the delegator hook file, its `.claude/settings.json` registration, and the
|
|
7
|
+
* discipline file that tells an agent the docs query exists (DOCS-02 §3-e).
|
|
8
|
+
*
|
|
9
|
+
* Preflight comes first and nothing is written before it clears (§5-d invariant 2). A
|
|
10
|
+
* generated hook whose import can never resolve blocks every call through its own
|
|
11
|
+
* fail-closed catch, and a tree that also has no config and no valve to open cannot be
|
|
12
|
+
* edited back into shape from inside the session — the brick §3-g exists to prevent.
|
|
13
|
+
*
|
|
14
|
+
* Nothing existing is overwritten (§5-d invariant 1). The settings file in particular is
|
|
15
|
+
* merged, never replaced: a consumer's other PreToolUse registrations and permissions are
|
|
16
|
+
* live configuration, and replacing them would disarm every other tool they wired.
|
|
17
|
+
*/
|
|
18
|
+
import { type ScaffoldReport } from './scaffold-project.js';
|
|
19
|
+
/** `initClaudeCode` input (DIST-02 §3-g) — the target tree and the preflight seam. */
|
|
20
|
+
export type InitClaudeCodeSpec = {
|
|
21
|
+
/** Project root to install into — every write below is relative to it. */
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
/**
|
|
24
|
+
* §3-g preflight seam: throws when the package cannot be resolved from the given root.
|
|
25
|
+
* ABSENT uses the real resolution, anchored at that root and nowhere else — anchoring it
|
|
26
|
+
* at the installer's own module would answer for the installer's install graph rather
|
|
27
|
+
* than the target project's, which is precisely the case that must fail.
|
|
28
|
+
*/
|
|
29
|
+
resolvePolydeukes?: (projectRoot: string) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Install the session surface into `spec.projectRoot` (DIST-02 §3-a), skipping whatever is
|
|
33
|
+
* already there and reporting both halves per artifact.
|
|
34
|
+
*
|
|
35
|
+
* Throws before any write when the package cannot be resolved from that root (§3-g) or when
|
|
36
|
+
* two config spellings already coexist there (§3-a third disposition) — both leave zero
|
|
37
|
+
* files. Translating a throw into exit 2 with the install command is the bin's job.
|
|
38
|
+
*/
|
|
39
|
+
export declare function initClaudeCode(spec: InitClaudeCodeSpec): ScaffoldReport;
|