@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.
- package/README.md +215 -372
- package/REFERENCE_ROUTING.md +243 -0
- package/extensions/compaction-language.ts +303 -0
- package/extensions/developer.ts +607 -20
- package/extensions/machine.ts +45 -0
- package/extensions/references/behavior-preserving-structural-change.md +87 -8
- package/extensions/skills.ts +330 -52
- package/extensions/state.ts +227 -2
- package/package.json +9 -3
- package/skills/abstraction-review/SKILL.md +37 -23
- package/skills/abstraction-review/reference-policy.json +66 -0
- package/skills/abstraction-review/references/field-card.md +113 -168
- package/skills/abstraction-review/references/repair-table.md +66 -80
- package/skills/abstraction-review/references/worked-examples.md +79 -256
- package/skills/model/SKILL.md +31 -13
- package/skills/model/reference-policy.json +96 -0
- package/skills/model/references/contract-and-replacement-models.md +112 -0
- package/skills/model/references/logic-query-semantics.md +89 -0
- package/skills/model/references/planning-models.md +75 -0
- package/skills/model/references/problem-modeling.md +143 -276
- package/skills/model/references/proof-obligations.md +71 -0
- package/skills/model/references/relational-constraint-models.md +110 -0
- package/skills/model/references/solver-result-boundaries.md +83 -0
- package/skills/model/references/temporal-behavior-models.md +106 -0
- package/skills/naming-judgment/SKILL.md +17 -4
- package/skills/naming-judgment/reference-policy.json +29 -0
- package/skills/naming-judgment/references/domain-naming.md +29 -10
- package/skills/schedule/SKILL.md +16 -6
- package/skills/schedule/reference-policy.json +29 -0
- package/skills/schedule/references/structural-change-timing.md +81 -18
- package/skills/signal/SKILL.md +17 -6
- package/skills/signal/reference-policy.json +29 -0
- package/skills/signal/references/structural-movement.md +87 -14
- package/skills/sketch/SKILL.md +38 -42
- package/skills/sketch/reference-policy.json +212 -0
- package/skills/sketch/references/accumulator-invariants.md +125 -0
- package/skills/sketch/references/closure-and-conventional-interfaces.md +87 -0
- package/skills/sketch/references/composition-by-wishes.md +69 -0
- package/skills/sketch/references/data-driven-design.md +99 -22
- package/skills/sketch/references/data-shape-template-catalog.md +58 -19
- package/skills/sketch/references/design-levels-and-boundaries.md +133 -0
- package/skills/sketch/references/earned-abstraction.md +75 -0
- package/skills/sketch/references/generative-recursion.md +122 -0
- package/skills/sketch/references/generic-dispatch-systems.md +75 -0
- package/skills/sketch/references/language-semantics.md +82 -0
- package/skills/sketch/references/meaning-preserving-conversions.md +69 -0
- package/skills/sketch/references/process-shape-and-resources.md +157 -0
- package/skills/sketch/references/representation-barriers.md +73 -0
- package/skills/sketch/references/responsibility-and-collaboration.md +108 -0
- package/skills/sketch/references/runtime-and-compilation.md +84 -0
- package/skills/sketch/references/selection-and-creation.md +76 -0
- package/skills/sketch/references/state-history-and-order.md +126 -0
- package/skills/sketch/references/type-transitions.md +54 -0
- package/skills/sketch/references/variation-roles.md +59 -0
- package/skills/verify/SKILL.md +17 -6
- package/skills/verify/reference-policy.json +30 -0
- package/skills/verify/references/verifier-selection-and-pass-but-wrong.md +152 -192
- package/SOURCES.md +0 -104
- package/skills/abstraction-review/references/recipe-cards.md +0 -322
- package/skills/model/references/worked-models-and-specialized-techniques.md +0 -218
- package/skills/sketch/references/abstraction-barriers-and-closure.md +0 -160
- package/skills/sketch/references/abstraction-composition-and-state.md +0 -184
- package/skills/sketch/references/composition-generative-recursion-and-accumulators.md +0 -196
- package/skills/sketch/references/generic-operations-and-languages.md +0 -134
- package/skills/sketch/references/processes-state-and-time.md +0 -171
- package/skills/sketch/references/responsibility-and-variation.md +0 -245
package/extensions/machine.ts
CHANGED
|
@@ -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.
|
|
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:
|
|
143
|
-
|
|
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,
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
package/extensions/skills.ts
CHANGED
|
@@ -1,74 +1,352 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
return value
|
|
50
|
+
.replaceAll("&", "&")
|
|
51
|
+
.replaceAll('"', """)
|
|
52
|
+
.replaceAll("<", "<")
|
|
53
|
+
.replaceAll(">", ">");
|
|
21
54
|
}
|
|
22
55
|
|
|
23
56
|
export function isWithinRoot(root: string, path: string): boolean {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
71
|
+
loadedSkills: Skill[],
|
|
72
|
+
skillsRoot: string,
|
|
38
73
|
): Map<string, Skill> {
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|