@jinn-network/jinn-layer 0.1.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.
Files changed (131) hide show
  1. package/dist/adapters/contribution-adapter.d.ts +18 -0
  2. package/dist/adapters/contribution-adapter.js +134 -0
  3. package/dist/adapters/corpus-adapter.d.ts +12 -0
  4. package/dist/adapters/corpus-adapter.js +212 -0
  5. package/dist/adapters/index.d.ts +17 -0
  6. package/dist/adapters/index.js +17 -0
  7. package/dist/adapters/json-map-store.d.ts +4 -0
  8. package/dist/adapters/json-map-store.js +31 -0
  9. package/dist/adapters/local-learning-adapter.d.ts +25 -0
  10. package/dist/adapters/local-learning-adapter.js +49 -0
  11. package/dist/adapters/skills-adapter.d.ts +8 -0
  12. package/dist/adapters/skills-adapter.js +55 -0
  13. package/dist/admission-policy.d.ts +38 -0
  14. package/dist/admission-policy.js +17 -0
  15. package/dist/bin/jinn-distill-mcp.d.ts +2 -0
  16. package/dist/bin/jinn-distill-mcp.js +10 -0
  17. package/dist/bin/jinn-layer.d.ts +7 -0
  18. package/dist/bin/jinn-layer.js +15 -0
  19. package/dist/bridge-fetch-evidence.d.ts +102 -0
  20. package/dist/bridge-fetch-evidence.js +677 -0
  21. package/dist/bridge-verdict-source.d.ts +47 -0
  22. package/dist/bridge-verdict-source.js +216 -0
  23. package/dist/bridge.d.ts +247 -0
  24. package/dist/bridge.js +439 -0
  25. package/dist/capture.d.ts +98 -0
  26. package/dist/capture.js +617 -0
  27. package/dist/cli.d.ts +148 -0
  28. package/dist/cli.js +2221 -0
  29. package/dist/cluster-selection.d.ts +34 -0
  30. package/dist/cluster-selection.js +135 -0
  31. package/dist/cluster.d.ts +89 -0
  32. package/dist/cluster.js +142 -0
  33. package/dist/consume.d.ts +139 -0
  34. package/dist/consume.js +280 -0
  35. package/dist/corpus-probes.d.ts +61 -0
  36. package/dist/corpus-probes.js +120 -0
  37. package/dist/corpus-store.d.ts +16 -0
  38. package/dist/corpus-store.js +101 -0
  39. package/dist/distill-captures.d.ts +56 -0
  40. package/dist/distill-captures.js +283 -0
  41. package/dist/distill-feedback.d.ts +20 -0
  42. package/dist/distill-feedback.js +30 -0
  43. package/dist/distill-llm.d.ts +138 -0
  44. package/dist/distill-llm.js +438 -0
  45. package/dist/distill-mcp-server.d.ts +30 -0
  46. package/dist/distill-mcp-server.js +281 -0
  47. package/dist/distill-mode.d.ts +62 -0
  48. package/dist/distill-mode.js +107 -0
  49. package/dist/distill-progress.d.ts +78 -0
  50. package/dist/distill-progress.js +69 -0
  51. package/dist/distill-prompt.d.ts +34 -0
  52. package/dist/distill-prompt.js +85 -0
  53. package/dist/distill-render.d.ts +123 -0
  54. package/dist/distill-render.js +303 -0
  55. package/dist/distill-runs.d.ts +28 -0
  56. package/dist/distill-runs.js +39 -0
  57. package/dist/distill-traces.d.ts +67 -0
  58. package/dist/distill-traces.js +211 -0
  59. package/dist/distill.d.ts +180 -0
  60. package/dist/distill.js +337 -0
  61. package/dist/distiller.d.ts +118 -0
  62. package/dist/distiller.js +142 -0
  63. package/dist/envelope.d.ts +9 -0
  64. package/dist/envelope.js +9 -0
  65. package/dist/eval-prep.d.ts +129 -0
  66. package/dist/eval-prep.js +523 -0
  67. package/dist/execution-publish.d.ts +62 -0
  68. package/dist/execution-publish.js +72 -0
  69. package/dist/gate.d.ts +39 -0
  70. package/dist/gate.js +106 -0
  71. package/dist/index.d.ts +49 -0
  72. package/dist/index.js +50 -0
  73. package/dist/ipfs-cid.d.ts +18 -0
  74. package/dist/ipfs-cid.js +149 -0
  75. package/dist/layer-default.d.ts +3 -0
  76. package/dist/layer-default.js +10 -0
  77. package/dist/ledger.d.ts +62 -0
  78. package/dist/ledger.js +104 -0
  79. package/dist/measurement.d.ts +90 -0
  80. package/dist/measurement.js +134 -0
  81. package/dist/path-safety.d.ts +19 -0
  82. package/dist/path-safety.js +303 -0
  83. package/dist/pipeline.d.ts +94 -0
  84. package/dist/pipeline.js +124 -0
  85. package/dist/plugin-wiring.d.ts +4 -0
  86. package/dist/plugin-wiring.js +54 -0
  87. package/dist/preview.d.ts +31 -0
  88. package/dist/preview.js +28 -0
  89. package/dist/process-contract.d.ts +303 -0
  90. package/dist/process-contract.js +118 -0
  91. package/dist/publish-skill.d.ts +86 -0
  92. package/dist/publish-skill.js +116 -0
  93. package/dist/publish.d.ts +294 -0
  94. package/dist/publish.js +1090 -0
  95. package/dist/scripts/scan-skills-registry.d.ts +24 -0
  96. package/dist/scripts/scan-skills-registry.js +207 -0
  97. package/dist/seed-import/curated-batch.d.ts +36 -0
  98. package/dist/seed-import/curated-batch.js +103 -0
  99. package/dist/seed-import/episode-execute.d.ts +50 -0
  100. package/dist/seed-import/episode-execute.js +235 -0
  101. package/dist/seed-import/episode-fetch.d.ts +119 -0
  102. package/dist/seed-import/episode-fetch.js +137 -0
  103. package/dist/seed-import/episode-plan.d.ts +11 -0
  104. package/dist/seed-import/episode-plan.js +27 -0
  105. package/dist/seed-import/episode-report.d.ts +39 -0
  106. package/dist/seed-import/episode-report.js +36 -0
  107. package/dist/seed-import/execute.d.ts +61 -0
  108. package/dist/seed-import/execute.js +245 -0
  109. package/dist/seed-import/fetch.d.ts +62 -0
  110. package/dist/seed-import/fetch.js +82 -0
  111. package/dist/seed-import/licence.d.ts +17 -0
  112. package/dist/seed-import/licence.js +36 -0
  113. package/dist/seed-import/plan.d.ts +10 -0
  114. package/dist/seed-import/plan.js +21 -0
  115. package/dist/seed-import/report.d.ts +34 -0
  116. package/dist/seed-import/report.js +31 -0
  117. package/dist/seed-import/state.d.ts +72 -0
  118. package/dist/seed-import/state.js +120 -0
  119. package/dist/signal.d.ts +36 -0
  120. package/dist/signal.js +48 -0
  121. package/dist/signing.d.ts +8 -0
  122. package/dist/signing.js +21 -0
  123. package/dist/skill-package.d.ts +70 -0
  124. package/dist/skill-package.js +141 -0
  125. package/dist/skill.d.ts +21 -0
  126. package/dist/skill.js +77 -0
  127. package/dist/snapshot-transcript.d.ts +54 -0
  128. package/dist/snapshot-transcript.js +117 -0
  129. package/dist/three-arm-run.d.ts +27 -0
  130. package/dist/three-arm-run.js +41 -0
  131. package/package.json +60 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * The distill LLM port — runs `jinn-skill-distill-prompt-v1` over one evidence
