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.
@@ -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 harness framework for developing alongside an AI coding partner.
2
+ * Polydeukes — a development discipline framework for building alongside an AI
3
+ * coding partner.
3
4
  *
4
- * Pre-alpha. This package currently reserves the name and exposes only version
5
- * metadata; the core, guard, ledger, kb, and verify modules are in design.
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 declare const version = "0.0.1";
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 harness framework for developing alongside an AI coding partner.
2
+ * Polydeukes — a development discipline framework for building alongside an AI
3
+ * coding partner.
3
4
  *
4
- * Pre-alpha. This package currently reserves the name and exposes only version
5
- * metadata; the core, guard, ledger, kb, and verify modules are in design.
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 const version = '0.0.1';
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;