polydeukes 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.ko.md +54 -74
  2. package/README.md +55 -86
  3. package/dist/baseline.d.ts +82 -0
  4. package/dist/baseline.js +166 -0
  5. package/dist/bin.d.ts +1 -1
  6. package/dist/bin.js +26 -6
  7. package/dist/claude-code-hook.d.ts +7 -5
  8. package/dist/claude-code-hook.js +69 -17
  9. package/dist/claude-code.d.ts +6 -0
  10. package/dist/claude-code.js +6 -0
  11. package/dist/covenant-check.d.ts +10 -17
  12. package/dist/covenant-check.js +44 -12
  13. package/dist/covenant-module.d.ts +2 -2
  14. package/dist/covenant-module.js +9 -1
  15. package/dist/docs/README.ko.md +60 -0
  16. package/dist/docs/README.md +64 -0
  17. package/dist/docs/catalog.json +464 -0
  18. package/dist/docs/concepts/judgment.ko.md +113 -0
  19. package/dist/docs/concepts/judgment.md +113 -0
  20. package/dist/docs/how-to/configure-project.ko.md +99 -0
  21. package/dist/docs/how-to/configure-project.md +95 -0
  22. package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
  23. package/dist/docs/how-to/connect-surfaces.md +118 -0
  24. package/dist/docs/how-to/write-disciplines.ko.md +124 -0
  25. package/dist/docs/how-to/write-disciplines.md +125 -0
  26. package/dist/docs/index.json +2046 -0
  27. package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
  28. package/dist/docs/reference/cli/covenant-check.md +98 -0
  29. package/dist/docs/reference/cli/docs.ko.md +97 -0
  30. package/dist/docs/reference/cli/docs.md +95 -0
  31. package/dist/docs/reference/cli/explain.ko.md +79 -0
  32. package/dist/docs/reference/cli/explain.md +84 -0
  33. package/dist/docs/reference/cli/init.ko.md +119 -0
  34. package/dist/docs/reference/cli/init.md +131 -0
  35. package/dist/docs/reference/configuration/index.ko.md +448 -0
  36. package/dist/docs/reference/configuration/index.md +474 -0
  37. package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
  38. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
  39. package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
  40. package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
  41. package/dist/docs/reference/packages/core.ko.md +128 -0
  42. package/dist/docs/reference/{core.md → packages/core.md} +32 -13
  43. package/dist/docs/reference/packages/covenant.ko.md +115 -0
  44. package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
  45. package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
  46. package/dist/docs/reference/packages/polydeukes.md +139 -0
  47. package/dist/docs/troubleshooting.ko.md +142 -0
  48. package/dist/docs/troubleshooting.md +97 -122
  49. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  50. package/dist/docs/tutorials/first-judgment.md +81 -0
  51. package/dist/docs-catalog.d.ts +25 -0
  52. package/dist/docs-catalog.js +450 -0
  53. package/dist/docs-library.d.ts +23 -0
  54. package/dist/docs-library.js +347 -0
  55. package/dist/docs-markdown.d.ts +32 -0
  56. package/dist/docs-markdown.js +150 -0
  57. package/dist/docs-query.d.ts +11 -40
  58. package/dist/docs-query.js +28 -122
  59. package/dist/docs-types.d.ts +105 -0
  60. package/dist/docs-types.js +2 -0
  61. package/dist/explain.d.ts +3 -5
  62. package/dist/explain.js +48 -47
  63. package/dist/index.d.ts +2 -3
  64. package/dist/index.js +1 -2
  65. package/dist/init-claude-code.d.ts +5 -3
  66. package/dist/init-claude-code.js +226 -63
  67. package/dist/init-grok.d.ts +51 -0
  68. package/dist/init-grok.js +242 -0
  69. package/dist/load-config.d.ts +5 -1
  70. package/dist/load-config.js +2 -1
  71. package/dist/pre-state-reader.d.ts +22 -0
  72. package/dist/pre-state-reader.js +32 -0
  73. package/dist/scaffold-project.js +48 -8
  74. package/dist/schema/polydeukes.schema.json +38 -91
  75. package/package.json +7 -7
  76. package/dist/docs/configuration.md +0 -103
  77. package/dist/docs/installation.md +0 -212
  78. package/dist/docs/reference/configuration.md +0 -338
  79. package/dist/docs/reference/polydeukes.md +0 -287
@@ -1,138 +1,44 @@
1
- /**
2
- * `queryDocs` the offline documentation query.
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. An unknown topic throws instead of
9
- * 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 so the bin can leave stdout at zero bytes and exit 2. Text written
13
- * halfway is read as the document and quoted as the document — the same direction the
14
- * 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 — the topic list `pdks docs` prints with no argument. */
19
- export const TOPICS = ['install', 'config', 'discipline', 'covenant', 'witness'];
20
- /** The 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;
1
+ import { runDocs } from './docs-library.js';
2
+ export { DOCS_TOPICS as TOPICS } from './docs-types.js';
3
+ /** Answer a legacy topic using the same catalog as search and show. */
4
+ export function queryDocs(spec) {
5
+ return runDocs({
6
+ docsRoot: spec.docsRoot,
7
+ args: spec.topic === undefined ? [] : [spec.topic],
8
+ version: '',
9
+ });
55
10
  }