3
+ * cluster via the `claude` CLI and returns a {@link DistillLLMOutput}
4
+ * (spec/2026-07-06-distillation-v1.md §7). This is the model call that
5
+ * `distillClusters` injects as `DistillDeps.distill`; the distiller itself owns
6
+ * the downstream scrub / contamination / publish steps.
7
+ *
8
+ * The port is deliberately thin: it builds the model input (the versioned
9
+ * distill prompt + a serialized view of the cluster), spawns `claude` in
10
+ * non-interactive mode reading that input on stdin, and parses a strict
11
+ * `{ name, description, body }` JSON object back out of stdout (tolerating a
12
+ * leading/trailing prose wrapper the model may emit around it).
13
+ *
14
+ * Auth is ambient: the `claude` CLI resolves its own credentials (a logged-in
15
+ * session, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_API_KEY` from the env). This
16
+ * port never reads, sets, or manages a key — matching the daemon's other
17
+ * `claude -p` call sites (`src/runner/claude.ts`, the claude-code adapter).
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ /** Default `claude` executable (resolved from PATH), mirrors the daemon default. */
24
+ const DEFAULT_CLAUDE_PATH = 'claude';
25
+ /** Default `codex` executable (resolved from PATH), using the logged-in Codex CLI session. */
26
+ const DEFAULT_CODEX_PATH = 'codex';
27
+ /**
28
+ * Default distiller model — the strongest available (opus-class), NOT the
29
+ * daemon-wide cheap default (spec §5, v0.5). Distillation is offline, one-shot,
30
+ * and cheap relative to the measurement run, and its quality is model-sensitive
31
+ * — so it pins the best model. Overridable via `JINN_DISTILL_MODEL` at the CLI.
32
+ */
33
+ export const DEFAULT_MODEL = 'claude-opus-4-8';
34
+ /** Default Codex distiller model for quality runs; override with `JINN_DISTILL_MODEL`. */
35
+ export const DEFAULT_CODEX_MODEL = 'gpt-5.5';
36
+ /**
37
+ * Default per-cluster subprocess deadline (#1534). A hung `claude`/`codex`
38
+ * child would otherwise block a run forever — silently, since nothing prints
39
+ * until the run completes. On expiry the child is killed and the cluster lands
40
+ * in `errors[]`; the run continues. Override with `--cluster-timeout` /
41
+ * `JINN_DISTILL_CLUSTER_TIMEOUT_S`.
42
+ */
43
+ export const DEFAULT_CLUSTER_TIMEOUT_MS = 600_000;
44
+ /** A distiller subprocess exceeded its per-cluster deadline and was killed. */
45
+ export class DistillTimeoutError extends Error {
46
+ constructor(label, timeoutMs) {
47
+ super(`${label}: distiller subprocess timed out after ${timeoutMs}ms and was killed`);
48
+ this.name = 'DistillTimeoutError';
49
+ }
50
+ }
51
+ /**
52
+ * The static, honest catalog of runnable distillers. Every entry is `local`
53
+ * today — no hosted distiller exists (both ports spawn a local CLI). The models
54
+ * reference `DEFAULT_MODEL` / `DEFAULT_CODEX_MODEL` directly so the catalog and
55
+ * the provider defaults cannot drift.
56
+ */
57
+ export const DISTILLER_CATALOG = [
58
+ {
59
+ provider: 'claude',
60
+ model: DEFAULT_MODEL,
61
+ execution: 'local',
62
+ cost: 'high (frontier pass)',
63
+ privacy: 'local — captures never leave the machine',
64
+ isDefault: true,
65
+ },
66
+ {
67
+ provider: 'codex',
68
+ model: DEFAULT_CODEX_MODEL,
69
+ execution: 'local',
70
+ cost: 'high (frontier pass)',
71
+ privacy: 'local — captures never leave the machine',
72
+ },
73
+ ];
74
+ const DISTILL_OUTPUT_SCHEMA = {
75
+ type: 'object',
76
+ additionalProperties: false,
77
+ required: ['name', 'description', 'body'],
78
+ properties: {
79
+ name: { type: 'string', minLength: 1 },
80
+ description: { type: 'string', minLength: 1 },
81
+ body: { type: 'string', minLength: 1 },
82
+ },
83
+ };
84
+ const META_DISTILL_OUTPUT_SCHEMA = {
85
+ type: 'object',
86
+ additionalProperties: false,
87
+ required: ['name', 'description', 'body', 'supports'],
88
+ properties: {
89
+ name: { type: 'string', minLength: 1 },
90
+ description: { type: 'string', minLength: 1 },
91
+ body: { type: 'string', minLength: 1 },
92
+ supports: {
93
+ type: 'array',
94
+ items: { type: 'string', minLength: 1 },
95
+ minItems: 1,
96
+ },
97
+ },
98
+ };
99
+ /** The prompt MODE each cluster tier keys on (§7). */
100
+ const MODE_BY_TIER = {
101
+ pattern: 'strategic-pattern',
102
+ lesson: 'failure-lesson',
103
+ contrastive: 'contrastive',
104
+ };
105
+ /**
106
+ * Serialize a cluster into the concrete model input appended after the prompt:
107
+ * the MODE the prompt keys on (§7 tier → strategic-pattern / failure-lesson /
108
+ * contrastive) plus the cluster's evidence payload. Only tier + `input` are
109
+ * sent — refs and instance ids are audit metadata the model must not echo.
110
+ */
111
+ function serializeCluster(cluster) {
112
+ const mode = MODE_BY_TIER[cluster.tier];
113
+ const evidence = JSON.stringify(cluster.input, null, 2);
114
+ return `MODE = ${mode}\n\nEVIDENCE (the cluster's verified traces):\n${evidence}`;
115
+ }
116
+ /**
117
+ * Build the full model input: the versioned distill prompt, the serialized
118
+ * cluster, and a strict-JSON output contract. The contract is explicit so the
119
+ * downstream parser has an object to extract even if the model wraps it in prose.
120
+ */
121
+ export function buildDistillInput(prompt, cluster) {
122
+ return [
123
+ prompt,
124
+ '',
125
+ serializeCluster(cluster),
126
+ '',
127
+ 'Return ONLY a single JSON object with exactly these string fields, and nothing else:',
128
+ '{ "name": "...", "description": "...", "body": "..." }',
129
+ '- name: lowercase-hyphen skill name.',
130
+ '- description: the retrieval surface ("Use when …").',
131
+ '- body: the markdown SKILL.md body.',
132
+ ].join('\n');
133
+ }
134
+ /**
135
+ * Extract the first balanced `{...}` JSON object from `stdout` and parse it,
136
+ * tolerating a leading/trailing prose wrapper (e.g. "Here is the skill: {…}").
137
+ * Scans for the first `{` and matches braces (skipping string literals) to the
138
+ * balancing `}` so a `}` inside a string does not truncate the object.
139
+ */
140
+ function extractJsonObject(stdout) {
141
+ const start = stdout.indexOf('{');
142
+ if (start === -1) {
143
+ throw new Error(`distill LLM: no JSON object in model output: ${stdout.slice(0, 200)}`);
144
+ }
145
+ let depth = 0;
146
+ let inString = false;
147
+ let escaped = false;
148
+ for (let i = start; i < stdout.length; i += 1) {
149
+ const ch = stdout[i];
150
+ if (inString) {
151
+ if (escaped)
152
+ escaped = false;
153
+ else if (ch === '\\')
154
+ escaped = true;
155
+ else if (ch === '"')
156
+ inString = false;
157
+ continue;
158
+ }
159
+ if (ch === '"')
160
+ inString = true;
161
+ else if (ch === '{')
162
+ depth += 1;
163
+ else if (ch === '}') {
164
+ depth -= 1;
165
+ if (depth === 0) {
166
+ const candidate = stdout.slice(start, i + 1);
167
+ try {
168
+ return JSON.parse(candidate);
169
+ }
170
+ catch (err) {
171
+ throw new Error(`distill LLM: model output was not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ throw new Error(`distill LLM: unterminated JSON object in model output: ${stdout.slice(start, start + 200)}`);
177
+ }
178
+ /**
179
+ * Validate `parsed` is a JSON object whose `name`/`description`/`body` are all
180
+ * non-empty strings, and return it narrowed. Shared by both distill asserts;
181
+ * `label` prefixes the error so the stage (distill vs meta-distill) stays legible.
182
+ */
183
+ function assertSkillOutputFields(parsed, label) {
184
+ if (typeof parsed !== 'object' || parsed === null) {
185
+ throw new Error(`${label}: model output is not a JSON object`);
186
+ }
187
+ const obj = parsed;
188
+ for (const field of ['name', 'description', 'body']) {
189
+ if (typeof obj[field] !== 'string' || obj[field] === '') {
190
+ throw new Error(`${label}: model output missing/invalid field "${field}"`);
191
+ }
192
+ }
193
+ return obj;
194
+ }
195
+ /** Validate the parsed object is a `{ name, description, body }` of non-empty strings. */
196
+ function assertDistillOutput(parsed) {
197
+ const obj = assertSkillOutputFields(parsed, 'distill LLM');
198
+ return { name: obj.name, description: obj.description, body: obj.body };
199
+ }
200
+ /**
201
+ * Shared `claude -p` runner for the distill ports: spawn `claude -p --model`,
202
+ * pipe `input` on stdin, collect stdout, and on a clean exit hand stdout to
203
+ * `parse` (which extracts + validates the JSON, and may throw). A non-zero exit
204
+ * or a spawn error rejects; `label` prefixes the process-level errors so the
205
+ * calling stage stays identifiable.
206
+ *
207
+ * `-p` (non-interactive print mode) with the prompt on stdin mirrors the daemon's
208
+ * other claude call sites (src/runner/claude.ts, the claude-code adapter) minus
209
+ * the MCP wiring these ports have no use for.
210
+ */
211
+ function runClaudeJson(claudePath, model, spawnImpl, input, parse, label, timeoutMs = DEFAULT_CLUSTER_TIMEOUT_MS) {
212
+ const args = ['-p', '--model', model];
213
+ return new Promise((resolve, reject) => {
214
+ const child = spawnImpl(claudePath, args);
215
+ let stdout = '';
216
+ let stderr = '';
217
+ let settled = false;
218
+ // #1534: a hung child blocks the whole run — kill it at the deadline and
219
+ // surface a per-cluster error so the run continues.
220
+ const deadline = setTimeout(() => {
221
+ if (settled)
222
+ return;
223
+ settled = true;
224
+ child.kill?.('SIGKILL');
225
+ reject(new DistillTimeoutError(label, timeoutMs));
226
+ }, timeoutMs);
227
+ deadline.unref?.();
228
+ child.stdout?.on('data', (d) => { stdout += d.toString(); });
229
+ child.stderr?.on('data', (d) => { stderr += d.toString(); });
230
+ child.on('error', (err) => {
231
+ if (settled)
232
+ return;
233
+ settled = true;
234
+ clearTimeout(deadline);
235
+ reject(err);
236
+ });
237
+ child.on('exit', (code) => {
238
+ if (settled)
239
+ return;
240
+ settled = true;
241
+ clearTimeout(deadline);
242
+ if (code !== 0) {
243
+ reject(new Error(`${label}: claude exited with code ${code}: ${stderr.slice(0, 500)}`));
244
+ return;
245
+ }
246
+ try {
247
+ resolve(parse(stdout));
248
+ }
249
+ catch (err) {
250
+ reject(err instanceof Error ? err : new Error(String(err)));
251
+ }
252
+ });
253
+ // Feed the prompt on stdin, then close it so `claude -p` runs and exits.
254
+ child.stdin?.write(input);
255
+ child.stdin?.end();
256
+ });
257
+ }
258
+ async function withTempJsonSchema(schema, fn) {
259
+ const dir = await mkdtemp(join(tmpdir(), 'jinn-codex-schema-'));
260
+ const schemaPath = join(dir, 'output.schema.json');
261
+ try {
262
+ await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf-8');
263
+ return await fn(schemaPath);
264
+ }
265
+ finally {
266
+ await rm(dir, { recursive: true, force: true });
267
+ }
268
+ }
269
+ /**
270
+ * Shared Codex runner for the distill ports. `codex exec` reads the full prompt
271
+ * on stdin, uses its logged-in CLI auth, and constrains the final message with
272
+ * a temporary JSON schema file so the same strict parser can validate output.
273
+ */
274
+ async function runCodexJson(codexPath, model, spawnImpl, input, schema, parse, label, timeoutMs = DEFAULT_CLUSTER_TIMEOUT_MS) {
275
+ return withTempJsonSchema(schema, (schemaPath) => new Promise((resolve, reject) => {
276
+ const args = [
277
+ '--ask-for-approval',
278
+ 'never',
279
+ 'exec',
280
+ '--model',
281
+ model,
282
+ '--sandbox',
283
+ 'read-only',
284
+ '--ephemeral',
285
+ '--output-schema',
286
+ schemaPath,
287
+ '-',
288
+ ];
289
+ const child = spawnImpl(codexPath, args);
290
+ let stdout = '';
291
+ let stderr = '';
292
+ let settled = false;
293
+ // #1534: same deadline as the claude port — kill and surface a per-cluster error.
294
+ const deadline = setTimeout(() => {
295
+ if (settled)
296
+ return;
297
+ settled = true;
298
+ child.kill?.('SIGKILL');
299
+ reject(new DistillTimeoutError(label, timeoutMs));
300
+ }, timeoutMs);
301
+ deadline.unref?.();
302
+ child.stdout?.on('data', (d) => { stdout += d.toString(); });
303
+ child.stderr?.on('data', (d) => { stderr += d.toString(); });
304
+ child.on('error', (err) => {
305
+ if (settled)
306
+ return;
307
+ settled = true;
308
+ clearTimeout(deadline);
309
+ reject(err);
310
+ });
311
+ child.on('exit', (code) => {
312
+ if (settled)
313
+ return;
314
+ settled = true;
315
+ clearTimeout(deadline);
316
+ if (code !== 0) {
317
+ reject(new Error(`${label}: codex exited with code ${code}: stderr=${stderr.slice(0, 500)} stdout=${stdout.slice(0, 500)}`));
318
+ return;
319
+ }
320
+ try {
321
+ resolve(parse(stdout));
322
+ }
323
+ catch (err) {
324
+ reject(err instanceof Error ? err : new Error(String(err)));
325
+ }
326
+ });
327
+ child.stdin?.write(input);
328
+ child.stdin?.end();
329
+ }));
330
+ }
331
+ /**
332
+ * Build the distill LLM port. Returns the `DistillDeps.distill` function: given
333
+ * a cluster, spawn `claude -p --model <model>`, pipe the built input on stdin,
334
+ * collect stdout, and parse the strict JSON output.
335
+ *
336
+ * @param opts.claudePath Path to the `claude` executable (default `claude`).
337
+ * @param opts.model Model id (default `claude-opus-4-8` — opus-class, §5).
338
+ * @param opts.spawnImpl Injected spawn (default `child_process.spawn`); tests
339
+ * pass a fake so no real `claude` is invoked.
340
+ */
341
+ export function createClaudeDistiller(opts = {}) {
342
+ const claudePath = opts.claudePath ?? DEFAULT_CLAUDE_PATH;
343
+ const model = opts.model ?? DEFAULT_MODEL;
344
+ const spawnImpl = opts.spawnImpl ?? ((command, args) => spawn(command, [...args]));
345
+ return async function distill(cluster) {
346
+ // Lazy import keeps this module free of a top-level dependency on the
347
+ // prompt's SHA sidecar; the prompt text itself is what we send.
348
+ const { JINN_SKILL_DISTILL_PROMPT_V1 } = await import('./distill-prompt.js');
349
+ const input = buildDistillInput(JINN_SKILL_DISTILL_PROMPT_V1, cluster);
350
+ return runClaudeJson(claudePath, model, spawnImpl, input, (stdout) => assertDistillOutput(extractJsonObject(stdout)), 'distill LLM', opts.timeoutMs);
351
+ };
352
+ }
353
+ /**
354
+ * Build the Codex distill LLM port. This mirrors {@link createClaudeDistiller}
355
+ * but invokes `codex exec` with read-only sandboxing, no approvals, ephemeral
356
+ * session state, and a JSON schema file for structured final output.
357
+ */
358
+ export function createCodexDistiller(opts = {}) {
359
+ const codexPath = opts.codexPath ?? DEFAULT_CODEX_PATH;
360
+ const model = opts.model ?? DEFAULT_CODEX_MODEL;
361
+ const spawnImpl = opts.spawnImpl ?? ((command, args) => spawn(command, [...args]));
362
+ return async function distill(cluster) {
363
+ const { JINN_SKILL_DISTILL_PROMPT_V1 } = await import('./distill-prompt.js');
364
+ const input = buildDistillInput(JINN_SKILL_DISTILL_PROMPT_V1, cluster);
365
+ return runCodexJson(codexPath, model, spawnImpl, input, DISTILL_OUTPUT_SCHEMA, (stdout) => assertDistillOutput(extractJsonObject(stdout)), 'distill LLM', opts.timeoutMs);
366
+ };
367
+ }
368
+ /**
369
+ * Serialize a meta-cluster into the model input appended after the meta prompt:
370
+ * the POLARITY (so the model keeps the right voice — diagnosis-only for
371
+ * failure-lesson) and each source labelled with its opaque id. Only id + the
372
+ * distilled skill's name/description/body are sent — evidence refs and instance
373
+ * ids are audit metadata the model must not echo.
374
+ */
375
+ function serializeMetaCluster(cluster) {
376
+ const sources = cluster.sources
377
+ .map((s) => `--- ${s.id} ---\nname: ${s.name}\ndescription: ${s.description}\nbody:\n${s.body}`)
378
+ .join('\n\n');
379
+ return `POLARITY = ${cluster.polarity}\n\nSOURCES (already-distilled skills, one per instance):\n${sources}`;
380
+ }
381
+ /** Build the full meta model input: the versioned meta prompt + the sources + the JSON contract. */
382
+ export function buildMetaDistillInput(prompt, cluster) {
383
+ return [
384
+ prompt,
385
+ '',
386
+ serializeMetaCluster(cluster),
387
+ '',
388
+ 'Return ONLY a single JSON object with exactly these fields, and nothing else:',
389
+ '{ "name": "...", "description": "...", "body": "...", "supports": ["s1", "s2"] }',
390
+ '- name: lowercase-hyphen skill name.',
391
+ '- description: the retrieval surface ("Use when … Not for: …").',
392
+ '- body: the markdown SKILL.md body (the five fixed sections).',
393
+ '- supports: the source ids (s1, s2, …) that corroborate the rule; at least two DISTINCT sources.',
394
+ ].join('\n');
395
+ }
396
+ /** Validate the parsed object is a { name, description, body, supports } meta output. */
397
+ function assertMetaDistillOutput(parsed) {
398
+ const obj = assertSkillOutputFields(parsed, 'meta-distill LLM');
399
+ if (!Array.isArray(obj.supports) || !obj.supports.every((s) => typeof s === 'string' && s.length > 0)) {
400
+ throw new Error('meta-distill LLM: model output missing/invalid field "supports" (expected a string[])');
401
+ }
402
+ return {
403
+ name: obj.name,
404
+ description: obj.description,
405
+ body: obj.body,
406
+ supports: obj.supports,
407
+ };
408
+ }
409
+ /**
410
+ * Build the stage-2 meta-distill LLM port (issue #1463). Same spawn/parse
411
+ * scaffolding as {@link createClaudeDistiller}: spawns `claude -p --model`,
412
+ * pipes {@link buildMetaDistillInput} on stdin, and parses the strict JSON
413
+ * (tolerating a prose wrapper via the shared {@link extractJsonObject}).
414
+ */
415
+ export function createClaudeMetaDistiller(opts = {}) {
416
+ const claudePath = opts.claudePath ?? DEFAULT_CLAUDE_PATH;
417
+ const model = opts.model ?? DEFAULT_MODEL;
418
+ const spawnImpl = opts.spawnImpl ?? ((command, args) => spawn(command, [...args]));
419
+ return async function metaDistillPort(cluster) {
420
+ const { JINN_SKILL_META_DISTILL_PROMPT_V1 } = await import('./distill-prompt.js');
421
+ const input = buildMetaDistillInput(JINN_SKILL_META_DISTILL_PROMPT_V1, cluster);
422
+ return runClaudeJson(claudePath, model, spawnImpl, input, (stdout) => assertMetaDistillOutput(extractJsonObject(stdout)), 'meta-distill LLM', opts.timeoutMs);
423
+ };
424
+ }
425
+ /**
426
+ * Build the Codex stage-2 meta-distill port. Same prompt and validation as the
427
+ * Claude meta port, with Codex structured output enforced by JSON schema.
428
+ */
429
+ export function createCodexMetaDistiller(opts = {}) {
430
+ const codexPath = opts.codexPath ?? DEFAULT_CODEX_PATH;
431
+ const model = opts.model ?? DEFAULT_CODEX_MODEL;
432
+ const spawnImpl = opts.spawnImpl ?? ((command, args) => spawn(command, [...args]));
433
+ return async function metaDistillPort(cluster) {
434
+ const { JINN_SKILL_META_DISTILL_PROMPT_V1 } = await import('./distill-prompt.js');
435
+ const input = buildMetaDistillInput(JINN_SKILL_META_DISTILL_PROMPT_V1, cluster);
436
+ return runCodexJson(codexPath, model, spawnImpl, input, META_DISTILL_OUTPUT_SCHEMA, (stdout) => assertMetaDistillOutput(extractJsonObject(stdout)), 'meta-distill LLM', opts.timeoutMs);
437
+ };
438
+ }
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ interface McpToolResponse extends Record<string, unknown> {
4
+ content: Array<{
5
+ type: 'text';
6
+ text: string;
7
+ }>;
8
+ isError?: true;
9
+ }
10
+ type SpawnFn = typeof spawn;
11
+ export interface DistillMcpDeps {
12
+ spawn?: SpawnFn;
13
+ env?: NodeJS.ProcessEnv;
14
+ platform?: NodeJS.Platform;
15
+ }
16
+ export declare function createDistillMcpServer(deps?: DistillMcpDeps): McpServer;
17
+ export interface LocalDistillRunArgs {
18
+ episodesDir?: string;
19
+ capturesDir?: string;
20
+ out?: string;
21
+ limit?: number;
22
+ traceIds?: string[];
23
+ sessionIds?: string[];
24
+ install?: string;
25
+ resume?: boolean;
26
+ distiller?: 'claude' | 'codex';
27
+ distillerModel?: string;
28
+ }
29
+ export declare function runLocalDistill(args: LocalDistillRunArgs, deps?: DistillMcpDeps): Promise<McpToolResponse>;
30
+ export {};