@skillerr/core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,977 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { DEFAULT_SKILL_POLICY, CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, assessSkillContract, isValidAgentHost, recipeToSkillSource, } from "@skillerr/protocol";
3
+ import { packSkill, finalizeManifest, buildFileMap } from "./pack.js";
4
+ const PLACEHOLDER_RE = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}|<([A-Z][A-Z0-9_]+)>|\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
5
+ const SECRET_PATTERNS = [
6
+ /\b(?:sk|pk|api)[_-][A-Za-z0-9]{8,}\b/g,
7
+ /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
8
+ /\b[A-Za-z0-9+/]{40,}={0,2}\b/g,
9
+ ];
10
+ const GENERALIZABLE_PATTERNS = [
11
+ {
12
+ re: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
13
+ name: "email",
14
+ reason: "Email addresses vary per deployment",
15
+ },
16
+ {
17
+ re: /\bhttps?:\/\/[^\s)]+/gi,
18
+ name: "base_url",
19
+ reason: "Service URLs are environment-specific",
20
+ },
21
+ {
22
+ re: /\b(?:sk|pk|api)[_-][A-Za-z0-9]{8,}\b/g,
23
+ name: "api_credential_ref",
24
+ reason: "Credentials must be secret refs, never embedded",
25
+ },
26
+ ];
27
+ function shortId(prefix) {
28
+ return `${prefix}_${createHash("sha256").update(randomUUID()).digest("hex").slice(0, 12)}`;
29
+ }
30
+ function knowledgeTypeFor(section) {
31
+ switch (section.type) {
32
+ case "decision":
33
+ return "decision";
34
+ case "tradeoff":
35
+ return "tradeoff";
36
+ case "lesson":
37
+ return "lesson";
38
+ case "correction_note":
39
+ return "correction";
40
+ case "requirement":
41
+ case "intent":
42
+ return "constraint";
43
+ case "reference":
44
+ case "resource":
45
+ case "doc":
46
+ return "reference";
47
+ default:
48
+ return "rule";
49
+ }
50
+ }
51
+ function slug(name) {
52
+ return (name
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]+/g, "_")
55
+ .replace(/^_|_$/g, "")
56
+ .slice(0, 40) || "value");
57
+ }
58
+ /** Scrub likely secrets from text before packaging. */
59
+ export function redactSecrets(text) {
60
+ let out = text;
61
+ for (const re of SECRET_PATTERNS) {
62
+ re.lastIndex = 0;
63
+ out = out.replace(re, "{{secret_ref}}");
64
+ }
65
+ return out;
66
+ }
67
+ export class CompileRefusalError extends Error {
68
+ kind = "compile_refused";
69
+ profile;
70
+ missing;
71
+ hints;
72
+ completeness;
73
+ constructor(report) {
74
+ super(`compile_refused (${report.profile}): missing [${report.missing.join(", ")}]. ${report.hints.join(" ")}`);
75
+ this.name = "CompileRefusalError";
76
+ this.profile = report.profile;
77
+ this.missing = report.missing;
78
+ this.hints = report.hints;
79
+ this.completeness = report;
80
+ }
81
+ }
82
+ const HINTS = {
83
+ agent_context: "Set SKILL_HOST to an AI host (cursor, ollama, lmstudio, llama-cpp, custom-agent, …). This is asserted provenance, not cryptographic proof.",
84
+ intent: "Add an intent/summary section describing what this skill is for.",
85
+ sections: "Agent must propose at least one section (decision, integration, workflow, …).",
86
+ workflow: "Need actionable workflow content (integration, prompt, implementation, or workflow_note).",
87
+ knowledge_or_prompts: "Need knowledge sections or prompt templates — not an empty package.",
88
+ inputs_declared: "Declare typed inputs or set SkillSource.inputs_declared='none'. Placeholders like {{base_url}} become inputs.",
89
+ journey: "Provide a redacted journey summary of the human+AI work (no raw chat, no secrets).",
90
+ generation_usage: "Optional but recommended: report token usage used to create this skill.",
91
+ human_approvals: "Human must approve inferred inputs/permissions before release mint.",
92
+ };
93
+ export function assessCompleteness(source, opts) {
94
+ const present = [];
95
+ const missing = [];
96
+ const check = (part, ok) => {
97
+ if (ok)
98
+ present.push(part);
99
+ else
100
+ missing.push(part);
101
+ };
102
+ check("agent_context", isValidAgentHost(source.agent.host));
103
+ check("intent", Boolean((source.intent ?? source.summary ?? source.title)?.trim().length));
104
+ check("sections", source.sections.length >= 1);
105
+ check("knowledge_or_prompts", opts.hasKnowledge || source.prompts.length > 0);
106
+ check("workflow", opts.hasWorkflowAction);
107
+ check("inputs_declared", opts.hasInputsDeclared);
108
+ check("journey", Boolean(source.journey?.summary?.trim()) && source.journey.redacted !== false);
109
+ if (source.generation_usage?.total_tokens || source.generation_usage?.input_tokens) {
110
+ present.push("generation_usage");
111
+ }
112
+ if (opts.profile === "release") {
113
+ check("human_approvals", opts.pendingApprovals.length === 0);
114
+ // generation_usage recommended but not hard-required for release
115
+ }
116
+ else {
117
+ // continuity: drop soft requirements from missing
118
+ const soft = new Set([
119
+ "workflow",
120
+ "inputs_declared",
121
+ "human_approvals",
122
+ "generation_usage",
123
+ ]);
124
+ for (let i = missing.length - 1; i >= 0; i--) {
125
+ if (soft.has(missing[i]))
126
+ missing.splice(i, 1);
127
+ }
128
+ }
129
+ const requiredMissing = opts.profile === "continuity"
130
+ ? missing.filter((m) => ["agent_context", "sections", "intent", "journey", "knowledge_or_prompts"].includes(m))
131
+ : missing.filter((m) => m !== "generation_usage");
132
+ return {
133
+ kind: "completeness_report",
134
+ profile: opts.profile,
135
+ complete: requiredMissing.length === 0,
136
+ present,
137
+ missing: requiredMissing,
138
+ hints: requiredMissing.map((m) => HINTS[m] ?? `Complete the ${m} declaration.`),
139
+ };
140
+ }
141
+ function sectionHasWorkflowAction(sections) {
142
+ return sections.some((s) => ["integration", "prompt", "implementation_note", "workflow_note", "code", "config"].includes(s.type));
143
+ }
144
+ function declaredItems(declaration) {
145
+ if (!declaration)
146
+ return [];
147
+ return declaration.status === "specified" ? declaration.items ?? [] : [];
148
+ }
149
+ function contractCompleteness(assessment, profile) {
150
+ const missing = [...new Set(assessment.issues.map((issue) => {
151
+ const root = issue.field.split(".")[0];
152
+ return (root === "contract" ? "semantic_contract" : root);
153
+ }))];
154
+ const contractParts = [
155
+ "semantic_contract",
156
+ "intent",
157
+ "triggers",
158
+ "inputs",
159
+ "preconditions",
160
+ "steps",
161
+ "branches",
162
+ "human_decisions",
163
+ "capabilities",
164
+ "permissions",
165
+ "forbidden_actions",
166
+ "outputs",
167
+ "recovery",
168
+ "verification",
169
+ "corrections",
170
+ "provenance",
171
+ ];
172
+ return {
173
+ kind: "completeness_report",
174
+ profile,
175
+ complete: assessment.complete,
176
+ present: contractParts.filter((part) => !missing.includes(part)),
177
+ missing,
178
+ hints: assessment.issues.map((issue) => `${issue.field}: ${issue.fix}`),
179
+ };
180
+ }
181
+ function compileContractStep(step) {
182
+ const base = {
183
+ id: step.id,
184
+ title: step.title,
185
+ optional: step.optional,
186
+ next: step.next,
187
+ on_fail: step.on_failure,
188
+ };
189
+ switch (step.kind) {
190
+ case "instruct":
191
+ return { ...base, kind: "instruct", text: step.instruction ?? "" };
192
+ case "prompt":
193
+ return { ...base, kind: "prompt", template: step.instruction ?? "" };
194
+ case "tool":
195
+ return {
196
+ ...base,
197
+ kind: "tool",
198
+ capability: step.capability ?? "",
199
+ arguments: step.arguments,
200
+ argument_bindings: step.argument_bindings,
201
+ result_as: step.result_as,
202
+ };
203
+ case "transform":
204
+ return {
205
+ ...base,
206
+ kind: "transform",
207
+ expression: step.instruction ?? "identity",
208
+ result_as: step.result_as,
209
+ };
210
+ case "checkpoint":
211
+ return { ...base, kind: "checkpoint", message: step.instruction };
212
+ case "human_decision":
213
+ return {
214
+ ...base,
215
+ kind: "human_decision",
216
+ prompt: step.instruction ?? step.decision ?? step.title,
217
+ result_as: step.result_as,
218
+ };
219
+ case "verify":
220
+ return { ...base, kind: "verify", assertions: step.assertions ?? [] };
221
+ case "emit":
222
+ return {
223
+ ...base,
224
+ kind: "emit",
225
+ output: step.output ?? "",
226
+ from: step.from ?? "",
227
+ };
228
+ }
229
+ }
230
+ function compileNativeContract(source, opts, profile) {
231
+ const contract = source.contract;
232
+ if (!isValidAgentHost(source.agent.host) || !source.journey?.summary?.trim()) {
233
+ const missing = [];
234
+ const hints = [];
235
+ if (!isValidAgentHost(source.agent.host)) {
236
+ missing.push("agent_context");
237
+ hints.push(HINTS.agent_context);
238
+ }
239
+ if (!source.journey?.summary?.trim()) {
240
+ missing.push("journey");
241
+ hints.push(HINTS.journey);
242
+ }
243
+ throw new CompileRefusalError({
244
+ kind: "completeness_report",
245
+ profile,
246
+ complete: false,
247
+ present: [],
248
+ missing,
249
+ hints,
250
+ });
251
+ }
252
+ const assessment = assessSkillContract(contract, profile);
253
+ const completeness = contractCompleteness(assessment, profile);
254
+ if (!assessment.complete && !opts.allow_incomplete && profile === "release") {
255
+ throw new CompileRefusalError(completeness);
256
+ }
257
+ const skillId = opts.skill_id ?? shortId("skl");
258
+ const inputs = declaredItems(contract.inputs).map((input) => ({
259
+ name: input.name,
260
+ description: input.description,
261
+ schema: structuredClone(input.schema),
262
+ required: input.required,
263
+ ...(input.default !== undefined ? { default: structuredClone(input.default) } : {}),
264
+ sensitivity: input.sensitivity,
265
+ source: input.source,
266
+ ask_when: input.ask_when,
267
+ approval: input.approval,
268
+ // Contract review approves the slot definition; human_before_use remains a runtime gate.
269
+ approved: true,
270
+ }));
271
+ const outputs = declaredItems(contract.outputs).map((output) => ({
272
+ name: output.name,
273
+ description: output.description,
274
+ schema: structuredClone(output.schema),
275
+ required: output.required,
276
+ media_type: output.media_type,
277
+ assert: output.assertions,
278
+ }));
279
+ const capabilities = declaredItems(contract.capabilities).map((capability) => ({
280
+ ...structuredClone(capability),
281
+ }));
282
+ const permissions = declaredItems(contract.permissions).map((permission) => ({
283
+ side_effect_class: permission.side_effect_class,
284
+ description: permission.description,
285
+ paths: permission.paths,
286
+ hosts: permission.hosts,
287
+ requires_consent: permission.consent === "explicit_human",
288
+ }));
289
+ const knowledge = source.sections.map((section) => ({
290
+ kind: "knowledge",
291
+ id: `k_${section.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}`,
292
+ type: knowledgeTypeFor(section),
293
+ title: section.title,
294
+ body: redactSecrets(section.body),
295
+ fidelity: "exact",
296
+ pinned: true,
297
+ sensitivity: section.sensitivity === "public" ? "public" : "private",
298
+ provenance: [{ kind: "section", id: section.id, revision: section.revision, hash: source.hash }],
299
+ }));
300
+ const steps = declaredItems(contract.steps).map(compileContractStep);
301
+ for (let i = 0; i < steps.length - 1; i++) {
302
+ if (!steps[i].next)
303
+ steps[i].next = steps[i + 1].id;
304
+ }
305
+ let entrypoint = steps[0]?.id;
306
+ for (const branch of declaredItems(contract.branches)) {
307
+ const branchStep = {
308
+ id: branch.id,
309
+ kind: "branch",
310
+ title: `Conditional branch: ${branch.condition}`,
311
+ cases: [{ when: branch.condition, goto: branch.then }],
312
+ else: branch.otherwise,
313
+ };
314
+ steps.push(branchStep);
315
+ if (branch.after_step) {
316
+ const parent = steps.find((step) => step.id === branch.after_step);
317
+ if (parent)
318
+ parent.next = branch.id;
319
+ }
320
+ }
321
+ for (const decision of declaredItems(contract.human_decisions)) {
322
+ const decisionStep = {
323
+ id: decision.id,
324
+ kind: "human_decision",
325
+ title: decision.prompt,
326
+ prompt: decision.prompt,
327
+ choices: decision.choices,
328
+ result_as: decision.id,
329
+ next: decision.required_before,
330
+ };
331
+ for (const step of steps) {
332
+ if (step.next === decision.required_before)
333
+ step.next = decision.id;
334
+ }
335
+ if (entrypoint === decision.required_before)
336
+ entrypoint = decision.id;
337
+ steps.push(decisionStep);
338
+ }
339
+ for (const input of declaredItems(contract.inputs).filter((item) => item.approval === "human_before_use")) {
340
+ const id = `approve_input_${slug(input.name)}`;
341
+ steps.push({
342
+ id,
343
+ kind: "human_decision",
344
+ title: `Approve input ${input.name}`,
345
+ prompt: `Approve use of input ${input.name}: ${input.description}`,
346
+ choices: ["approve", "deny"],
347
+ result_as: id,
348
+ next: entrypoint,
349
+ });
350
+ entrypoint = id;
351
+ }
352
+ for (const edge of declaredItems(contract.recovery)) {
353
+ const from = steps.find((step) => step.id === edge.from_step);
354
+ if (from && edge.goto)
355
+ from.on_fail = edge.goto;
356
+ }
357
+ if (declaredItems(contract.preconditions).length) {
358
+ const id = "contract_preconditions";
359
+ steps.push({
360
+ id,
361
+ kind: "verify",
362
+ title: "Verify contract preconditions",
363
+ assertions: declaredItems(contract.preconditions).map((item) => `precondition:${item.id}`),
364
+ next: entrypoint,
365
+ });
366
+ entrypoint = id;
367
+ }
368
+ const verification = declaredItems(contract.verification);
369
+ if (verification.length) {
370
+ const id = "contract_verification";
371
+ const terminal = steps.find((step) => !step.next && step.id !== id);
372
+ if (terminal)
373
+ terminal.next = id;
374
+ steps.push({
375
+ id,
376
+ kind: "verify",
377
+ title: "Verify domain assertions",
378
+ assertions: verification.map((item) => `contract_assertion:${item.id}`),
379
+ });
380
+ if (!entrypoint)
381
+ entrypoint = id;
382
+ }
383
+ if (!entrypoint) {
384
+ const id = "contract_noop";
385
+ steps.push({
386
+ id,
387
+ kind: "instruct",
388
+ title: "No operational steps declared",
389
+ text: contract.intent,
390
+ });
391
+ entrypoint = id;
392
+ }
393
+ const constraints = declaredItems(contract.forbidden_actions).map((forbidden) => ({
394
+ kind: "steering_constraint",
395
+ id: forbidden.id,
396
+ verb: "reject",
397
+ effect: "forbidden",
398
+ statement: forbidden.description,
399
+ }));
400
+ const report = {
401
+ kind: "compilation_report",
402
+ skill_id: skillId,
403
+ source_id: source.id,
404
+ profile,
405
+ created_at: new Date().toISOString(),
406
+ mappings: [],
407
+ inferred_inputs: [],
408
+ issues: assessment.issues.map((issue) => ({
409
+ severity: profile === "release" ? "error" : "warning",
410
+ code: `contract_${issue.code}`,
411
+ message: `${issue.field}: ${issue.message}`,
412
+ related: [issue.field],
413
+ })),
414
+ pending_approvals: [],
415
+ approved: contract.provenance.human_review.status === "reviewed",
416
+ completeness,
417
+ semantic_contract: "native_0.5",
418
+ };
419
+ const safeJourney = {
420
+ ...source.journey,
421
+ summary: redactSecrets(source.journey.summary),
422
+ open_questions: source.journey.open_questions?.map(redactSecrets),
423
+ decisions: source.journey.decisions?.map(redactSecrets),
424
+ redacted: true,
425
+ };
426
+ const files = {
427
+ manifest: {
428
+ kind: "dot-skill",
429
+ id: skillId,
430
+ version: opts.version ?? "1.0.0",
431
+ title: opts.title ?? contract.title,
432
+ description: opts.description ?? source.summary ?? contract.intent,
433
+ intent: contract.intent,
434
+ contract: structuredClone(contract),
435
+ triggers: declaredItems(contract.triggers),
436
+ preconditions: structuredClone(contract.preconditions),
437
+ branches: structuredClone(contract.branches),
438
+ human_decisions: structuredClone(contract.human_decisions),
439
+ forbidden_actions: structuredClone(contract.forbidden_actions),
440
+ recovery: structuredClone(contract.recovery),
441
+ verification: structuredClone(contract.verification),
442
+ corrections: structuredClone(contract.corrections),
443
+ authors: source.actor ? [source.actor] : undefined,
444
+ container_version: CONTAINER_VERSION,
445
+ protocol_version: PROTOCOL_VERSION,
446
+ entrypoint,
447
+ inputs,
448
+ outputs,
449
+ capabilities,
450
+ permissions,
451
+ policy: { ...DEFAULT_SKILL_POLICY },
452
+ content: [],
453
+ package_digest: "sha256:" + "0".repeat(64),
454
+ provenance_mode: opts.provenance_mode ?? (profile === "continuity" ? "redacted" : "full"),
455
+ compile_profile: profile,
456
+ completeness,
457
+ package_sensitivity: contract.sensitivity,
458
+ mint: { mint_status: "draft" },
459
+ needs_human_review: profile === "continuity" || contract.provenance.human_review.status !== "reviewed",
460
+ },
461
+ workflow: {
462
+ kind: "workflow",
463
+ dialect_version: WORKFLOW_DIALECT_VERSION,
464
+ entrypoint,
465
+ steps,
466
+ constraints,
467
+ preconditions: structuredClone(contract.preconditions),
468
+ branches: structuredClone(contract.branches),
469
+ human_decisions: structuredClone(contract.human_decisions),
470
+ recovery: structuredClone(contract.recovery),
471
+ verification: structuredClone(contract.verification),
472
+ },
473
+ knowledge,
474
+ provenance: {
475
+ source: opts.provenance_mode === "proof_only"
476
+ ? undefined
477
+ : {
478
+ id: source.id,
479
+ hash: source.hash,
480
+ title: source.title,
481
+ contract: structuredClone(contract),
482
+ agent: {
483
+ ...source.agent,
484
+ endpoint: source.agent.endpoint
485
+ ? redactSecrets(source.agent.endpoint)
486
+ : undefined,
487
+ },
488
+ section_ids: source.sections.map((section) => `${section.id}@${section.revision}`),
489
+ },
490
+ journey: safeJourney,
491
+ generation_usage: opts.generation_usage ?? source.generation_usage,
492
+ proof: { source_id: source.id, source_hash: source.hash },
493
+ compilation_report: report,
494
+ },
495
+ };
496
+ const fileMap = buildFileMap(files);
497
+ files.manifest = finalizeManifest(files.manifest, fileMap);
498
+ const packageBytes = packSkill(files);
499
+ return {
500
+ files,
501
+ report,
502
+ packageBytes,
503
+ pending_approvals: [],
504
+ completeness,
505
+ };
506
+ }
507
+ export function compileSkillSource(source, opts = {}) {
508
+ const profile = opts.profile ?? "release";
509
+ if (source.contract)
510
+ return compileNativeContract(source, opts, profile);
511
+ if (profile === "release") {
512
+ throw new CompileRefusalError({
513
+ kind: "completeness_report",
514
+ profile,
515
+ complete: false,
516
+ present: [],
517
+ missing: ["semantic_contract"],
518
+ hints: [
519
+ "Legacy 0.4 SkillSource/Recipe text is a lossy adapter. Add a protocol 0.5 SkillContract and assess it before release compile; use continuity only for migration.",
520
+ ],
521
+ });
522
+ }
523
+ const skillId = opts.skill_id ?? shortId("skl");
524
+ const version = opts.version ?? "1.0.0";
525
+ const issues = [];
526
+ const mappings = [];
527
+ const knowledge = [];
528
+ const steps = [];
529
+ const inferredInputs = [];
530
+ const inputNames = new Set();
531
+ const constraints = [];
532
+ if (!isValidAgentHost(source.agent.host)) {
533
+ const completeness = assessCompleteness(source, {
534
+ profile,
535
+ hasWorkflowAction: false,
536
+ hasKnowledge: false,
537
+ hasInputsDeclared: true,
538
+ pendingApprovals: ["agent_context"],
539
+ });
540
+ completeness.missing = ["agent_context"];
541
+ completeness.complete = false;
542
+ completeness.hints = [HINTS.agent_context];
543
+ throw new CompileRefusalError(completeness);
544
+ }
545
+ const addInput = (slot) => {
546
+ if (inputNames.has(slot.name)) {
547
+ const existing = inferredInputs.find((i) => i.name === slot.name);
548
+ if (existing && slot.sensitivity === "secret") {
549
+ existing.sensitivity = "secret";
550
+ existing.source = "secret";
551
+ }
552
+ return;
553
+ }
554
+ inputNames.add(slot.name);
555
+ inferredInputs.push(slot);
556
+ };
557
+ for (const section of source.sections) {
558
+ let body = redactSecrets(section.body);
559
+ PLACEHOLDER_RE.lastIndex = 0;
560
+ let m;
561
+ while ((m = PLACEHOLDER_RE.exec(section.body))) {
562
+ const rawName = m[1] || m[2] || m[3] || "value";
563
+ const name = slug(rawName);
564
+ const isSecret = /secret|credential|token|password|key/i.test(name);
565
+ addInput({
566
+ name,
567
+ schema: { type: "string" },
568
+ description: `Value for ${name} generalized from section ${section.title}`,
569
+ source: isSecret ? "secret" : "human",
570
+ required: true,
571
+ sensitivity: isSecret ? "secret" : "private",
572
+ ask_when: "if_missing",
573
+ provenance: [{ kind: "section", id: section.id, revision: section.revision }],
574
+ generalization_reason: "Explicit placeholder in approved text",
575
+ approved: opts.approve_inferred_inputs === true,
576
+ });
577
+ }
578
+ for (const pat of GENERALIZABLE_PATTERNS) {
579
+ pat.re.lastIndex = 0;
580
+ if (pat.re.test(section.body)) {
581
+ const name = pat.name;
582
+ addInput({
583
+ name,
584
+ schema: { type: "string" },
585
+ description: pat.reason,
586
+ source: name.includes("credential") ? "secret" : "human",
587
+ required: true,
588
+ sensitivity: name.includes("credential") ? "secret" : "private",
589
+ ask_when: "if_missing",
590
+ provenance: [{ kind: "section", id: section.id, revision: section.revision }],
591
+ generalization_reason: pat.reason,
592
+ approved: opts.approve_inferred_inputs === true,
593
+ });
594
+ if (name.includes("credential")) {
595
+ body = body.replace(pat.re, "{{api_credential_ref}}");
596
+ }
597
+ }
598
+ }
599
+ const kid = `k_${section.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}`;
600
+ const item = {
601
+ kind: "knowledge",
602
+ id: kid,
603
+ type: knowledgeTypeFor(section),
604
+ title: section.title,
605
+ body,
606
+ fidelity: "exact",
607
+ pinned: true,
608
+ sensitivity: section.sensitivity === "public" ? "public" : "private",
609
+ provenance: [
610
+ { kind: "section", id: section.id, revision: section.revision, hash: source.hash },
611
+ ],
612
+ };
613
+ knowledge.push(item);
614
+ mappings.push({
615
+ from: { kind: "section", id: section.id, revision: section.revision },
616
+ to: { kind: "knowledge", id: kid },
617
+ });
618
+ if (section.type === "prompt") {
619
+ const stepId = `s_prompt_${kid}`;
620
+ steps.push({
621
+ id: stepId,
622
+ kind: "prompt",
623
+ title: section.title,
624
+ template: body,
625
+ knowledge_refs: [kid],
626
+ provenance: [{ kind: "section", id: section.id, revision: section.revision }],
627
+ });
628
+ mappings.push({
629
+ from: { kind: "section", id: section.id, revision: section.revision },
630
+ to: { kind: "step", id: stepId },
631
+ });
632
+ }
633
+ else if (section.type === "integration" ||
634
+ section.type === "implementation_note" ||
635
+ section.type === "workflow_note") {
636
+ const stepId = `s_instruct_${kid}`;
637
+ steps.push({
638
+ id: stepId,
639
+ kind: "instruct",
640
+ title: section.title,
641
+ text: body,
642
+ knowledge_refs: [kid],
643
+ provenance: [{ kind: "section", id: section.id, revision: section.revision }],
644
+ });
645
+ mappings.push({
646
+ from: { kind: "section", id: section.id, revision: section.revision },
647
+ to: { kind: "step", id: stepId },
648
+ });
649
+ }
650
+ else if (section.type === "question") {
651
+ issues.push({
652
+ severity: "warning",
653
+ code: "unresolved_question",
654
+ message: `Section ${section.title} is a question — may need an input slot or human_decision`,
655
+ related: [section.id],
656
+ });
657
+ }
658
+ }
659
+ for (const s of source.steering) {
660
+ const cid = `c_${s.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}`;
661
+ const effect = s.verb === "affirm" ? "invariant" : s.verb === "reject" ? "forbidden" : "decision_rule";
662
+ const statement = s.note?.trim() || `${s.verb} on ${s.target_kind} ${s.target_id}`;
663
+ constraints.push({
664
+ kind: "steering_constraint",
665
+ id: cid,
666
+ verb: s.verb,
667
+ effect,
668
+ statement,
669
+ source_steering_id: s.id,
670
+ targets: [s.target_id],
671
+ provenance: [{ kind: "steering", id: s.id }],
672
+ });
673
+ mappings.push({
674
+ from: { kind: "steering", id: s.id },
675
+ to: { kind: "constraint", id: cid },
676
+ });
677
+ }
678
+ if (steps.length === 0 && knowledge.length > 0) {
679
+ steps.push({
680
+ id: "s_apply_knowledge",
681
+ kind: "instruct",
682
+ title: "Apply captured knowledge",
683
+ text: "Follow the pinned knowledge items and steering constraints exactly. Ask only for declared inputs. Resume from journey provenance when continuing prior work.",
684
+ knowledge_refs: knowledge.map((k) => k.id),
685
+ });
686
+ }
687
+ for (let i = 0; i < steps.length - 1; i++) {
688
+ if (!steps[i].next)
689
+ steps[i].next = steps[i + 1].id;
690
+ }
691
+ if (steps.length > 0) {
692
+ const last = steps[steps.length - 1];
693
+ const verifyId = "s_verify";
694
+ const emitId = "s_emit";
695
+ last.next = verifyId;
696
+ steps.push({
697
+ id: verifyId,
698
+ kind: "verify",
699
+ title: "Verify constraints",
700
+ assertions: [
701
+ "all_required_inputs_resolved",
702
+ ...constraints
703
+ .filter((c) => c.effect === "invariant")
704
+ .map((c) => `constraint_present:${c.id}`),
705
+ ],
706
+ next: emitId,
707
+ });
708
+ steps.push({
709
+ id: emitId,
710
+ kind: "emit",
711
+ title: "Emit result",
712
+ output: "result",
713
+ from: last.id,
714
+ });
715
+ }
716
+ const pending = [];
717
+ for (const slot of inferredInputs) {
718
+ if (!slot.approved)
719
+ pending.push(`input:${slot.name}`);
720
+ mappings.push({
721
+ from: slot.provenance?.[0] ?? { kind: "author", id: "compiler" },
722
+ to: { kind: "input", id: slot.name },
723
+ });
724
+ }
725
+ const needsCodeWrite = source.sections.some((i) => i.type === "code");
726
+ const permissions = needsCodeWrite
727
+ ? [
728
+ {
729
+ side_effect_class: "write",
730
+ description: "May modify project files when applying code knowledge",
731
+ requires_consent: true,
732
+ },
733
+ ]
734
+ : [];
735
+ if (needsCodeWrite && !opts.approve_permissions) {
736
+ pending.push("permission:write");
737
+ issues.push({
738
+ severity: "warning",
739
+ code: "permission_approval",
740
+ message: "Write permission inferred from code sections — requires human approval",
741
+ });
742
+ }
743
+ // Explicit "no inputs" declaration when none inferred
744
+ const hasInputsDeclared = inferredInputs.length > 0 || source.inputs_declared === "none";
745
+ const safeJourney = {
746
+ ...source.journey,
747
+ summary: redactSecrets(source.journey.summary),
748
+ open_questions: source.journey.open_questions?.map(redactSecrets),
749
+ decisions: source.journey.decisions?.map(redactSecrets),
750
+ redacted: true,
751
+ };
752
+ const usage = opts.generation_usage ?? source.generation_usage;
753
+ const completeness = assessCompleteness(source, {
754
+ profile,
755
+ hasWorkflowAction: sectionHasWorkflowAction(source.sections) || source.prompts.length > 0,
756
+ hasKnowledge: knowledge.length > 0,
757
+ hasInputsDeclared,
758
+ pendingApprovals: pending,
759
+ });
760
+ if (!source.contract) {
761
+ completeness.complete = false;
762
+ if (!completeness.missing.includes("semantic_contract")) {
763
+ completeness.missing.push("semantic_contract");
764
+ }
765
+ completeness.hints.push("Add a 0.5 SkillContract. Legacy text was retained for continuity but structured semantics are unknown.");
766
+ }
767
+ if (!completeness.complete && profile === "continuity") {
768
+ // continuity still requires hard parts
769
+ const hard = completeness.missing.filter((m) => ["agent_context", "sections", "intent", "journey", "knowledge_or_prompts"].includes(m));
770
+ if (hard.length && !opts.allow_incomplete) {
771
+ throw new CompileRefusalError({ ...completeness, missing: hard, complete: false });
772
+ }
773
+ }
774
+ if (inferredInputs.some((i) => !i.approved)) {
775
+ issues.push({
776
+ severity: "info",
777
+ code: "pending_input_approval",
778
+ message: "Inferred input slots require human approval before release mint",
779
+ });
780
+ }
781
+ if (!steps.length) {
782
+ // continuity with only questions etc.
783
+ steps.push({
784
+ id: "s_resume",
785
+ kind: "instruct",
786
+ title: "Resume continuity",
787
+ text: source.journey.summary || "Continue from open questions in provenance.",
788
+ knowledge_refs: knowledge.map((k) => k.id),
789
+ });
790
+ steps.push({
791
+ id: "s_emit",
792
+ kind: "emit",
793
+ title: "Emit result",
794
+ output: "result",
795
+ from: "s_resume",
796
+ });
797
+ steps[0].next = "s_emit";
798
+ }
799
+ const entrypoint = steps[0].id;
800
+ const report = {
801
+ kind: "compilation_report",
802
+ skill_id: skillId,
803
+ source_id: source.id,
804
+ recipe_id: source.source_refs?.find((r) => r.kind === "recipe")?.id,
805
+ profile,
806
+ created_at: new Date().toISOString(),
807
+ mappings,
808
+ inferred_inputs: inferredInputs,
809
+ issues,
810
+ pending_approvals: pending,
811
+ approved: pending.length === 0,
812
+ completeness,
813
+ semantic_contract: "legacy_lossy",
814
+ losses: [
815
+ "intent/triggers may be inferred from title or summary",
816
+ "inputs inferred from placeholders lose original schema semantics",
817
+ "preconditions, branches, outputs, recovery, and verification may remain prose",
818
+ "human review cannot be reconstructed from legacy text",
819
+ ],
820
+ };
821
+ const files = {
822
+ manifest: {
823
+ kind: "dot-skill",
824
+ id: skillId,
825
+ version,
826
+ title: opts.title ?? source.title,
827
+ description: opts.description ??
828
+ source.summary ??
829
+ `Skill compiled from source ${source.id}`,
830
+ intent: source.intent ?? source.summary,
831
+ triggers: [{ id: "legacy_title", description: source.title }],
832
+ authors: source.actor ? [source.actor] : undefined,
833
+ container_version: CONTAINER_VERSION,
834
+ protocol_version: PROTOCOL_VERSION,
835
+ entrypoint,
836
+ inputs: inferredInputs,
837
+ outputs: [
838
+ {
839
+ name: "result",
840
+ description: "Primary textual or structured result of the skill",
841
+ schema: { type: "string" },
842
+ required: true,
843
+ },
844
+ ],
845
+ capabilities: needsCodeWrite
846
+ ? [
847
+ {
848
+ name: "filesystem.write",
849
+ description: "Write files in the workspace",
850
+ side_effect_class: "write",
851
+ fallback: "ask_human",
852
+ required: false,
853
+ adapters: [{ kind: "host", name: "workspace" }],
854
+ },
855
+ ]
856
+ : [],
857
+ permissions,
858
+ policy: { ...DEFAULT_SKILL_POLICY },
859
+ content: [],
860
+ package_digest: "sha256:" + "0".repeat(64),
861
+ provenance_mode: opts.provenance_mode ?? (profile === "continuity" ? "redacted" : "full"),
862
+ compile_profile: profile,
863
+ completeness,
864
+ package_sensitivity: source.sensitivity,
865
+ mint: { mint_status: "draft" },
866
+ needs_human_review: pending.length > 0 || profile === "continuity",
867
+ legacy: true,
868
+ },
869
+ workflow: {
870
+ kind: "workflow",
871
+ dialect_version: WORKFLOW_DIALECT_VERSION,
872
+ entrypoint,
873
+ steps,
874
+ constraints,
875
+ },
876
+ knowledge,
877
+ prompts: source.prompts.length
878
+ ? Object.fromEntries(source.prompts.map((prompt) => [
879
+ `${prompt.id}.txt`,
880
+ redactSecrets(prompt.body),
881
+ ]))
882
+ : undefined,
883
+ provenance: {
884
+ source: opts.provenance_mode === "proof_only"
885
+ ? undefined
886
+ : {
887
+ id: source.id,
888
+ hash: source.hash,
889
+ title: source.title,
890
+ agent: {
891
+ ...source.agent,
892
+ endpoint: source.agent.endpoint
893
+ ? redactSecrets(source.agent.endpoint)
894
+ : undefined,
895
+ },
896
+ sensitivity: source.sensitivity,
897
+ section_ids: source.sections.map((s) => `${s.id}@${s.revision}`),
898
+ source_refs: source.source_refs,
899
+ },
900
+ journey: safeJourney,
901
+ generation_usage: usage,
902
+ proof: {
903
+ source_id: source.id,
904
+ source_hash: source.hash,
905
+ section_ids: source.sections.map((s) => `${s.id}@${s.revision}`),
906
+ agent_host: source.agent.host,
907
+ },
908
+ compilation_report: report,
909
+ },
910
+ };
911
+ const fileMap = buildFileMap(files);
912
+ files.manifest = finalizeManifest(files.manifest, fileMap);
913
+ const packageBytes = packSkill(files);
914
+ return {
915
+ files,
916
+ report,
917
+ packageBytes,
918
+ pending_approvals: pending,
919
+ completeness,
920
+ };
921
+ }
922
+ /** Skillerr / legacy adapter entry — converts Recipe then compiles. */
923
+ export function compileRecipeToSkill(recipe, opts = {}) {
924
+ const source = recipeToSkillSource(recipe, {
925
+ agent: {
926
+ host: opts.host ?? recipe.provenance.hosts[0],
927
+ model: opts.model ?? recipe.provenance.models[0],
928
+ },
929
+ });
930
+ if (opts.generation_usage)
931
+ source.generation_usage = opts.generation_usage;
932
+ return compileSkillSource(source, opts);
933
+ }
934
+ export function approveCompilation(result, approvals) {
935
+ const files = structuredClone(result.files);
936
+ for (const slot of files.manifest.inputs) {
937
+ if (!approvals.inputs ||
938
+ approvals.inputs.includes(slot.name) ||
939
+ approvals.inputs.includes("*")) {
940
+ slot.approved = true;
941
+ }
942
+ }
943
+ const pending = files.manifest.inputs
944
+ .filter((i) => !i.approved)
945
+ .map((i) => `input:${i.name}`);
946
+ if (files.manifest.permissions.some((p) => p.requires_consent) && !approvals.permissions) {
947
+ pending.push(...files.manifest.permissions
948
+ .filter((p) => p.requires_consent)
949
+ .map((p) => `permission:${p.side_effect_class}`));
950
+ }
951
+ files.manifest.needs_human_review =
952
+ pending.length > 0 || files.manifest.compile_profile === "continuity";
953
+ if (files.provenance?.compilation_report) {
954
+ files.provenance.compilation_report.pending_approvals = pending;
955
+ files.provenance.compilation_report.approved = pending.length === 0;
956
+ files.provenance.compilation_report.inferred_inputs = files.manifest.inputs;
957
+ if (files.provenance.compilation_report.completeness) {
958
+ const c = files.provenance.compilation_report.completeness;
959
+ if (pending.length === 0) {
960
+ c.missing = c.missing.filter((m) => m !== "human_approvals");
961
+ if (!c.present.includes("human_approvals"))
962
+ c.present.push("human_approvals");
963
+ c.complete = c.missing.length === 0;
964
+ }
965
+ files.manifest.completeness = c;
966
+ }
967
+ }
968
+ const fileMap = buildFileMap(files);
969
+ files.manifest = finalizeManifest(files.manifest, fileMap);
970
+ return {
971
+ files,
972
+ report: files.provenance.compilation_report,
973
+ packageBytes: packSkill(files),
974
+ pending_approvals: pending,
975
+ completeness: files.manifest.completeness,
976
+ };
977
+ }