@hobin/developer 0.1.8 → 0.1.10

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 (66) hide show
  1. package/README.md +215 -372
  2. package/REFERENCE_ROUTING.md +243 -0
  3. package/extensions/compaction-language.ts +303 -0
  4. package/extensions/developer.ts +607 -20
  5. package/extensions/machine.ts +45 -0
  6. package/extensions/references/behavior-preserving-structural-change.md +87 -8
  7. package/extensions/skills.ts +330 -52
  8. package/extensions/state.ts +227 -2
  9. package/package.json +9 -3
  10. package/skills/abstraction-review/SKILL.md +37 -23
  11. package/skills/abstraction-review/reference-policy.json +66 -0
  12. package/skills/abstraction-review/references/field-card.md +113 -168
  13. package/skills/abstraction-review/references/repair-table.md +66 -80
  14. package/skills/abstraction-review/references/worked-examples.md +79 -256
  15. package/skills/model/SKILL.md +31 -13
  16. package/skills/model/reference-policy.json +96 -0
  17. package/skills/model/references/contract-and-replacement-models.md +112 -0
  18. package/skills/model/references/logic-query-semantics.md +89 -0
  19. package/skills/model/references/planning-models.md +75 -0
  20. package/skills/model/references/problem-modeling.md +143 -276
  21. package/skills/model/references/proof-obligations.md +71 -0
  22. package/skills/model/references/relational-constraint-models.md +110 -0
  23. package/skills/model/references/solver-result-boundaries.md +83 -0
  24. package/skills/model/references/temporal-behavior-models.md +106 -0
  25. package/skills/naming-judgment/SKILL.md +17 -4
  26. package/skills/naming-judgment/reference-policy.json +29 -0
  27. package/skills/naming-judgment/references/domain-naming.md +29 -10
  28. package/skills/schedule/SKILL.md +16 -6
  29. package/skills/schedule/reference-policy.json +29 -0
  30. package/skills/schedule/references/structural-change-timing.md +81 -18
  31. package/skills/signal/SKILL.md +17 -6
  32. package/skills/signal/reference-policy.json +29 -0
  33. package/skills/signal/references/structural-movement.md +87 -14
  34. package/skills/sketch/SKILL.md +38 -42
  35. package/skills/sketch/reference-policy.json +212 -0
  36. package/skills/sketch/references/accumulator-invariants.md +125 -0
  37. package/skills/sketch/references/closure-and-conventional-interfaces.md +87 -0
  38. package/skills/sketch/references/composition-by-wishes.md +69 -0
  39. package/skills/sketch/references/data-driven-design.md +99 -22
  40. package/skills/sketch/references/data-shape-template-catalog.md +58 -19
  41. package/skills/sketch/references/design-levels-and-boundaries.md +133 -0
  42. package/skills/sketch/references/earned-abstraction.md +75 -0
  43. package/skills/sketch/references/generative-recursion.md +122 -0
  44. package/skills/sketch/references/generic-dispatch-systems.md +75 -0
  45. package/skills/sketch/references/language-semantics.md +82 -0
  46. package/skills/sketch/references/meaning-preserving-conversions.md +69 -0
  47. package/skills/sketch/references/process-shape-and-resources.md +157 -0
  48. package/skills/sketch/references/representation-barriers.md +73 -0
  49. package/skills/sketch/references/responsibility-and-collaboration.md +108 -0
  50. package/skills/sketch/references/runtime-and-compilation.md +84 -0
  51. package/skills/sketch/references/selection-and-creation.md +76 -0
  52. package/skills/sketch/references/state-history-and-order.md +126 -0
  53. package/skills/sketch/references/type-transitions.md +54 -0
  54. package/skills/sketch/references/variation-roles.md +59 -0
  55. package/skills/verify/SKILL.md +17 -6
  56. package/skills/verify/reference-policy.json +30 -0
  57. package/skills/verify/references/verifier-selection-and-pass-but-wrong.md +152 -192
  58. package/SOURCES.md +0 -104
  59. package/skills/abstraction-review/references/recipe-cards.md +0 -322
  60. package/skills/model/references/worked-models-and-specialized-techniques.md +0 -218
  61. package/skills/sketch/references/abstraction-barriers-and-closure.md +0 -160
  62. package/skills/sketch/references/abstraction-composition-and-state.md +0 -184
  63. package/skills/sketch/references/composition-generative-recursion-and-accumulators.md +0 -196
  64. package/skills/sketch/references/generic-operations-and-languages.md +0 -134
  65. package/skills/sketch/references/processes-state-and-time.md +0 -171
  66. package/skills/sketch/references/responsibility-and-variation.md +0 -245
