polydeukes 0.6.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 (55) hide show
  1. package/README.ko.md +54 -80
  2. package/README.md +55 -94
  3. package/dist/bin.js +8 -5
  4. package/dist/docs/README.ko.md +60 -0
  5. package/dist/docs/README.md +64 -0
  6. package/dist/docs/catalog.json +464 -0
  7. package/dist/docs/concepts/judgment.ko.md +113 -0
  8. package/dist/docs/concepts/judgment.md +113 -0
  9. package/dist/docs/how-to/configure-project.ko.md +99 -0
  10. package/dist/docs/how-to/configure-project.md +95 -0
  11. package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
  12. package/dist/docs/how-to/connect-surfaces.md +118 -0
  13. package/dist/docs/how-to/write-disciplines.ko.md +124 -0
  14. package/dist/docs/how-to/write-disciplines.md +125 -0
  15. package/dist/docs/index.json +2046 -0
  16. package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
  17. package/dist/docs/reference/cli/covenant-check.md +98 -0
  18. package/dist/docs/reference/cli/docs.ko.md +97 -0
  19. package/dist/docs/reference/cli/docs.md +95 -0
  20. package/dist/docs/reference/cli/explain.ko.md +79 -0
  21. package/dist/docs/reference/cli/explain.md +84 -0
  22. package/dist/docs/reference/cli/init.ko.md +119 -0
  23. package/dist/docs/reference/cli/init.md +131 -0
  24. package/dist/docs/reference/configuration/index.ko.md +448 -0
  25. package/dist/docs/reference/{configuration.md → configuration/index.md} +48 -30
  26. package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
  27. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +10 -6
  28. package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
  29. package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +20 -12
  30. package/dist/docs/reference/packages/core.ko.md +128 -0
  31. package/dist/docs/reference/{core.md → packages/core.md} +21 -8
  32. package/dist/docs/reference/packages/covenant.ko.md +115 -0
  33. package/dist/docs/reference/{covenant.md → packages/covenant.md} +18 -11
  34. package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
  35. package/dist/docs/reference/packages/polydeukes.md +139 -0
  36. package/dist/docs/troubleshooting.ko.md +142 -0
  37. package/dist/docs/troubleshooting.md +98 -150
  38. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  39. package/dist/docs/tutorials/first-judgment.md +81 -0
  40. package/dist/docs-catalog.d.ts +25 -0
  41. package/dist/docs-catalog.js +450 -0
  42. package/dist/docs-library.d.ts +23 -0
  43. package/dist/docs-library.js +347 -0
  44. package/dist/docs-markdown.d.ts +32 -0
  45. package/dist/docs-markdown.js +150 -0
  46. package/dist/docs-query.d.ts +11 -40
  47. package/dist/docs-query.js +28 -122
  48. package/dist/docs-types.d.ts +105 -0
  49. package/dist/docs-types.js +2 -0
  50. package/dist/init-claude-code.d.ts +1 -1
  51. package/dist/init-claude-code.js +159 -42
  52. package/package.json +5 -5
  53. package/dist/docs/configuration.md +0 -103
  54. package/dist/docs/installation.md +0 -241
  55. package/dist/docs/reference/polydeukes.md +0 -315
@@ -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'];
@@ -28,7 +28,7 @@ import { type ScaffoldReport } from './scaffold-project.ts';
28
28
  * lets an advised call through with exit 0, and the reason never reaches the model at call
29
29
  * time — reading the telemetry log at task boundaries is the only way it arrives.
30
30
  */
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 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 | Entry key |\n| --- | --- | --- |\n| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | `declare` (mechanism `added-only`) |\n| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | `declare` (mechanism `self-absolution-ban`) |\n| 3 | Is it about the shell command line itself, regardless of files? | `declare` (mechanism `forbidden-command`, reading the `command` source) |\n| 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | `declare` (mechanism `precedent`, reading a `transcript` source) |\n| 5 | None of the above | `draft: true` (step 4b) |\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\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 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\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 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 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 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";
32
32
  /** `initClaudeCode` input — the target tree and the preflight seam. */