56
11
  /**
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.
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.
12
+ * Extract a section by its exact heading, retaining the original internal helper's contract.
13
+ * Topic retrieval uses stable IDs instead; this helper remains for existing internal callers.
67
14
  */
68
15
  export function extractSection(markdown, heading) {
69
16
  const lines = markdown.split('\n');
70
- const level = headingLevel(heading);
71
- let openMarker;
17
+ const level = /^(#{1,6}) /.exec(heading)?.[1].length ?? 0;
18
+ let fence;
72
19
  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
- }
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const line = lines[index];
22
+ const marker = /^(`{3,}|~{3,})/.exec(line.trim())?.[1];
23
+ if (marker) {
24
+ if (fence === undefined)
25
+ fence = { marker: marker[0], length: marker.length };
26
+ else if (fence.marker === marker[0] && marker.length >= fence.length)
27
+ fence = undefined;
86
28
  continue;
87
29
  }
88
- if (openMarker !== undefined) {
30
+ if (fence !== undefined)
89
31
  continue;
90
- }
91
32
  if (start === -1) {
92
- if (line === heading) {
93
- start = i;
94
- }
33
+ if (line === heading)
34
+ start = index;
95
35
  continue;
96
36
  }
97
- if (headingLevel(line) > 0 && headingLevel(line) <= level) {
98
- return lines.slice(start, i).join('\n');
99
- }
37
+ const nextLevel = /^(#{1,6}) /.exec(line)?.[1].length ?? 0;
38
+ if (nextLevel > 0 && nextLevel <= level)
39
+ return lines.slice(start, index).join('\n');
100
40
  }
101
- if (start === -1) {
41
+ if (start === -1)
102
42
  throw new Error(`heading not found: ${heading}`);
103
- }
104
- // A section that closes the document ends at end of file; the topic map points at one.
105
43
  return lines.slice(start).join('\n');
106
44
  }
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.
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 mapped 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
- }
@@ -0,0 +1,105 @@
1
+ /** Languages included in every bundled document. */
2
+ export type DocsLanguage = 'en' | 'ko';
3
+ /** Backward-compatible topic names; their targets belong to the catalog. */
4
+ export declare const DOCS_TOPICS: readonly ['install', 'config', 'discipline', 'covenant', 'witness'];
5
+ /** Source-root-relative Markdown path and localized metadata used for retrieval and ranking. */
6
+ export type DocsTranslation = {
7
+ path: string;
8
+ title: string;
9
+ summary: string;
10
+ terms?: string[];
11
+ };
12
+ /** Catalog entry identifying a document across languages and selecting it for bundling. */
13
+ export type DocsDocument = {
14
+ id: string;
15
+ category: string;
16
+ order: number;
17
+ bundled: boolean;
18
+ en: DocsTranslation;
19
+ ko?: DocsTranslation;
20
+ };
21
+ /** Source inventory shared by the documentation build and offline query commands. */
22
+ export type DocsCatalog = {
23
+ schemaVersion: number;
24
+ documents: DocsDocument[];
25
+ topics: Record<string, DocsTopic>;
26
+ redirects?: DocsRedirect[];
27
+ };
28
+ /** Ordered content references for a legacy topic, plus a document ID for further reading. */
29
+ export type DocsTopic = {
30
+ references: DocsReference[];
31
+ seeAlso: string;
32
+ };
33
+ /** Stable document ID, optionally narrowed to a section; omission selects the whole document. */
34
+ export type DocsReference = {
35
+ documentId: string;
36
+ sectionId?: string;
37
+ };
38
+ /** Move-notice path and canonical destination, both relative to the documentation root. */
39
+ export type DocsRedirect = {
40
+ path: string;
41
+ target: string;
42
+ };
43
+ /** Anchored Markdown slice with zero-based line bounds: inclusive start, exclusive end. */
44
+ export type DocsSection = {
45
+ id: string;
46
+ title: string;
47
+ level: number;
48
+ startLine: number;
49
+ endLine: number;
50
+ text: string;
51
+ };
52
+ /** Bilingual metadata returned for a document included in a completed build. */
53
+ export type DocsBundleDocument = {
54
+ id: string;
55
+ bundled: boolean;
56
+ category: string;
57
+ order: number;
58
+ translations: Record<DocsLanguage, DocsTranslation>;
59
+ };
60
+ /** Indexed section location; the hash covers its entire translated document, not the section. */
61
+ export type DocsBundleSection = {
62
+ documentId: string;
63
+ language: DocsLanguage;
64
+ sectionId: string;
65
+ title: string;
66
+ level: number;
67
+ path: string;
68
+ hash: string;
69
+ };
70
+ /** Persisted bundle metadata and document hashes, reconstructed on load to detect inconsistency. */
71
+ export type DocsIndex = {
72
+ schemaVersion: 1;
73
+ documents: Array<{
74
+ id: string;
75
+ bundled: boolean;
76
+ category: string;
77
+ order: number;
78
+ translations: Record<DocsLanguage, DocsTranslation>;
79
+ hashes: Record<DocsLanguage, string>;
80
+ }>;
81
+ sections: DocsBundleSection[];
82
+ };
83
+ /** In-memory catalog and index with loaded Markdown keyed by stable document ID. */
84
+ export type LoadedDocsBundle = {
85
+ catalog: DocsCatalog;
86
+ index: DocsIndex;
87
+ documents: Map<string, LoadedDocsDocument>;
88
+ topics: Record<string, DocsTopic>;
89
+ };
90
+ /** Loaded bilingual Markdown with parsed sections and empty search-term lists where omitted. */
91
+ export type LoadedDocsDocument = {
92
+ id: string;
93
+ bundled: boolean;
94
+ category: string;
95
+ order: number;
96
+ translations: Record<DocsLanguage, {
97
+ path: string;
98
+ title: string;
99
+ summary: string;
100
+ terms: string[];
101
+ markdown: string;
102
+ hash: string;
103
+ sections: DocsSection[];
104
+ }>;
105
+ };
@@ -0,0 +1,2 @@
1
+ /** Backward-compatible topic names; their targets belong to the catalog. */
2
+ export const DOCS_TOPICS = ['install', 'config', 'discipline', 'covenant', 'witness'];
package/dist/explain.d.ts CHANGED
@@ -16,11 +16,9 @@ export type ExplainSpec = {
16
16
  /**
17
17
  * Read the config at `repoRoot`, assemble both surfaces, and render them.
18
18
  *
19
- * The session assembly is given a transcript path, so its `transcript-mod` registration and
20
- * the context family exist here exactly as they do under a normal hook payload — the path is
21
- * never read, because the injected transcript is the no-op one. Without a transcript the
22
- * compiler would report every context entry as a skip, which is the COMMIT surface's answer,
23
- * not the session's.
19
+ * The session assembly is given a transcript path, so its `transcript-mod` registration
20
+ * exists here exactly as it does under a normal hook payload — the path is never read,
21
+ * because the injected transcript is the no-op one.
24
22
  */
25
23
  export declare function explain(spec: ExplainSpec): Promise<{
26
24
  text: string;
package/dist/explain.js CHANGED
@@ -11,39 +11,45 @@
11
11
  */
12
12
  import { join } from 'node:path';
13
13
  import { resolveGitAdapterSettings } from '@polydeukes/adapter-git';
14
- import { noopTranscript } from '@polydeukes/core';
14
+ import { AXIS_NAMES, deriveShape, noopTranscript, RELATION_NAMES } from '@polydeukes/core';
15
15
  import { assembleSessionRegistrations } from './claude-code-hook.js';
16
16
  import { assembleCommitRegistrations } from './covenant-check.js';
17
17
  import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
18
18
  import { loadConfig } from './load-config.js';
19
19
  /** The three meta-covenant labels: registrations that protect the judging chain itself. */
20
20
  const META_LABELS = new Set(['self-mod', 'shell-mod', 'transcript-mod']);
21
- /** Normalize an optional glob field to an array (absent = empty). */
22
- function toGlobs(value) {
23
- if (value === undefined)
24
- return [];
25
- return typeof value === 'string' ? [value] : value;
26
- }
27
21
  /**
28
- * The routing scope of a config entry, in its own family's shape: the delta and context
29
- * families scope by `in`/`except` globs, the path family by its own glob, and the command
30
- * family by nothing at all it judges the command line, which no path scopes.
22
+ * The description of a declaration entry: its catalogue coordinate (the mechanism, the axes
23
+ * its sources derive, and the relations its entries decide), then what it routes on, how
24
+ * large its two regex lists are, how many sources it names and how many of those carry
25
+ * each non-file kind, whether it carries a valve, and whether the author left a `why`. An
26
+ * absent scope block admits every world.
27
+ *
28
+ * The axes are derived, never read off the declaration: `loadConfig` has already run the
29
+ * declaration through the validator, so the shape here is the one the catalogue admitted.
31
30
  */
32
- function scopeOf(entry) {
33
- if (entry.immutable !== undefined) {
34
- return `immutable ${toGlobs(entry.immutable).join(', ')}`;
35
- }
36
- if (entry.forbidCommand !== undefined) {
37
- return 'forbidCommand · (no path scope)';
38
- }
39
- const family = entry.requirePrecedent === undefined
40
- ? 'forbid'
41
- : `requirePrecedent ${Object.keys(entry.requirePrecedent).join(', ')}`;
42
- const inGlobs = toGlobs(entry.in);
43
- const scope = inGlobs.length === 0 ? 'every file' : `in ${inGlobs.join(', ')}`;
44
- const exceptGlobs = toGlobs(entry.except);
45
- const except = exceptGlobs.length === 0 ? '' : ` · except ${exceptGlobs.join(', ')}`;
46
- return `${family} · ${scope}${except}`;
31
+ function declareDescription(entry, enforce) {
32
+ const declare = entry.declare;
33
+ const shape = deriveShape(declare);
34
+ const axes = AXIS_NAMES.filter((axis) => shape.axes.has(axis)).join(',');
35
+ const relations = RELATION_NAMES.filter((relation) => shape.relations.has(relation)).join(',');
36
+ const relate = declare.relate.map((relateEntry) => relateEntry.id).join(', ');
37
+ const scope = declare.scope === undefined ? 'scope every world' : `scope ${declare.scope.source}`;
38
+ const include = declare.scope?.include?.length ?? 0;
39
+ const exclude = declare.scope?.exclude?.length ?? 0;
40
+ const bindings = Object.values(declare.sources ?? {});
41
+ // A file binding is the unmarked kind, so only the two the surface has to supply are
42
+ // counted out; a kind nothing binds is left off rather than printed as a zero.
43
+ const kinds = ['sidecar', 'transcript']
44
+ .map((kind) => ({ kind, count: bindings.filter((binding) => kind in binding).length }))
45
+ .filter(({ count }) => count > 0)
46
+ .map(({ kind, count }) => `${kind} ${count}`);
47
+ const counted = kinds.length === 0 ? '' : ` (${kinds.join(', ')})`;
48
+ const sources = `sources ${bindings.length}${counted}`;
49
+ const valve = declare.witness === undefined ? '—' : '✓';
50
+ const why = entry.why === undefined ? '—' : '✓';
51
+ return (`${declare.mechanism} · ${axes} · ${relations} ${relate} · ${scope} · ` +
52
+ `include ${include} · exclude ${exclude} · ${sources} · valve ${valve} · why ${why}${enforce}`);
47
53
  }
48
54
  /** One rendered line: the kind column, the label column, then the description. */
49
55
  function row(kind, label, width, description) {
@@ -59,8 +65,8 @@ function metaDescription(registration, surface) {
59
65
  /** Render one surface: its header, its tallies, and one line per registration. */
60
66
  function renderSurface(spec) {
61
67
  const lines = [];
62
- const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.excluded.map((entry) => entry.id.length), ...spec.drafts.map((draft) => draft.id.length));
63
- let judged = 0;
68
+ const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.drafts.map((draft) => draft.id.length));
69
+ let declare = 0;
64
70
  let skip = 0;
65
71
  let meta = 0;
66
72
  for (const registration of spec.registrations) {
@@ -75,38 +81,35 @@ function renderSurface(spec) {
75
81
  lines.push(row('skip', registration.label, width, registration.skip.reason));
76
82
  continue;
77
83
  }
78
- judged += 1;
84
+ // Every non-meta body registration is one config entry's declaration; a label the config
85
+ // does not carry is an assembly the renderer was never told about.
79
86
  const entry = spec.disciplines.find((candidate) => candidate.id === registration.label);
87
+ if (entry === undefined) {
88
+ throw new Error(`explain: registration '${registration.label}' matches no config entry`);
89
+ }
80
90
  // The DECLARED level is rendered, never the effective one: an omission stays unmarked
81
91
  // so the default and an author's explicit choice of it never read alike, and the
82
92
  // surface header states what the omission resolves to.
83
- const level = entry?.enforce === undefined ? '' : ` · enforce: ${entry.enforce}`;
84
- const description = entry === undefined
85
- ? ''
86
- : `${scopeOf(entry)} · why ${entry.why === undefined ? '—' : '✓'}${level}`;
87
- lines.push(row('judge', registration.label, width, description));
88
- }
89
- for (const entry of spec.excluded) {
90
- lines.push(row('excluded', entry.id, width, 'forbidCommand — no shell axis on this surface'));
93
+ const level = entry.enforce === undefined ? '' : ` · enforce: ${entry.enforce}`;
94
+ declare += 1;
95
+ lines.push(row('declare', registration.label, width, declareDescription(entry, level)));
91
96
  }
92
97
  for (const draft of spec.drafts) {
93
98
  lines.push(row('draft', draft.id, width, 'unpromoted — no judgment'));
94
99
  }
95
- const tally = ` registrations ${meta + judged + skip} · judged ${judged} · skip ${skip} · ` +
96
- `meta ${meta} · excluded ${spec.excluded.length} · draft ${spec.drafts.length}`;
100
+ const tally = ` registrations ${meta + declare + skip} · ` +
101
+ `declare ${declare} · skip ${skip} · meta ${meta} · draft ${spec.drafts.length}`;
97
102
  return [spec.header, tally, ...lines].join('\n');
98
103
  }
99
104
  /**
100
105
  * Read the config at `repoRoot`, assemble both surfaces, and render them.
101
106
  *
102
- * The session assembly is given a transcript path, so its `transcript-mod` registration and
103
- * the context family exist here exactly as they do under a normal hook payload — the path is
104
- * never read, because the injected transcript is the no-op one. Without a transcript the
105
- * compiler would report every context entry as a skip, which is the COMMIT surface's answer,
106
- * not the session's.
107
+ * The session assembly is given a transcript path, so its `transcript-mod` registration
108
+ * exists here exactly as it does under a normal hook payload — the path is never read,
109
+ * because the injected transcript is the no-op one.
107
110
  */
108
111
  export async function explain(spec) {
109
- const { config, configPath } = loadConfig(spec.repoRoot);
112
+ const { config, configPath } = loadConfig({ rootDir: spec.repoRoot });
110
113
  // Resolved and imported exactly as the two runners do, so what this renders is the table
111
114
  // that would judge: a dist those runners would refuse cannot be rendered as if it worked.
112
115
  // The load names the missing module and the recovery command.
@@ -125,14 +128,13 @@ export async function explain(spec) {
125
128
  rootDir: spec.repoRoot,
126
129
  covenant,
127
130
  });
128
- const gitSettings = resolveGitAdapterSettings(config.adapters?.git);
131
+ const gitSettings = resolveGitAdapterSettings({ namespace: config.adapters?.git });
129
132
  const text = [
130
133
  `pdks explain — ${configPath}`,
131
134
  '',
132
135
  renderSurface({
133
136
  header: 'surface: session (claude-code hook) · disciplines: advise unless enforce: block · meta: block',
134
137
  registrations: session,
135
- excluded: [],
136
138
  drafts,
137
139
  disciplines,
138
140
  selfModScope: 'common; includes the config file itself',
@@ -141,7 +143,6 @@ export async function explain(spec) {
141
143
  renderSurface({
142
144
  header: `surface: commit (git pre-commit) · enforce: ${gitSettings.enforce} · disciplines: advise unless enforce: block`,
143
145
  registrations: commit,
144
- excluded: disciplines.filter((entry) => entry.forbidCommand !== undefined),
145
146
  drafts,
146
147
  disciplines,
147
148
  selfModScope: 'common ∪ adapters.git; deduped, includes the config file itself',
package/dist/index.d.ts CHANGED
@@ -18,6 +18,5 @@
18
18
  * See https://github.com/huskyhoochu/polydeukes
19
19
  */
20
20
  export type { ResolvedConfig } from '@polydeukes/core';
21
- export { type ClaudeCodeHookSpec, runClaudeCodeHook } from './claude-code-hook.js';
22
- export { type CheckDomain, type CovenantCheckSpec, runCovenantCheck } from './covenant-check.js';
23
- export { type LoadedConfig, loadConfig } from './load-config.js';
21
+ export { type CheckDomain, type CovenantCheckOutcome, type CovenantCheckSpec, runCovenantCheck, } from './covenant-check.ts';
22
+ export { type LoadConfigSpec, type LoadedConfig, loadConfig } from './load-config.ts';
package/dist/index.js CHANGED
@@ -17,6 +17,5 @@
17
17
  *
18
18
  * See https://github.com/huskyhoochu/polydeukes
19
19
  */
20
- export { runClaudeCodeHook } from './claude-code-hook.js';
21
- export { runCovenantCheck } from './covenant-check.js';
20
+ export { runCovenantCheck, } from './covenant-check.js';
22
21
  export { loadConfig } from './load-config.js';
@@ -14,9 +14,11 @@
14
14
  *
15
15
  * Nothing existing is overwritten. The settings file in particular is merged, never
16
16
  * replaced: a consumer's other PreToolUse registrations and permissions are live
17
- * configuration, and replacing them would disarm every other tool they wired.
17
+ * configuration, and replacing them would disarm every other tool they wired. A grok JSON
18
+ * whose command still names the grok mjs is rewritten to this hook's command so the two
19
+ * installers do not leave two spawn strings.
18
20
  */
19
- import { type ScaffoldReport } from './scaffold-project.js';
21
+ import { type ScaffoldReport } from './scaffold-project.ts';
20
22
  /**
21
23
  * The generated classification skill — the procedure that turns a described problem into a
22
24
  * registered entry. A classification procedure an agent never learns about is one that never
@@ -26,7 +28,7 @@ import { type ScaffoldReport } from './scaffold-project.js';
26
28
  * lets an advised call through with exit 0, and the reason never reaches the model at call
27
29
  * time — reading the telemetry log at task boundaries is the only way it arrives.
28
30
  */
29
- export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the current families can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nAsk these questions in order; the first yes decides.\n\n| # | Question | Family | Entry key |\n| --- | --- | --- | --- |\n| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | delta | `forbid` |\n| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | path | `immutable` |\n| 3 | Is it about the shell command line itself, regardless of files? | command | `forbidCommand` |\n| 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | context | `requirePrecedent` |\n| 5 | None of the above | \u2014 | `draft: true` (step 4b) |\n\nExisting occurrences are forgiven by the delta family \u2014 only new additions break the promise.\nThat is usually what you want: a discipline adopted today should not indict yesterday's code.\n\nTwo path-shaped promises take no `disciplines:` entry at all. A path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry\nkey. And a path that must never be CREATED is not expressible today: `immutable` allows\ncreation by design, so register that promise as a draft (step 4b).\n\n### 3. Check the observation boundary\n\nTwo kinds of promise cannot be judged here, whatever their shape:\n\n- **Destruction outside the repository** \u2014 judgment observes the project root only. Register\n nothing; use the agent's own permission deny policy for commands like `rm -rf ~`.\n- **Writes by child processes** \u2014 a test runner or script writing files is invisible to the\n session surface, which judges declared tool calls only. Say so to the user; the commit\n surface will still see the result as a staged diff.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n forbid: '\\.only\\('\n enforce: advise\n```\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow `in:` to the files where any\n match IS a break (`in:`/`except:` scope `forbid` and `requirePrecedent` only), or\n accept \"editing this file at all\" as the trigger.\n- **`^` silently disarms on the delta axis.** `forbid` scans whole file content as one\n string, so a line-start anchor matches the first line only \u2014 write `(^|\\n)` there.\n `forbidCommand` judges per line and the whole string, so `^` is safe on that axis.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`forbid` vs `forbidden`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the SHAPE of the promise inside `why`, so the promotion destination is already\nwritten down when a later engine can express it. Name the shape in these terms:\n\n| Shape | The promise reads like |\n| --- | --- |\n| pairing | every element of set A has a counterpart in set B (translation keys, i18n) |\n| companion | if X appears in a unit, Y must appear with it |\n| ordered | a sequence must keep its order (migration journals, version ladders) |\n| fingerprint | a derived artifact must match the hash/stamp of its source |\n| producer-owned | only a designated generator may write this artifact |\n| self-absolution | the party being judged must not write its own verdict field |\n| actor-scope | the same action is fine for one actor and a break for another |\n| phase-order | several precedents, in a fixed order |\n| turn-locality | the evidence must be in the same turn or time window |\n| stated-ground | the reason must be written down before the action |\n| controlled-vocabulary | only an enumerated set of words/values is allowed |\n| naming-convention | names must match a pattern per kind |\n| irreversible-marker | once present, a marker may never be removed |\n| delegation-scope | a delegated task may touch only its granted scope |\n| scope-valve | a defined exception valve, judged rather than ad hoc |\n| claim-verification | the claim must be re-run/measured, not trusted |\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'locale-files-move-together'\n why: 'pairing \u2014 en.json and ko.json must change in the same commit; one side alone is a break'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its family and\nsurfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run its family can\nactually reach:\n\n| Family | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| `forbid` / `immutable` | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidCommand` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `requirePrecedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 this family judges on the session surface only (the commit surface records it `skipped`) |\n\nThen undo the scratch break, repeat the same run, and confirm silence on the\nmust-NOT-match direction. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
31
+ export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the declaration grammar and observed evidence can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nChoose from the current catalogue, then check whether the intended surface can supply the\nrequired evidence. A mechanism name constrains the declaration; it does not implement the\npromise by itself. The extracted axes and body relations must be subsets of the admitted\nsets below. Scope filtering is separate from the extracted axes.\n\n| Mechanism | Admitted axes | Body relations | Evidence or structural condition |\n| --- | --- | --- | --- |\n| `pairing` | `world` | `equal` | Compare supplied files or channels; extract keys when values may differ. |\n| `companion` | `change`, `world` | `implies` | Compare presence by key; a multi-file promise needs the observed change set. |\n| `monotonic-order` | `change`, `world` | `ordered` | Extract a sequence with an explicit comparison field; order is not presence. |\n| `fingerprint-sync` | `world` | `equal` | Compare supplied stamps; no generator or compiler runs during judgment. |\n| `producer-owned` | `actor` | `empty`, `nonEmpty` | Requires host-provided actor evidence, not an artifact's self-reported producer. |\n| `self-absolution-ban` | `change` | `unchanged`, `empty` | Extract protected fields or path changes; choose creation/deletion supply explicitly. |\n| `actor-scope` | `actor` | `empty`, `nonEmpty` | Requires a proven actor; a missing actor is not proof of the main session. |\n| `precedent` | `history`, `world` | `nonEmpty` | Requires an observed earlier call in a transcript or supplied channel. |\n| `phase-order` | `history` | `ordered` | Compare observed call ordinals; missing phases need a separate presence promise. |\n| `turn-locality` | `history` | `nonEmpty` | Requires observed turns and time or ordinal boundaries. |\n| `stated-ground` | `history` | `nonEmpty` | Can require recorded text, not establish whether its reasoning is sound. |\n| `controlled-vocabulary` | `change`, `world` | `subset` | Extract values and an explicit allowed set. |\n| `naming` | `change` | `empty`, `nonEmpty` | Scope must read `target.path`; match the intended name pattern. |\n| `added-only` | `change` | `empty` | Compare pre/post extractions and judge only newly added matches. |\n| `one-way-marker` | `change` | `subset` | Existing markers must remain in the extracted post-change set. |\n| `delegated-scope` | \u2014 | \u2014 | Reserved for a definition-time evaluator; not accepted in current declarations. |\n| `scoped-valve` | `change`, `actor`, `world`, `history` | `empty`, `nonEmpty`, `equal`, `subset`, `implies`, `ordered`, `unchanged` | Requires a `witness` block expressing the exception condition. |\n| `forbidden-command` | `change` | `empty` | Scope must read `command`; a text pattern is not shell semantic analysis. |\n\nThese four requests illustrate the classification boundary:\n\n| Request | Classification | Proof |\n| --- | --- | --- |\n| The English and Korean locale files must carry identical keys. | `pairing`, with two supplied files. | An unmatched key breaks; translated values may differ. |\n| Every status must belong to an allowed list. | `controlled-vocabulary`, with a supplied allowed set. | An unknown status breaks; an allowed status passes. |\n| A successful package lookup must precede a manifest edit. | `precedent`, with observed session history. | Failed or absent lookups break; an unavailable transcript is a supply case. |\n| A fresh benchmark must execute during judgment to prove a performance claim. | `draft`: the engine does not execute benchmarks. | Comparing an existing report would be a different promise. |\n\nRun `pdks docs show write-disciplines` for the key-pairing walkthrough and\n`pdks docs show configuration --section disciplines` for the declaration grammar.\nUse `--lang ko` for Korean; these commands read the installed version offline.\n\nAn `added-only` declaration forgives existing occurrences \u2014 only what the edit adds breaks\nthe promise. That is usually what you want: a discipline adopted today should not indict\nyesterday's code.\n\nOne path-shaped promise takes no `disciplines:` entry at all: a path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry key.\n\n### 3. Check the observation boundary\n\nDo not confuse an expressible relation with available evidence:\n\n- **Files outside the repository** \u2014 file-change protection observes the project root.\n Use the host's permission policy for comprehensive protection outside it. A command-text\n pattern may recognize a particular string, but does not observe all resulting writes.\n- **Writes by child processes** \u2014 arbitrary writes inside a test runner or script are not\n individually observed by the session surface. A commit comparison can observe the resulting\n files when they enter its selected diff; it does not recover the originating tool history.\n- **Missing history or actor channels** \u2014 choose the declaration's supply policy explicitly.\n Commit observations have no session transcript; `supply: pass` records a skip, not success.\n- **Fresh execution or semantic proof** \u2014 the engine compares supplied evidence. It does not\n run a new benchmark or prove that a written explanation is true. Preserve that unmet promise\n as a draft rather than silently replacing it with a weaker text check.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n declare:\n mechanism: 'added-only'\n scope: { source: 'target.path', include: ['^src/'] }\n supply: { pre: 'empty', post: 'empty' }\n extract:\n before:\n - { op: 'source', of: 'pre' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n after:\n - { op: 'source', of: 'post' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n added:\n - { op: 'onlyIn', of: 'after', notIn: 'before' }\n relate:\n - id: 'nothing-added'\n relation: { op: 'empty', of: 'added' }\n message: 'adds {key}: {value}'\n enforce: advise\n```\n\nA command-line ban reads the fixed source `command` and scopes on it \u2014 the scope is part of\nthe mechanism's shape, so a `forbidden-command` entry without it is refused at load time:\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-force-push'\n why: 'a force push rewrites history nobody reviewed'\n declare:\n mechanism: 'forbidden-command'\n scope: { source: 'command' }\n extract:\n hits:\n - { op: 'source', of: 'command' }\n - { op: 'lines' }\n - { op: 'matches', re: 'git push\\b.*--force(?![\\w-])' }\n relate:\n - { id: 'no-force', relation: { op: 'empty', of: 'hits' }, message: '{value}' }\n enforce: advise\n```\n\nThe following examples implement the first three classification cases. Both locale files and\nthe allowed-status file must exist and contain valid JSON. File bindings use the proposed\ncontents for a file changed by the current observation, not a second stale disk read.\n\n```yaml\nlanguages:\n json:\n productionGlob: 'locales/**/*.json'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'locale-key-parity'\n why: 'the ko and en locales must carry the same keys'\n declare:\n mechanism: 'pairing'\n scope: { source: 'target.path', include: ['^locales/(ko|en)[.]json$'] }\n sources:\n ko: { file: 'locales/ko.json' }\n en: { file: 'locales/en.json' }\n supply: { ko: 'error', en: 'error' }\n extract:\n koKeys: [{ op: 'source', of: 'ko' }, { op: 'json' }, { op: 'flattenKeys' }]\n enKeys: [{ op: 'source', of: 'en' }, { op: 'json' }, { op: 'flattenKeys' }]\n relate:\n - id: 'parity'\n relation: { op: 'equal', of: ['koKeys', 'enKeys'] }\n messageBySide:\n left: '{key} is in ko only'\n right: '{key} is in en only'\n enforce: advise\n```\n\n```yaml\nlanguages:\n json:\n productionGlob: '*.json'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'status-vocabulary'\n why: 'statuses.json may contain only values listed in allowed-statuses.json'\n declare:\n mechanism: 'controlled-vocabulary'\n scope: { source: 'target.path', include: ['^statuses[.]json$'] }\n sources: { allowed: { file: 'allowed-statuses.json' } }\n supply: { post: 'error', allowed: 'error' }\n extract:\n selected: [{ op: 'source', of: 'post' }, { op: 'json' }, { op: 'items' }]\n permitted: [{ op: 'source', of: 'allowed' }, { op: 'json' }, { op: 'items' }]\n relate:\n - id: 'allowed-status'\n relation: { op: 'subset', of: 'selected', in: 'permitted' }\n message: 'unknown status: {value}'\n enforce: advise\n```\n\nHere both status files are JSON arrays of strings. This declaration scopes on statuses.json;\nediting only the allowed list does not trigger it. Broaden the observation deliberately if\nchanges to that list must recheck all dependent files.\n\n```yaml\nlanguages:\n typescript:\n productionGlob: 'src/**'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'manifest-needs-npm-view'\n why: 'a successful package lookup must precede a manifest edit'\n declare:\n mechanism: 'precedent'\n scope: { source: 'target.path', include: ['^(packages/[^/]+/)?package[.]json$'] }\n sources: { session: { transcript: true } }\n supply: { session: 'pass' }\n extract:\n npmView:\n - { op: 'source', of: 'session' }\n - { op: 'toolUses', names: ['Bash'] }\n - { op: 'filter', when: [{ field: 'succeeded', eq: true }] }\n - { op: 'select', path: 'args.command' }\n - { op: 'matches', re: '^npm view ' }\n relate:\n - id: 'npm-view'\n relation: { op: 'nonEmpty', of: 'npmView' }\n message: 'no successful npm view precedes this edit'\n enforce: advise\n```\n\nThe precedent example proves only that an observed successful Bash call starts with npm view;\nit does not prove that the lookup concerns the dependency being edited. The commit surface has\nno transcript and therefore skips this example by its explicit supply policy.\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow the declaration's own `scope`\n block to the files where any match IS a break, or accept \"editing this file at all\" as\n the trigger.\n- **`^` means what the preceding step left.** After a `lines` step a declaration's\n pattern sees one line at a time, so `^` anchors to that line; over an unsplit source it\n anchors to the whole text and matches the first line only. A ban over the command line\n puts `lines` before its `matches` for exactly that reason.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`only(` vs `only_helper(`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the intended promise and the exact missing capability inside `why`. Do not classify\npairing, vocabulary, or history promises as drafts merely because they are absent from a short\nexample list. Check the catalogue, extraction steps, and observation channel first. A reserved\n`delegated-scope` declaration cannot be registered as a judged entry.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'benchmark-supports-performance-claim'\n why: 'a performance claim needs a fresh benchmark run during judgment; the engine cannot execute it'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its mechanism\nand surfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run the declaration's\nown mechanism can actually reach:\n\n| Mechanism | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| a file-reading one (`added-only`, `naming`, \u2026) | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidden-command` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `precedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 a declaration reading the session judges on the session surface only (the commit surface has none, so its `supply` policy records it `skipped`) |\n\nThen undo the scratch break, repeat the same observation, and confirm a passing row for the\nmust-NOT-match case. Silence alone may mean a scope miss, unchanged files, or unavailable evidence;\ncheck `pdks explain` and telemetry for `config-fault`, `no-observation`, or `supply-pass`. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Updating this skill without losing local edits\n\nAn upgrade does not overwrite an existing skill; rerunning `pdks init claude-code` reports it\nskipped. Generate a fresh copy in a disposable project using the installed package, compare it\nwith this file, and merge the changes you want. Keep a backup of local additions. Do not delete\nthe existing skill to force regeneration in the working project.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
30
32
  /** `initClaudeCode` input — the target tree and the preflight seam. */
31
33
  export type InitClaudeCodeSpec = {
32
34
  /** Project root to install into — every write below is relative to it. */