@@ -7,6 +7,7 @@ import type {
7
7
  FocusEvent,
8
8
  JudgmentEvent,
9
9
  PendingQuestion,
10
+ ReferenceLoadEvent,
10
11
  RouteEvent,
11
12
  } from "./state.ts";
12
13
 
@@ -14,6 +15,7 @@ type DeveloperMachineEvent =
14
15
  | { type: "ACTIVATION"; event: ActivationEvent }
15
16
  | { type: "FOCUS"; event: FocusEvent }
16
17
  | { type: "ROUTE"; event: RouteEvent }
18
+ | { type: "REFERENCE_LOAD"; event: ReferenceLoadEvent }
17
19
  | { type: "JUDGMENT"; event: JudgmentEvent };
18
20
 
19
21
  export type DeveloperMachineTag =
@@ -82,6 +84,37 @@ function applyRouteContext(
82
84
  };
83
85
  }
84
86
 
87
+ function applyReferenceLoadContext(
88
+ state: DeveloperState,
89
+ event: ReferenceLoadEvent,
90
+ ): DeveloperState {
91
+ const route = state.activeRoute;
92
+ if (!route || route.routeId !== event.routeId) return state;
93
+ const existingIndex = route.loadedReferences.findIndex(
94
+ (reference) => reference.path === event.path,
95
+ );
96
+ const existingReference = route.loadedReferences[existingIndex];
97
+ const loadedReference = {
98
+ path: event.path,
99
+ reason: event.reason,
100
+ contentSha256: event.contentSha256,
101
+ sourceTrace: event.sourceTrace,
102
+ referenceRouteIds: [
103
+ ...new Set([
104
+ ...(existingReference?.referenceRouteIds ?? []),
105
+ ...event.referenceRouteIds,
106
+ ]),
107
+ ],
108
+ };
109
+ const loadedReferences = [...route.loadedReferences];
110
+ if (existingIndex === -1) loadedReferences.push(loadedReference);
111
+ else loadedReferences[existingIndex] = loadedReference;
112
+ return {
113
+ ...state,
114
+ activeRoute: { ...route, loadedReferences },
115
+ };
116
+ }
117
+
85
118
  function questionsAfterJudgment(
86
119
  state: DeveloperState,
87
120
  route: RouteEvent,
@@ -207,6 +240,8 @@ function reduceDeveloperContext(
207
240
  return { ...state, focusedQuestionId: event.questionId };
208
241
  case "route":
209
242
  return applyRouteContext(state, event);
243
+ case "reference-load":
244
+ return applyReferenceLoadContext(state, event);
210
245
  case "judgment":
211
246
  return applyJudgmentContext(state, event);
212
247
  default:
@@ -222,6 +257,8 @@ function machineEvent(event: DeveloperEvent): DeveloperMachineEvent {
222
257
  return { type: "FOCUS", event };
223
258
  case "route":
224
259
  return { type: "ROUTE", event };
260
+ case "reference-load":
261
+ return { type: "REFERENCE_LOAD", event };
225
262
  case "judgment":
226
263
  return { type: "JUDGMENT", event };
227
264
  default:
@@ -252,6 +289,13 @@ function canApplyEvent(
252
289
  (question) => question.gate === "before-implementation",
253
290
  ))
254
291
  );
292
+ case "REFERENCE_LOAD":
293
+ return (
294
+ context.enabled &&
295
+ context.activeRoute?.routeId === event.event.routeId &&
296
+ context.activeRoute.target === event.event.target &&
297
+ context.activeRoute.target !== "implementation"
298
+ );
255
299
  case "JUDGMENT":
256
300
  return (
257
301
  context.enabled && context.activeRoute?.routeId === event.event.routeId
@@ -322,6 +366,7 @@ export const developerMachine = machineSetup.createMachine({
322
366
  ACTIVATION: { guard: "eventAllowed", actions: "applyEvent" },
323
367
  FOCUS: { guard: "eventAllowed", actions: "applyEvent" },
324
368
  ROUTE: { guard: "eventAllowed", actions: "applyEvent" },
369
+ REFERENCE_LOAD: { guard: "eventAllowed", actions: "applyEvent" },
325
370
  JUDGMENT: { guard: "eventAllowed", actions: "applyEvent" },
326
371
  },
327
372
  states: {
@@ -15,11 +15,16 @@ Structural move: one concrete rearrangement
15
15
  Pressure: why the accepted task needs or benefits from it now
16
16
  Baseline evidence: the cheapest relevant check that is currently green
17
17
  Affected surface: files, callers, persisted forms, or public boundaries
18
+ Runtime semantics: effects, evaluation/order, numeric representation, failure, termination
19
+ Resource claim: unchanged / deliberately changed / not part of preserved behavior
18
20
  Stop: the next stable, pausable state
19
21
  ```
20
22
 
21
23
  If the behavior claim, timing, or candidate is still under judgment, close or
22
24
  pause the implementation route and route that question to the appropriate leaf.
25
+ The behavior/structure distinction is observer-relative. Equal return values do
26
+ not make a movement structural when it changes effects, exception timing,
27
+ ordering, resource use, persistence, compatibility, or another admitted observer.
23
28
 
24
29
  ## Smallest Green Transformation
25
30
 
@@ -40,6 +45,52 @@ new shape parses
40
45
  For movement across a boundary, update one sender or caller at a time when the
41
46
  old and new forms can safely coexist. Confirm that each intermediate state is
42
47
  valid. Do not rely on a final large diff to explain which step changed behavior.
48
+ A forwarding interface over the old implementation can be a temporary seam, but
49
+ it is not evidence that the interface is a stable public abstraction.
50
+
51
+ For suspected dead code, combine static references, dynamic reachability, and a
52
+ scoped observation window appropriate to rare jobs, reflection, configuration,
53
+ and external entry points. A log with no hits narrows uncertainty; it does not
54
+ prove absence. Delete in a separately reversible step and retain the recovery
55
+ path and residual risk.
56
+
57
+ ## Derive Completely, Then Simplify
58
+
59
+ When a change affects cases or data shape, first make the complete case space
60
+ explicit. Derive branches, selectors, candidate recursive calls, and failure
61
+ cases from the accepted model; only then merge clauses or remove redundant guards
62
+ with a stated preserving law. Guessing the compact version can omit a case while
63
+ remaining green on a narrow suite.
64
+
65
+ Propagate a changed data definition through:
66
+
67
+ ```text
68
+ data and persisted examples
69
+ -> behavior examples
70
+ -> templates and collaborations
71
+ -> implementations
72
+ -> tests and properties
73
+ -> callers, serializers, and public compatibility surfaces
74
+ ```
75
+
76
+ A local type edit is not a stable landing while an old accepted form remains
77
+ unhandled. Conversely, a structurally available selector or recursion may be
78
+ absent from the final body when purpose explicitly proves it irrelevant.
79
+
80
+ ## Runtime-Semantic Boundary
81
+
82
+ A source-level rearrangement is not behavior-preserving merely because an
83
+ algebraic identity holds mathematically. Check whether the move changes:
84
+
85
+ - short-circuiting, exception timing, effects, or repeated external reads;
86
+ - floating-point association, overflow, underflow, or non-finite propagation;
87
+ - collection order, duplicate handling, sharing, or mutation;
88
+ - termination, branch starvation, or failure-versus-divergence behavior;
89
+ - time, stack, allocation, or query/update complexity when resource behavior is
90
+ part of the accepted claim.
91
+
92
+ If any item is intentionally changed, split that claim from the structural move
93
+ and route it explicitly.
43
94
 
44
95
  ## Evidence Rhythm
45
96
 
@@ -49,9 +100,16 @@ Within the routed movement:
49
100
  2. inspect the diff for mixed behavior and structural changes;
50
101
  3. reduce the step when the failure cannot be explained locally;
51
102
  4. return to the declared green, deployable stable landing;
52
- 5. close the implementation route there and let Developer re-observe and route
103
+ 5. distinguish local integration evidence from production deployment and
104
+ external-effect evidence;
105
+ 6. close the implementation route there and let Developer re-observe and route
53
106
  the next question instead of following a predetermined final design.
54
107
 
108
+ Separate commits or pull requests can expose structural purpose, but repository
109
+ policy, review latency, deployment independence, and team evidence determine the
110
+ actual batch. Never infer “absolute safety” from a small diff or omit review only
111
+ because a change is labeled a tidying.
112
+
55
113
  A passing test is useful only when it exercises the preserved behavior. When no
56
114
  cheap verifier exists, keep the movement smaller and record the residual risk.
57
115
 
@@ -139,12 +197,33 @@ The protocol is being misused when:
139
197
  ## Source Trace
140
198
 
141
199
  - Sandi Metz, Katrina Owen, and TJ Stankus, *99 Bottles of OOP*, Second
142
- Edition, version 2.2.2, 2024: chapters 3-5 on methodical transformations,
143
- flocking, gradual refactoring, stable landings, and sender-by-sender movement.
200
+ Edition, version 2.2.2, 2024: Chapters 3-5, pp. 51-137, especially
201
+ pp. 58-71, 84-101, and 113-125, on methodical transformations, flocking,
202
+ gradual refactoring, stable landings, and sender-by-sender movement.
144
203
  - Kent Beck, *Tidy First?: A Personal Exercise in Empirical Software Design*,
145
- O'Reilly Media, 2023: separating behavior and structure, keeping tidyings
146
- small, and managing structural change as optionality.
204
+ First Edition, Second Release, O'Reilly Media, 2025-12-12:
205
+ Part I, Chapters 1-15, pp. 3-32, for small structural moves, reversible seams,
206
+ dead-code observation, dependency-preserving reorder, extraction, inlining,
207
+ and communication; Part II, Chapters 16-20, pp. 35-50, for separating change
208
+ purposes, chaining, batches, rhythm, and untangling; and Chapter 28,
209
+ pp. 75-76, for reversibility pressure. The protocol narrows the source's
210
+ behavior observer and qualifies its PR, review, deployment, telemetry, and
211
+ absolute-safety claims; production recovery remains a Developer adaptation.
147
212
  - Matthias Felleisen, Robert Bruce Findler, Matthew Flatt, and Shriram
148
- Krishnamurthi, *How to Design Programs, Second Edition*, MIT Press, 2018:
149
- iterative refinement and propagating a changed data design through dependent
150
- artifacts.
213
+ Krishnamurthi, *How to Design Programs*, official living build 9.2.0.3,
214
+ released 2026-05-28 and audited 2026-05-28:
215
+ Chapter 19
216
+ and Chapter 20
217
+ for model stabilization and staged simplification;
218
+ Chapter 23
219
+ for exhaustive case derivation followed by algebraic simplification;
220
+ Intermezzo 4
221
+ for order-sensitive inexact arithmetic;
222
+ Intermezzo 5
223
+ for resource-class changes; and
224
+ Chapter 31,
225
+ Chapter 32,
226
+ Chapter 33,
227
+ and Chapter 34
228
+ for accumulator transformations that may change order, totality, time, and
229
+ space.
@@ -1,74 +1,352 @@
1
- import { realpathSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync, type Dirent } from "node:fs";
3
+ import { readdir, readFile } from "node:fs/promises";
3
4
  import { isAbsolute, relative, resolve } from "node:path";
4
5
 
5
6
  import {
6
- DEFAULT_MAX_BYTES,
7
- DEFAULT_MAX_LINES,
8
- stripFrontmatter,
9
- type Skill,
7
+ DEFAULT_MAX_BYTES,
8
+ DEFAULT_MAX_LINES,
9
+ stripFrontmatter,
10
+ type Skill,
10
11
  } from "@earendil-works/pi-coding-agent";
11
12
 
12
13
  const METHOD_OUTPUT_OVERHEAD_BYTES = 4096;
13
14
  const METHOD_OUTPUT_OVERHEAD_LINES = 20;
15
+ const REFERENCE_PATH_PATTERN = /^references\/[^/]+\.md$/u;
16
+
17
+ export interface LoadedSkillReference {
18
+ path: string;
19
+ filePath: string;
20
+ content: string;
21
+ contentSha256: string;
22
+ sourceTrace: string;
23
+ }
24
+
25
+ export interface SkillReferenceRoute {
26
+ id: string;
27
+ question: string;
28
+ trigger: string;
29
+ methodStep: string;
30
+ references: string[];
31
+ readOrder: "any" | "listed";
32
+ artifacts: string[];
33
+ stop: string;
34
+ separateWhen: string;
35
+ }
36
+
37
+ export interface SkillReferenceExemption {
38
+ when: string;
39
+ evidence: string[];
40
+ }
41
+
42
+ export interface SkillReferencePolicy {
43
+ routes: SkillReferenceRoute[];
44
+ exemption?: SkillReferenceExemption;
45
+ contentSha256?: string;
46
+ }
14
47
 
15
48
  function escapeAttribute(value: string): string {
16
- return value
17
- .replaceAll("&", "&")
18
- .replaceAll('"', """)
19
- .replaceAll("<", "&lt;")
20
- .replaceAll(">", "&gt;");
49
+ return value
50
+ .replaceAll("&", "&amp;")
51
+ .replaceAll('"', "&quot;")
52
+ .replaceAll("<", "&lt;")
53
+ .replaceAll(">", "&gt;");
21
54
  }
22
55
 
23
56
  export function isWithinRoot(root: string, path: string): boolean {
24
- const canonical = (value: string) => {
25
- try {
26
- return realpathSync.native(value);
27
- } catch {
28
- return resolve(value);
29
- }
30
- };
31
- const relation = relative(canonical(root), canonical(path));
32
- return relation === "" || (!relation.startsWith("..") && !isAbsolute(relation));
57
+ const canonical = (value: string) => {
58
+ try {
59
+ return realpathSync.native(value);
60
+ } catch {
61
+ return resolve(value);
62
+ }
63
+ };
64
+ const relation = relative(canonical(root), canonical(path));
65
+ return (
66
+ relation === "" || (!relation.startsWith("..") && !isAbsolute(relation))
67
+ );
33
68
  }
34
69
 
35
70
  export function availablePackageSkills(
36
- loadedSkills: Skill[],
37
- skillsRoot: string,
71
+ loadedSkills: Skill[],
72
+ skillsRoot: string,
38
73
  ): Map<string, Skill> {
39
- const available = new Map<string, Skill>();
74
+ const available = new Map<string, Skill>();
75
+
76
+ for (const skill of loadedSkills) {
77
+ if (skill.disableModelInvocation) continue;
78
+ if (!isWithinRoot(skillsRoot, skill.filePath)) continue;
79
+ if (available.has(skill.name))
80
+ throw new Error(
81
+ `Duplicate Pi-loaded Developer skill name: ${skill.name}`,
82
+ );
83
+ available.set(skill.name, skill);
84
+ }
85
+
86
+ return available;
87
+ }
88
+
89
+ export async function skillReferencePaths(skill: Skill): Promise<string[]> {
90
+ const referencesRoot = resolve(skill.baseDir, "references");
91
+ let entries: Dirent[];
92
+ try {
93
+ entries = await readdir(referencesRoot, { withFileTypes: true });
94
+ } catch (error) {
95
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
96
+ throw error;
97
+ }
98
+
99
+ const paths: string[] = [];
100
+ for (const entry of entries) {
101
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
102
+ if (!isWithinRoot(referencesRoot, resolve(referencesRoot, entry.name)))
103
+ continue;
104
+ paths.push(`references/${entry.name}`);
105
+ }
106
+ return paths.sort((left, right) => left.localeCompare(right));
107
+ }
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
111
+ }
112
+
113
+ function parseNonEmptyStrings(value: unknown, label: string): string[] {
114
+ if (
115
+ !Array.isArray(value) ||
116
+ value.length === 0 ||
117
+ !value.every((item) => typeof item === "string" && item.trim().length > 0)
118
+ ) {
119
+ throw new Error(`${label} must be a non-empty string array.`);
120
+ }
121
+ const strings = value.map((item) => item.trim());
122
+ if (new Set(strings).size !== strings.length) {
123
+ throw new Error(`${label} must not contain duplicates.`);
124
+ }
125
+ return strings;
126
+ }
127
+
128
+ function parseReferenceRoutes(
129
+ skill: Skill,
130
+ value: unknown,
131
+ catalog: string[],
132
+ ): SkillReferenceRoute[] {
133
+ if (!Array.isArray(value) || value.length === 0) {
134
+ throw new Error(
135
+ `Reference policy for ${skill.name} must contain at least one route.`,
136
+ );
137
+ }
138
+ const routes: SkillReferenceRoute[] = value.map((candidate, index) => {
139
+ if (
140
+ !isRecord(candidate) ||
141
+ typeof candidate.id !== "string" ||
142
+ !/^[a-z][a-z0-9-]*$/u.test(candidate.id) ||
143
+ typeof candidate.question !== "string" ||
144
+ candidate.question.trim().length === 0 ||
145
+ typeof candidate.trigger !== "string" ||
146
+ candidate.trigger.trim().length === 0 ||
147
+ typeof candidate.method_step !== "string" ||
148
+ candidate.method_step.trim().length === 0 ||
149
+ typeof candidate.stop !== "string" ||
150
+ candidate.stop.trim().length === 0 ||
151
+ typeof candidate.separate_when !== "string" ||
152
+ candidate.separate_when.trim().length === 0 ||
153
+ (candidate.read_order !== undefined &&
154
+ candidate.read_order !== "any" &&
155
+ candidate.read_order !== "listed")
156
+ ) {
157
+ throw new Error(
158
+ `Reference policy for ${skill.name} has an invalid route at index ${index}.`,
159
+ );
160
+ }
161
+ const references = parseNonEmptyStrings(
162
+ candidate.references,
163
+ `Reference route ${candidate.id} references`,
164
+ );
165
+ if (
166
+ references.some(
167
+ (path) => !REFERENCE_PATH_PATTERN.test(path) || !catalog.includes(path),
168
+ )
169
+ ) {
170
+ throw new Error(
171
+ `Reference route ${candidate.id} for ${skill.name} names an unavailable reference.`,
172
+ );
173
+ }
174
+ return {
175
+ id: candidate.id,
176
+ question: candidate.question.trim(),
177
+ trigger: candidate.trigger.trim(),
178
+ methodStep: candidate.method_step.trim(),
179
+ references,
180
+ readOrder: candidate.read_order === "listed" ? "listed" : "any",
181
+ artifacts: parseNonEmptyStrings(
182
+ candidate.artifacts,
183
+ `Reference route ${candidate.id} artifacts`,
184
+ ),
185
+ stop: candidate.stop.trim(),
186
+ separateWhen: candidate.separate_when.trim(),
187
+ };
188
+ });
189
+ if (new Set(routes.map((route) => route.id)).size !== routes.length) {
190
+ throw new Error(
191
+ `Reference policy for ${skill.name} must use unique route IDs.`,
192
+ );
193
+ }
194
+ const covered = new Set(routes.flatMap((route) => route.references));
195
+ const unmapped = catalog.filter((path) => !covered.has(path));
196
+ if (unmapped.length > 0) {
197
+ throw new Error(
198
+ `Reference policy for ${skill.name} leaves references unrouted: ${unmapped.join(", ")}.`,
199
+ );
200
+ }
201
+ return routes;
202
+ }
203
+
204
+ function parseReferencePolicy(
205
+ skill: Skill,
206
+ source: string,
207
+ catalog: string[],
208
+ ): Omit<SkillReferencePolicy, "contentSha256"> {
209
+ let parsed: unknown;
210
+ try {
211
+ parsed = JSON.parse(source);
212
+ } catch (error) {
213
+ throw new Error(`Invalid reference policy JSON for ${skill.name}.`, {
214
+ cause: error,
215
+ });
216
+ }
217
+ if (
218
+ !isRecord(parsed) ||
219
+ parsed.version !== 2 ||
220
+ !isRecord(parsed.exemption) ||
221
+ typeof parsed.exemption.when !== "string" ||
222
+ parsed.exemption.when.trim().length === 0
223
+ ) {
224
+ throw new Error(
225
+ `Reference policy for ${skill.name} must declare version 2, judgment-integrated routes, and exemption criteria.`,
226
+ );
227
+ }
228
+ return {
229
+ routes: parseReferenceRoutes(skill, parsed.routes, catalog),
230
+ exemption: {
231
+ when: parsed.exemption.when.trim(),
232
+ evidence: parseNonEmptyStrings(
233
+ parsed.exemption.evidence,
234
+ `Reference policy ${skill.name} exemption evidence`,
235
+ ),
236
+ },
237
+ };
238
+ }
239
+
240
+ export async function loadSkillReferencePolicy(
241
+ skill: Skill,
242
+ availableReferences?: string[],
243
+ ): Promise<SkillReferencePolicy> {
244
+ let catalog = availableReferences;
245
+ if (!catalog) catalog = await skillReferencePaths(skill);
246
+ const policyPath = resolve(skill.baseDir, "reference-policy.json");
247
+ let source: string;
248
+ try {
249
+ source = await readFile(policyPath, "utf8");
250
+ } catch (error) {
251
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
252
+ if (catalog.length > 0) {
253
+ throw new Error(
254
+ `Developer skill ${skill.name} has references but no reference-policy.json.`,
255
+ );
256
+ }
257
+ return { routes: [] };
258
+ }
259
+ throw error;
260
+ }
261
+
262
+ return {
263
+ ...parseReferencePolicy(skill, source, catalog),
264
+ contentSha256: createHash("sha256")
265
+ .update(source.trim(), "utf8")
266
+ .digest("hex"),
267
+ };
268
+ }
269
+
270
+ function extractSourceTrace(content: string): string {
271
+ const lines = content.split(/\r?\n/);
272
+ const start = lines.findIndex((line) => line.trim() === "## Source Trace");
273
+ if (start === -1) return "";
274
+ const end = lines.findIndex(
275
+ (line, index) => index > start && /^##\s+/u.test(line),
276
+ );
277
+ return lines
278
+ .slice(start + 1, end === -1 ? undefined : end)
279
+ .join("\n")
280
+ .trim();
281
+ }
282
+
283
+ export async function loadSkillReference(
284
+ skill: Skill,
285
+ requestedPath: string,
286
+ ): Promise<LoadedSkillReference> {
287
+ const path = requestedPath.replaceAll("\\", "/").replace(/^\.\/+/, "");
288
+ if (!REFERENCE_PATH_PATTERN.test(path)) {
289
+ throw new Error(
290
+ `Developer references must use a direct skill-relative path such as references/example.md: ${requestedPath}`,
291
+ );
292
+ }
293
+
294
+ const available = await skillReferencePaths(skill);
295
+ if (!available.includes(path)) {
296
+ throw new Error(
297
+ `Reference ${path} is unavailable for ${skill.name}. Available references: ${available.join(", ") || "none"}.`,
298
+ );
299
+ }
40
300
 
41
- for (const skill of loadedSkills) {
42
- if (skill.disableModelInvocation) continue;
43
- if (!isWithinRoot(skillsRoot, skill.filePath)) continue;
44
- if (available.has(skill.name)) throw new Error(`Duplicate Pi-loaded Developer skill name: ${skill.name}`);
45
- available.set(skill.name, skill);
46
- }
301
+ const filePath = resolve(skill.baseDir, path);
302
+ if (!isWithinRoot(resolve(skill.baseDir, "references"), filePath)) {
303
+ throw new Error(
304
+ `Reference path escapes the ${skill.name} references directory.`,
305
+ );
306
+ }
307
+ const source = await readFile(filePath, "utf8");
308
+ const content = source.trim();
309
+ const contentBytes = Buffer.byteLength(content, "utf8");
310
+ const contentLines = content.split(/\r?\n/).length;
311
+ if (
312
+ contentBytes > DEFAULT_MAX_BYTES - METHOD_OUTPUT_OVERHEAD_BYTES ||
313
+ contentLines > DEFAULT_MAX_LINES - METHOD_OUTPUT_OVERHEAD_LINES
314
+ ) {
315
+ throw new Error(
316
+ `Developer reference ${path} is too large for safe forced loading. Split it into focused direct references.`,
317
+ );
318
+ }
47
319
 
48
- return available;
320
+ return {
321
+ path,
322
+ filePath,
323
+ content,
324
+ contentSha256: createHash("sha256").update(content, "utf8").digest("hex"),
325
+ sourceTrace: extractSourceTrace(content),
326
+ };
49
327
  }
50
328
 
51
329
  export async function renderSkillMethod(skill: Skill): Promise<string> {
52
- const source = await readFile(skill.filePath, "utf8");
53
- const body = stripFrontmatter(source).trim();
54
- const bodyBytes = Buffer.byteLength(body, "utf8");
55
- const bodyLines = body.split(/\r?\n/).length;
56
- if (
57
- bodyBytes > DEFAULT_MAX_BYTES - METHOD_OUTPUT_OVERHEAD_BYTES ||
58
- bodyLines > DEFAULT_MAX_LINES - METHOD_OUTPUT_OVERHEAD_LINES
59
- ) {
60
- throw new Error(
61
- `Developer skill ${skill.name} is too large for safe forced loading. Move detail into relative references before routing it.`,
62
- );
63
- }
64
- const name = escapeAttribute(skill.name);
65
- const location = escapeAttribute(skill.filePath);
66
- const baseDir = escapeAttribute(skill.baseDir);
67
- return [
68
- `<developer-method name="${name}" location="${location}" base-dir="${baseDir}">`,
69
- body,
70
- "</developer-method>",
71
- "",
72
- `Resolve relative references from ${skill.baseDir}.`,
73
- ].join("\n");
330
+ const source = await readFile(skill.filePath, "utf8");
331
+ const body = stripFrontmatter(source).trim();
332
+ const bodyBytes = Buffer.byteLength(body, "utf8");
333
+ const bodyLines = body.split(/\r?\n/).length;
334
+ if (
335
+ bodyBytes > DEFAULT_MAX_BYTES - METHOD_OUTPUT_OVERHEAD_BYTES ||
336
+ bodyLines > DEFAULT_MAX_LINES - METHOD_OUTPUT_OVERHEAD_LINES
337
+ ) {
338
+ throw new Error(
339
+ `Developer skill ${skill.name} is too large for safe forced loading. Move detail into relative references before routing it.`,
340
+ );
341
+ }
342
+ const name = escapeAttribute(skill.name);
343
+ const location = escapeAttribute(skill.filePath);
344
+ const baseDir = escapeAttribute(skill.baseDir);
345
+ return [
346
+ `<developer-method name="${name}" location="${location}" base-dir="${baseDir}">`,
347
+ body,
348
+ "</developer-method>",
349
+ "",
350
+ `Resolve relative references from ${skill.baseDir}.`,
351
+ ].join("\n");
74
352
  }