33
33
  export type InitClaudeCodeSpec = {
34
34
  /** Project root to install into — every write below is relative to it. */
@@ -113,7 +113,9 @@ This project is judged by Polydeukes, and the matching documentation ships insid
113
113
  installed package. \`pdks docs\` answers offline, from the same version that does the
114
114
  judging; a web search answers from whichever release it indexed.
115
115
 
116
- Run \`pdks docs\` for the topic list, \`pdks docs <topic>\` for one section.
116
+ Run \`pdks docs\` for the topic list, \`pdks docs <topic>\` for topic content,
117
+ \`pdks docs search "locale key pairing"\` to find a section, or
118
+ \`pdks docs show write-disciplines\` to retrieve the guide. Add \`--lang ko\` for Korean.
117
119
 
118
120
  A local install puts the bin in \`node_modules/.bin\`, which a plain shell does not have on
119
121
  PATH. If \`pdks\` is not found, run \`./node_modules/.bin/pdks docs <topic>\` — or your package
@@ -134,7 +136,7 @@ ${TOPICS.map((topic) => `| ${DOCS_TOPIC_PURPOSE[topic]} | \`pdks docs ${topic}\`
134
136
  */
135
137
  export const GENERATED_SKILL = `---
136
138
  name: discipline-draft
137
- description: Turn a described discipline problem into a registered entry in polydeukes.config — 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").
139
+ description: Turn a described discipline problem into a registered entry in polydeukes.config — 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").
138
140
  ---
139
141
 
140
142
  # discipline-draft — from a problem description to a registered discipline
@@ -154,15 +156,44 @@ promises and classify each separately.
154
156
 
155
157
  ### 2. Classify the shape
156
158
 
157
- Ask these questions in order; the first yes decides.
158
-
159
- | # | Question | Entry key |
159
+ Choose from the current catalogue, then check whether the intended surface can supply the
160
+ required evidence. A mechanism name constrains the declaration; it does not implement the
161
+ promise by itself. The extracted axes and body relations must be subsets of the admitted
162
+ sets below. Scope filtering is separate from the extracted axes.
163
+
164
+ | Mechanism | Admitted axes | Body relations | Evidence or structural condition |
165
+ | --- | --- | --- | --- |
166
+ | \`pairing\` | \`world\` | \`equal\` | Compare supplied files or channels; extract keys when values may differ. |
167
+ | \`companion\` | \`change\`, \`world\` | \`implies\` | Compare presence by key; a multi-file promise needs the observed change set. |
168
+ | \`monotonic-order\` | \`change\`, \`world\` | \`ordered\` | Extract a sequence with an explicit comparison field; order is not presence. |
169
+ | \`fingerprint-sync\` | \`world\` | \`equal\` | Compare supplied stamps; no generator or compiler runs during judgment. |
170
+ | \`producer-owned\` | \`actor\` | \`empty\`, \`nonEmpty\` | Requires host-provided actor evidence, not an artifact's self-reported producer. |
171
+ | \`self-absolution-ban\` | \`change\` | \`unchanged\`, \`empty\` | Extract protected fields or path changes; choose creation/deletion supply explicitly. |
172
+ | \`actor-scope\` | \`actor\` | \`empty\`, \`nonEmpty\` | Requires a proven actor; a missing actor is not proof of the main session. |
173
+ | \`precedent\` | \`history\`, \`world\` | \`nonEmpty\` | Requires an observed earlier call in a transcript or supplied channel. |
174
+ | \`phase-order\` | \`history\` | \`ordered\` | Compare observed call ordinals; missing phases need a separate presence promise. |
175
+ | \`turn-locality\` | \`history\` | \`nonEmpty\` | Requires observed turns and time or ordinal boundaries. |
176
+ | \`stated-ground\` | \`history\` | \`nonEmpty\` | Can require recorded text, not establish whether its reasoning is sound. |
177
+ | \`controlled-vocabulary\` | \`change\`, \`world\` | \`subset\` | Extract values and an explicit allowed set. |
178
+ | \`naming\` | \`change\` | \`empty\`, \`nonEmpty\` | Scope must read \`target.path\`; match the intended name pattern. |
179
+ | \`added-only\` | \`change\` | \`empty\` | Compare pre/post extractions and judge only newly added matches. |
180
+ | \`one-way-marker\` | \`change\` | \`subset\` | Existing markers must remain in the extracted post-change set. |
181
+ | \`delegated-scope\` | — | — | Reserved for a definition-time evaluator; not accepted in current declarations. |
182
+ | \`scoped-valve\` | \`change\`, \`actor\`, \`world\`, \`history\` | \`empty\`, \`nonEmpty\`, \`equal\`, \`subset\`, \`implies\`, \`ordered\`, \`unchanged\` | Requires a \`witness\` block expressing the exception condition. |
183
+ | \`forbidden-command\` | \`change\` | \`empty\` | Scope must read \`command\`; a text pattern is not shell semantic analysis. |
184
+
185
+ These four requests illustrate the classification boundary:
186
+
187
+ | Request | Classification | Proof |
160
188
  | --- | --- | --- |
161
- | 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | \`declare\` (mechanism \`added-only\`) |
162
- | 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | \`declare\` (mechanism \`self-absolution-ban\`) |
163
- | 3 | Is it about the shell command line itself, regardless of files? | \`declare\` (mechanism \`forbidden-command\`, reading the \`command\` source) |
164
- | 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | \`declare\` (mechanism \`precedent\`, reading a \`transcript\` source) |
165
- | 5 | None of the above | \`draft: true\` (step 4b) |
189
+ | The English and Korean locale files must carry identical keys. | \`pairing\`, with two supplied files. | An unmatched key breaks; translated values may differ. |
190
+ | Every status must belong to an allowed list. | \`controlled-vocabulary\`, with a supplied allowed set. | An unknown status breaks; an allowed status passes. |
191
+ | 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. |
192
+ | 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. |
193
+
194
+ Run \`pdks docs show write-disciplines\` for the key-pairing walkthrough and
195
+ \`pdks docs show configuration --section disciplines\` for the declaration grammar.
196
+ Use \`--lang ko\` for Korean; these commands read the installed version offline.
166
197
 
167
198
  An \`added-only\` declaration forgives existing occurrences — only what the edit adds breaks
168
199
  the promise. That is usually what you want: a discipline adopted today should not indict
@@ -173,13 +204,19 @@ belongs in the top-level \`protectedPaths:\` list — its own config block, neve
173
204
 
174
205
  ### 3. Check the observation boundary
175
206
 
176
- Two kinds of promise cannot be judged here, whatever their shape:
207
+ Do not confuse an expressible relation with available evidence:
177
208
 
178
- - **Destruction outside the repository** — judgment observes the project root only. Register
179
- nothing; use the agent's own permission deny policy for commands like \`rm -rf ~\`.
180
- - **Writes by child processes** — a test runner or script writing files is invisible to the
181
- session surface, which judges declared tool calls only. Say so to the user; the commit
182
- surface will still see the result as a staged diff.
209
+ - **Files outside the repository** — file-change protection observes the project root.
210
+ Use the host's permission policy for comprehensive protection outside it. A command-text
211
+ pattern may recognize a particular string, but does not observe all resulting writes.
212
+ - **Writes by child processes** — arbitrary writes inside a test runner or script are not
213
+ individually observed by the session surface. A commit comparison can observe the resulting
214
+ files when they enter its selected diff; it does not recover the originating tool history.
215
+ - **Missing history or actor channels** — choose the declaration's supply policy explicitly.
216
+ Commit observations have no session transcript; \`supply: pass\` records a skip, not success.
217
+ - **Fresh execution or semantic proof** — the engine compares supplied evidence. It does not
218
+ run a new benchmark or prove that a written explanation is true. Preserve that unmet promise
219
+ as a draft rather than silently replacing it with a weaker text check.
183
220
 
184
221
  ### 4a. Expressible now — register a judged entry
185
222
 
@@ -240,12 +277,101 @@ disciplines:
240
277
  hits:
241
278
  - { op: 'source', of: 'command' }
242
279
  - { op: 'lines' }
243
- - { op: 'matches', re: 'git push\\\\b.*--force(?![\\\\w-])' }
280
+ - { op: 'matches', re: 'git push\\b.*--force(?![\\w-])' }
244
281
  relate:
245
282
  - { id: 'no-force', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
246
283
  enforce: advise
247
284
  \`\`\`
248
285
 
286
+ The following examples implement the first three classification cases. Both locale files and
287
+ the allowed-status file must exist and contain valid JSON. File bindings use the proposed
288
+ contents for a file changed by the current observation, not a second stale disk read.
289
+
290
+ \`\`\`yaml
291
+ languages:
292
+ json:
293
+ productionGlob: 'locales/**/*.json'
294
+ testCmd: 'pnpm test'
295
+ disciplines:
296
+ - id: 'locale-key-parity'
297
+ why: 'the ko and en locales must carry the same keys'
298
+ declare:
299
+ mechanism: 'pairing'
300
+ scope: { source: 'target.path', include: ['^locales/(ko|en)[.]json$'] }
301
+ sources:
302
+ ko: { file: 'locales/ko.json' }
303
+ en: { file: 'locales/en.json' }
304
+ supply: { ko: 'error', en: 'error' }
305
+ extract:
306
+ koKeys: [{ op: 'source', of: 'ko' }, { op: 'json' }, { op: 'flattenKeys' }]
307
+ enKeys: [{ op: 'source', of: 'en' }, { op: 'json' }, { op: 'flattenKeys' }]
308
+ relate:
309
+ - id: 'parity'
310
+ relation: { op: 'equal', of: ['koKeys', 'enKeys'] }
311
+ messageBySide:
312
+ left: '{key} is in ko only'
313
+ right: '{key} is in en only'
314
+ enforce: advise
315
+ \`\`\`
316
+
317
+ \`\`\`yaml
318
+ languages:
319
+ json:
320
+ productionGlob: '*.json'
321
+ testCmd: 'pnpm test'
322
+ disciplines:
323
+ - id: 'status-vocabulary'
324
+ why: 'statuses.json may contain only values listed in allowed-statuses.json'
325
+ declare:
326
+ mechanism: 'controlled-vocabulary'
327
+ scope: { source: 'target.path', include: ['^statuses[.]json$'] }
328
+ sources: { allowed: { file: 'allowed-statuses.json' } }
329
+ supply: { post: 'error', allowed: 'error' }
330
+ extract:
331
+ selected: [{ op: 'source', of: 'post' }, { op: 'json' }, { op: 'items' }]
332
+ permitted: [{ op: 'source', of: 'allowed' }, { op: 'json' }, { op: 'items' }]
333
+ relate:
334
+ - id: 'allowed-status'
335
+ relation: { op: 'subset', of: 'selected', in: 'permitted' }
336
+ message: 'unknown status: {value}'
337
+ enforce: advise
338
+ \`\`\`
339
+
340
+ Here both status files are JSON arrays of strings. This declaration scopes on statuses.json;
341
+ editing only the allowed list does not trigger it. Broaden the observation deliberately if
342
+ changes to that list must recheck all dependent files.
343
+
344
+ \`\`\`yaml
345
+ languages:
346
+ typescript:
347
+ productionGlob: 'src/**'
348
+ testCmd: 'pnpm test'
349
+ disciplines:
350
+ - id: 'manifest-needs-npm-view'
351
+ why: 'a successful package lookup must precede a manifest edit'
352
+ declare:
353
+ mechanism: 'precedent'
354
+ scope: { source: 'target.path', include: ['^(packages/[^/]+/)?package[.]json$'] }
355
+ sources: { session: { transcript: true } }
356
+ supply: { session: 'pass' }
357
+ extract:
358
+ npmView:
359
+ - { op: 'source', of: 'session' }
360
+ - { op: 'toolUses', names: ['Bash'] }
361
+ - { op: 'filter', when: [{ field: 'succeeded', eq: true }] }
362
+ - { op: 'select', path: 'args.command' }
363
+ - { op: 'matches', re: '^npm view ' }
364
+ relate:
365
+ - id: 'npm-view'
366
+ relation: { op: 'nonEmpty', of: 'npmView' }
367
+ message: 'no successful npm view precedes this edit'
368
+ enforce: advise
369
+ \`\`\`
370
+
371
+ The precedent example proves only that an observed successful Bash call starts with npm view;
372
+ it does not prove that the lookup concerns the dependency being edited. The commit surface has
373
+ no transcript and therefore skips this example by its explicit supply policy.
374
+
249
375
  **Write the regex yourself — the user states the promise, you author the pattern.** The
250
376
  pattern is the part users find hardest, so never hand the prose back and ask for one. Three
251
377
  authoring traps, each measured on a live config:
@@ -268,27 +394,10 @@ authoring traps, each measured on a live config:
268
394
 
269
395
  A draft is prose with a handle: \`id\`, \`why\`, and the literal marker \`draft: true\` — no other
270
396
  keys. It produces no judgment and no telemetry; \`pdks explain\` lists it as unpromoted.
271
- Record the SHAPE of the promise inside \`why\`, so the promotion destination is already
272
- written down when a later engine can express it. Name the shape in these terms:
273
-
274
- | Shape | The promise reads like |
275
- | --- | --- |
276
- | pairing | every element of set A has a counterpart in set B (translation keys, i18n) |
277
- | companion | if X appears in a unit, Y must appear with it |
278
- | ordered | a sequence must keep its order (migration journals, version ladders) |
279
- | fingerprint | a derived artifact must match the hash/stamp of its source |
280
- | producer-owned | only a designated generator may write this artifact |
281
- | self-absolution | the party being judged must not write its own verdict field |
282
- | actor-scope | the same action is fine for one actor and a break for another |
283
- | phase-order | several precedents, in a fixed order |
284
- | turn-locality | the evidence must be in the same turn or time window |
285
- | stated-ground | the reason must be written down before the action |
286
- | controlled-vocabulary | only an enumerated set of words/values is allowed |
287
- | naming-convention | names must match a pattern per kind |
288
- | irreversible-marker | once present, a marker may never be removed |
289
- | delegation-scope | a delegated task may touch only its granted scope |
290
- | scope-valve | a defined exception valve, judged rather than ad hoc |
291
- | claim-verification | the claim must be re-run/measured, not trusted |
397
+ Record the intended promise and the exact missing capability inside \`why\`. Do not classify
398
+ pairing, vocabulary, or history promises as drafts merely because they are absent from a short
399
+ example list. Check the catalogue, extraction steps, and observation channel first. A reserved
400
+ \`delegated-scope\` declaration cannot be registered as a judged entry.
292
401
 
293
402
  \`\`\`yaml
294
403
  languages:
@@ -296,8 +405,8 @@ languages:
296
405
  productionGlob: 'src/**'
297
406
  testCmd: 'echo "set a verification command for {scope}"'
298
407
  disciplines:
299
- - id: 'locale-files-move-together'
300
- why: 'pairing — en.json and ko.json must change in the same commit; one side alone is a break'
408
+ - id: 'benchmark-supports-performance-claim'
409
+ why: 'a performance claim needs a fresh benchmark run during judgment; the engine cannot execute it'
301
410
  draft: true
302
411
  \`\`\`
303
412
 
@@ -316,10 +425,18 @@ own mechanism can actually reach:
316
425
  | \`forbidden-command\` | run one harmless command matching the pattern | the telemetry log tail — at advise the call proceeds and its row records the id |
317
426
  | \`precedent\` | one in-scope edit made without the required precedent | the telemetry log tail — a declaration reading the session judges on the session surface only (the commit surface has none, so its \`supply\` policy records it \`skipped\`) |
318
427
 
319
- Then undo the scratch break, repeat the same run, and confirm silence on the
320
- must-NOT-match direction. Close by telling the user which rung the entry landed on and
428
+ Then undo the scratch break, repeat the same observation, and confirm a passing row for the
429
+ must-NOT-match case. Silence alone may mean a scope miss, unchanged files, or unavailable evidence;
430
+ check \`pdks explain\` and telemetry for \`config-fault\`, \`no-observation\`, or \`supply-pass\`. Close by telling the user which rung the entry landed on and
321
431
  that \`enforce: block\` is theirs to add later if the advise record earns it.
322
432
 
433
+ ## Updating this skill without losing local edits
434
+
435
+ An upgrade does not overwrite an existing skill; rerunning \`pdks init claude-code\` reports it
436
+ skipped. Generate a fresh copy in a disposable project using the installed package, compare it
437
+ with this file, and merge the changes you want. Keep a backup of local additions. Do not delete
438
+ the existing skill to force regeneration in the working project.
439
+
323
440
  ## Reading the advise record
324
441
 
325
442
  An \`advised\` row means a promise was broken and the call went through anyway. Rows land in