@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.
- package/dist/adapters/contribution-adapter.d.ts +18 -0
- package/dist/adapters/contribution-adapter.js +134 -0
- package/dist/adapters/corpus-adapter.d.ts +12 -0
- package/dist/adapters/corpus-adapter.js +212 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/json-map-store.d.ts +4 -0
- package/dist/adapters/json-map-store.js +31 -0
- package/dist/adapters/local-learning-adapter.d.ts +25 -0
- package/dist/adapters/local-learning-adapter.js +49 -0
- package/dist/adapters/skills-adapter.d.ts +8 -0
- package/dist/adapters/skills-adapter.js +55 -0
- package/dist/admission-policy.d.ts +38 -0
- package/dist/admission-policy.js +17 -0
- package/dist/bin/jinn-distill-mcp.d.ts +2 -0
- package/dist/bin/jinn-distill-mcp.js +10 -0
- package/dist/bin/jinn-layer.d.ts +7 -0
- package/dist/bin/jinn-layer.js +15 -0
- package/dist/bridge-fetch-evidence.d.ts +102 -0
- package/dist/bridge-fetch-evidence.js +677 -0
- package/dist/bridge-verdict-source.d.ts +47 -0
- package/dist/bridge-verdict-source.js +216 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +439 -0
- package/dist/capture.d.ts +98 -0
- package/dist/capture.js +617 -0
- package/dist/cli.d.ts +148 -0
- package/dist/cli.js +2221 -0
- package/dist/cluster-selection.d.ts +34 -0
- package/dist/cluster-selection.js +135 -0
- package/dist/cluster.d.ts +89 -0
- package/dist/cluster.js +142 -0
- package/dist/consume.d.ts +139 -0
- package/dist/consume.js +280 -0
- package/dist/corpus-probes.d.ts +61 -0
- package/dist/corpus-probes.js +120 -0
- package/dist/corpus-store.d.ts +16 -0
- package/dist/corpus-store.js +101 -0
- package/dist/distill-captures.d.ts +56 -0
- package/dist/distill-captures.js +283 -0
- package/dist/distill-feedback.d.ts +20 -0
- package/dist/distill-feedback.js +30 -0
- package/dist/distill-llm.d.ts +138 -0
- package/dist/distill-llm.js +438 -0
- package/dist/distill-mcp-server.d.ts +30 -0
- package/dist/distill-mcp-server.js +281 -0
- package/dist/distill-mode.d.ts +62 -0
- package/dist/distill-mode.js +107 -0
- package/dist/distill-progress.d.ts +78 -0
- package/dist/distill-progress.js +69 -0
- package/dist/distill-prompt.d.ts +34 -0
- package/dist/distill-prompt.js +85 -0
- package/dist/distill-render.d.ts +123 -0
- package/dist/distill-render.js +303 -0
- package/dist/distill-runs.d.ts +28 -0
- package/dist/distill-runs.js +39 -0
- package/dist/distill-traces.d.ts +67 -0
- package/dist/distill-traces.js +211 -0
- package/dist/distill.d.ts +180 -0
- package/dist/distill.js +337 -0
- package/dist/distiller.d.ts +118 -0
- package/dist/distiller.js +142 -0
- package/dist/envelope.d.ts +9 -0
- package/dist/envelope.js +9 -0
- package/dist/eval-prep.d.ts +129 -0
- package/dist/eval-prep.js +523 -0
- package/dist/execution-publish.d.ts +62 -0
- package/dist/execution-publish.js +72 -0
- package/dist/gate.d.ts +39 -0
- package/dist/gate.js +106 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +50 -0
- package/dist/ipfs-cid.d.ts +18 -0
- package/dist/ipfs-cid.js +149 -0
- package/dist/layer-default.d.ts +3 -0
- package/dist/layer-default.js +10 -0
- package/dist/ledger.d.ts +62 -0
- package/dist/ledger.js +104 -0
- package/dist/measurement.d.ts +90 -0
- package/dist/measurement.js +134 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +303 -0
- package/dist/pipeline.d.ts +94 -0
- package/dist/pipeline.js +124 -0
- package/dist/plugin-wiring.d.ts +4 -0
- package/dist/plugin-wiring.js +54 -0
- package/dist/preview.d.ts +31 -0
- package/dist/preview.js +28 -0
- package/dist/process-contract.d.ts +303 -0
- package/dist/process-contract.js +118 -0
- package/dist/publish-skill.d.ts +86 -0
- package/dist/publish-skill.js +116 -0
- package/dist/publish.d.ts +294 -0
- package/dist/publish.js +1090 -0
- package/dist/scripts/scan-skills-registry.d.ts +24 -0
- package/dist/scripts/scan-skills-registry.js +207 -0
- package/dist/seed-import/curated-batch.d.ts +36 -0
- package/dist/seed-import/curated-batch.js +103 -0
- package/dist/seed-import/episode-execute.d.ts +50 -0
- package/dist/seed-import/episode-execute.js +235 -0
- package/dist/seed-import/episode-fetch.d.ts +119 -0
- package/dist/seed-import/episode-fetch.js +137 -0
- package/dist/seed-import/episode-plan.d.ts +11 -0
- package/dist/seed-import/episode-plan.js +27 -0
- package/dist/seed-import/episode-report.d.ts +39 -0
- package/dist/seed-import/episode-report.js +36 -0
- package/dist/seed-import/execute.d.ts +61 -0
- package/dist/seed-import/execute.js +245 -0
- package/dist/seed-import/fetch.d.ts +62 -0
- package/dist/seed-import/fetch.js +82 -0
- package/dist/seed-import/licence.d.ts +17 -0
- package/dist/seed-import/licence.js +36 -0
- package/dist/seed-import/plan.d.ts +10 -0
- package/dist/seed-import/plan.js +21 -0
- package/dist/seed-import/report.d.ts +34 -0
- package/dist/seed-import/report.js +31 -0
- package/dist/seed-import/state.d.ts +72 -0
- package/dist/seed-import/state.js +120 -0
- package/dist/signal.d.ts +36 -0
- package/dist/signal.js +48 -0
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +21 -0
- package/dist/skill-package.d.ts +70 -0
- package/dist/skill-package.js +141 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +77 -0
- package/dist/snapshot-transcript.d.ts +54 -0
- package/dist/snapshot-transcript.js +117 -0
- package/dist/three-arm-run.d.ts +27 -0
- package/dist/three-arm-run.js +41 -0
- 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 {};
|