@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,281 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { z } from 'zod/v3';
|
|
8
|
+
import { DEFAULT_CAPTURES_DIR, DEFAULT_DISTILL_CAPTURE_LIMIT, DEFAULT_EPISODES_DIR, loadRecentDistillSources, } from './distill-captures.js';
|
|
9
|
+
import { clusterTraceCards, readTrace, searchTraceCards, traceCardFromCapture, } from './distill-traces.js';
|
|
10
|
+
import { recordDistillFeedback } from './distill-feedback.js';
|
|
11
|
+
const traceReadModes = ['summary', 'events', 'tool_calls', 'transcript_excerpt', 'full_transcript'];
|
|
12
|
+
export function createDistillMcpServer(deps = {}) {
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: 'jinn-local-distill',
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
});
|
|
17
|
+
server.tool('distill_trace_search', 'Search local trace cards from canonical local episodes (plus a read-only legacy capture fallback). Use this before reading full traces; returns compact cards only.', {
|
|
18
|
+
episodesDir: z.string().optional().describe('Directory containing canonical EpisodeV1 files. Defaults to JINN_LAYER_EPISODES_DIR or ~/.jinn-client/harness-layer/episodes.'),
|
|
19
|
+
capturesDir: z.string().optional().describe('Deprecated directory containing legacy CapturedTask JSON files.'),
|
|
20
|
+
limit: z.number().int().positive().max(500).optional().describe('Maximum captures to load and maximum cards to return.'),
|
|
21
|
+
query: z.string().optional().describe('Free-text query across summary, tools, commands, files, errors, and outcome.'),
|
|
22
|
+
command: z.string().optional().describe('Command substring filter.'),
|
|
23
|
+
file: z.string().optional().describe('Touched-file substring filter.'),
|
|
24
|
+
error: z.string().optional().describe('Error substring filter.'),
|
|
25
|
+
tool: z.string().optional().describe('Tool-name substring filter.'),
|
|
26
|
+
outcome: z.enum(['completed', 'failed', 'abandoned']).optional().describe('Outcome status filter.'),
|
|
27
|
+
}, async (args) => {
|
|
28
|
+
const source = distillSourceDirs(args, deps.env);
|
|
29
|
+
const limit = args.limit ?? DEFAULT_DISTILL_CAPTURE_LIMIT;
|
|
30
|
+
const captures = await loadRecentDistillSources({ ...source, limit });
|
|
31
|
+
const cards = searchTraceCards(captures.map(traceCardFromCapture), {
|
|
32
|
+
query: args.query,
|
|
33
|
+
command: args.command,
|
|
34
|
+
file: args.file,
|
|
35
|
+
error: args.error,
|
|
36
|
+
tool: args.tool,
|
|
37
|
+
outcome: args.outcome,
|
|
38
|
+
limit,
|
|
39
|
+
});
|
|
40
|
+
return jsonResponse({ ...source, count: cards.length, cards });
|
|
41
|
+
});
|
|
42
|
+
server.tool('distill_trace_read', 'Read a selected local trace by traceId or sessionId. Prefer summary, transcript_excerpt, or tool_calls before requesting full_transcript.', {
|
|
43
|
+
episodesDir: z.string().optional().describe('Directory containing canonical EpisodeV1 files.'),
|
|
44
|
+
capturesDir: z.string().optional().describe('Deprecated directory containing legacy CapturedTask JSON files.'),
|
|
45
|
+
limit: z.number().int().positive().max(500).optional().describe('Maximum captures to load while locating the trace.'),
|
|
46
|
+
traceId: z.string().optional().describe('Trace id returned by distill_trace_search, e.g. local-capture:<sessionId>.'),
|
|
47
|
+
sessionId: z.string().optional().describe('Raw session id when traceId is not available.'),
|
|
48
|
+
mode: z.enum(traceReadModes).optional().describe('Read mode. full_transcript requires allowFullTranscript=true.'),
|
|
49
|
+
query: z.string().optional().describe('Optional excerpt filter for transcript_excerpt mode.'),
|
|
50
|
+
readLimit: z.number().int().positive().max(200).optional().describe('Maximum events returned for transcript_excerpt mode.'),
|
|
51
|
+
allowFullTranscript: z.boolean().optional().describe('Required explicit opt-in for full_transcript reads.'),
|
|
52
|
+
}, async (args) => {
|
|
53
|
+
const source = distillSourceDirs(args, deps.env);
|
|
54
|
+
const captures = await loadRecentDistillSources({
|
|
55
|
+
...source,
|
|
56
|
+
limit: args.limit ?? DEFAULT_DISTILL_CAPTURE_LIMIT,
|
|
57
|
+
});
|
|
58
|
+
const capture = findCapture(captures, { traceId: args.traceId, sessionId: args.sessionId });
|
|
59
|
+
if (!capture)
|
|
60
|
+
return errorResponse(`Trace not found: ${args.traceId ?? args.sessionId ?? '(missing id)'}`);
|
|
61
|
+
try {
|
|
62
|
+
return jsonResponse(readTrace(capture, {
|
|
63
|
+
mode: args.mode ?? 'summary',
|
|
64
|
+
query: args.query,
|
|
65
|
+
limit: args.readLimit,
|
|
66
|
+
allowFullTranscript: args.allowFullTranscript,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.tool('distill_trace_cluster', 'Group local trace cards into candidate skill opportunities using repeated commands, files, and error signals. This is a cheap pre-distill scan.', {
|
|
74
|
+
episodesDir: z.string().optional().describe('Directory containing canonical EpisodeV1 files.'),
|
|
75
|
+
capturesDir: z.string().optional().describe('Deprecated directory containing legacy CapturedTask JSON files.'),
|
|
76
|
+
limit: z.number().int().positive().max(500).optional().describe('Maximum captures to load.'),
|
|
77
|
+
}, async (args) => {
|
|
78
|
+
const source = distillSourceDirs(args, deps.env);
|
|
79
|
+
const captures = await loadRecentDistillSources({
|
|
80
|
+
...source,
|
|
81
|
+
limit: args.limit ?? DEFAULT_DISTILL_CAPTURE_LIMIT,
|
|
82
|
+
});
|
|
83
|
+
const candidates = clusterTraceCards(captures.map(traceCardFromCapture));
|
|
84
|
+
return jsonResponse({ ...source, count: candidates.length, candidates });
|
|
85
|
+
});
|
|
86
|
+
server.tool('distill_local', 'Run local skill distillation over captured traces by delegating to jinn-layer distill. Requires confirm:true because it can call an LLM and write staged or installed skills.', {
|
|
87
|
+
confirm: z.boolean().optional().describe('Must be true to execute. Omit or false to receive a preview envelope.'),
|
|
88
|
+
episodesDir: z.string().optional().describe('Directory containing canonical EpisodeV1 files.'),
|
|
89
|
+
capturesDir: z.string().optional().describe('Deprecated directory containing legacy CapturedTask JSON files.'),
|
|
90
|
+
out: z.string().optional().describe('Output directory for distilled skills.'),
|
|
91
|
+
limit: z.number().int().positive().max(500).optional().describe('Maximum captures to distill.'),
|
|
92
|
+
traceIds: z.array(z.string()).optional().describe('Optional selected trace IDs to distill, as returned by distill_trace_search or distill_trace_cluster.'),
|
|
93
|
+
sessionIds: z.array(z.string()).optional().describe('Optional selected raw session IDs to distill.'),
|
|
94
|
+
install: z.string().optional().describe('Install choice passed to --install: all, none, or a specific skill name.'),
|
|
95
|
+
resume: z.boolean().optional().describe('Pass --resume so already-covered sessions are skipped.'),
|
|
96
|
+
distiller: z.enum(['claude', 'codex']).optional().describe('LLM provider to use for the distiller pass.'),
|
|
97
|
+
distillerModel: z.string().optional().describe('Distiller model name. This is separate from the runtime model recorded in the skill.'),
|
|
98
|
+
}, async (args) => {
|
|
99
|
+
if (!args.confirm) {
|
|
100
|
+
return jsonResponse({
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
status: 'preview',
|
|
103
|
+
tool: 'distill_local',
|
|
104
|
+
description: 'Run local distillation over captured traces.',
|
|
105
|
+
effects: [
|
|
106
|
+
'Reads canonical local EpisodeV1 files and, when using the default location, legacy captured-task JSON as a compatibility fallback.',
|
|
107
|
+
'Calls the selected user LLM provider through the existing jinn-layer distill flow.',
|
|
108
|
+
'Writes distilled skill packages under the output/staging directory and may install selected skills.',
|
|
109
|
+
],
|
|
110
|
+
followUp: {
|
|
111
|
+
tool: 'distill_local',
|
|
112
|
+
arguments: { ...args, confirm: true },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return runLocalDistill({
|
|
117
|
+
episodesDir: args.episodesDir,
|
|
118
|
+
capturesDir: args.capturesDir,
|
|
119
|
+
out: args.out,
|
|
120
|
+
limit: args.limit,
|
|
121
|
+
traceIds: args.traceIds,
|
|
122
|
+
sessionIds: args.sessionIds,
|
|
123
|
+
install: args.install,
|
|
124
|
+
resume: args.resume,
|
|
125
|
+
distiller: args.distiller,
|
|
126
|
+
distillerModel: args.distillerModel,
|
|
127
|
+
}, deps);
|
|
128
|
+
});
|
|
129
|
+
server.tool('distill_feedback_record', 'Record local user feedback for an experimental distilled skill. Use this after a later session uses the skill and the user accepts a helped/hurt/mixed/unused verdict.', {
|
|
130
|
+
skillName: z.string().min(1).describe('Distilled skill name.'),
|
|
131
|
+
verdict: z.enum(['helped', 'hurt', 'mixed', 'unused']).describe('User-visible outcome of using the skill.'),
|
|
132
|
+
sessionId: z.string().optional().describe('Session where the skill was used, if known.'),
|
|
133
|
+
notes: z.string().optional().describe('Short user-approved note about what to improve or preserve.'),
|
|
134
|
+
acceptedChanges: z.array(z.string()).optional().describe('Concrete user-approved changes to apply in a later skill improvement pass.'),
|
|
135
|
+
feedbackPath: z.string().optional().describe('Override feedback JSONL path. Defaults to JINN_LAYER_DISTILL_FEEDBACK_PATH or ~/.jinn-client/harness-layer/distill-feedback.jsonl.'),
|
|
136
|
+
}, async (args) => {
|
|
137
|
+
const record = recordDistillFeedback({
|
|
138
|
+
skillName: args.skillName,
|
|
139
|
+
verdict: args.verdict,
|
|
140
|
+
...(args.sessionId ? { sessionId: args.sessionId } : {}),
|
|
141
|
+
...(args.notes ? { notes: args.notes } : {}),
|
|
142
|
+
...(args.acceptedChanges ? { acceptedChanges: args.acceptedChanges } : {}),
|
|
143
|
+
}, {
|
|
144
|
+
...(args.feedbackPath ? { path: args.feedbackPath } : {}),
|
|
145
|
+
...(deps.env ? { env: deps.env } : {}),
|
|
146
|
+
});
|
|
147
|
+
return jsonResponse({ recorded: record });
|
|
148
|
+
});
|
|
149
|
+
return server;
|
|
150
|
+
}
|
|
151
|
+
export async function runLocalDistill(args, deps = {}) {
|
|
152
|
+
const resolved = resolveJinnLayerCommand(deps.env, deps.platform);
|
|
153
|
+
const hasSelection = (args.traceIds?.length ?? 0) > 0 || (args.sessionIds?.length ?? 0) > 0;
|
|
154
|
+
// Preserve the synchronous spawn boundary for ordinary calls. Selection
|
|
155
|
+
// needs an async canonical-store read; an unselected run does not.
|
|
156
|
+
const selected = hasSelection ? await materializeSelectedCaptures(args, deps.env) : {};
|
|
157
|
+
if (selected.error)
|
|
158
|
+
return errorResponse(selected.error);
|
|
159
|
+
const argv = [
|
|
160
|
+
...resolved.prefixArgs,
|
|
161
|
+
'distill',
|
|
162
|
+
'--where',
|
|
163
|
+
'local',
|
|
164
|
+
'--json',
|
|
165
|
+
];
|
|
166
|
+
const capturesDir = selected.capturesDir ?? args.capturesDir;
|
|
167
|
+
if (!selected.capturesDir && args.episodesDir)
|
|
168
|
+
argv.push('--episodes', args.episodesDir);
|
|
169
|
+
if (capturesDir)
|
|
170
|
+
argv.push('--captures', capturesDir);
|
|
171
|
+
if (args.out)
|
|
172
|
+
argv.push('--out', args.out);
|
|
173
|
+
if (args.limit !== undefined)
|
|
174
|
+
argv.push('--limit', String(args.limit));
|
|
175
|
+
if (args.install)
|
|
176
|
+
argv.push('--install', args.install);
|
|
177
|
+
if (args.resume)
|
|
178
|
+
argv.push('--resume');
|
|
179
|
+
if (args.distiller)
|
|
180
|
+
argv.push('--distiller', args.distiller);
|
|
181
|
+
if (args.distillerModel)
|
|
182
|
+
argv.push('--distiller-model', args.distillerModel);
|
|
183
|
+
const spawnFn = deps.spawn ?? spawn;
|
|
184
|
+
const child = spawnFn(resolved.command, argv, {
|
|
185
|
+
env: { ...process.env, ...(deps.env ?? {}) },
|
|
186
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
187
|
+
});
|
|
188
|
+
try {
|
|
189
|
+
const result = await collectChild(child);
|
|
190
|
+
return jsonResponse({
|
|
191
|
+
command: [basename(resolved.command), ...argv],
|
|
192
|
+
exitCode: result.exitCode,
|
|
193
|
+
stdout: result.stdout,
|
|
194
|
+
stderr: result.stderr,
|
|
195
|
+
...(selected.traceCount !== undefined ? { selectedTraceCount: selected.traceCount } : {}),
|
|
196
|
+
}, result.exitCode === 0 ? undefined : true);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (selected.tempDir)
|
|
200
|
+
rmSync(selected.tempDir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function jsonResponse(value, isError) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
206
|
+
...(isError ? { isError } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function errorResponse(message) {
|
|
210
|
+
return jsonResponse({ error: { message } }, true);
|
|
211
|
+
}
|
|
212
|
+
function distillSourceDirs(args, env = process.env) {
|
|
213
|
+
const episodesOverride = args.episodesDir ?? env['JINN_LAYER_EPISODES_DIR'];
|
|
214
|
+
const legacyOverride = args.capturesDir ?? env['JINN_LAYER_CAPTURES_DIR'];
|
|
215
|
+
const episodesDir = episodesOverride ?? legacyOverride ?? DEFAULT_EPISODES_DIR;
|
|
216
|
+
const legacyCapturesDir = legacyOverride ?? (episodesOverride ? undefined : DEFAULT_CAPTURES_DIR);
|
|
217
|
+
return {
|
|
218
|
+
episodesDir,
|
|
219
|
+
...(legacyCapturesDir ? { legacyCapturesDir } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function findCapture(captures, ids) {
|
|
223
|
+
const sessionId = ids.sessionId ?? ids.traceId?.replace(/^local-capture:/, '');
|
|
224
|
+
if (!sessionId)
|
|
225
|
+
return undefined;
|
|
226
|
+
return captures.find((capture) => capture.session.sessionId === sessionId);
|
|
227
|
+
}
|
|
228
|
+
function resolveJinnLayerCommand(env = process.env, platform = process.platform) {
|
|
229
|
+
const sibling = fileURLToPath(new URL('./bin/jinn-layer.js', import.meta.url));
|
|
230
|
+
if (platform === 'win32') {
|
|
231
|
+
if (!existsSync(sibling)) {
|
|
232
|
+
throw new Error(`co-installed jinn-layer CLI is missing on Windows: ${sibling}`);
|
|
233
|
+
}
|
|
234
|
+
return { command: process.execPath, prefixArgs: [sibling] };
|
|
235
|
+
}
|
|
236
|
+
if (env['JINN_LAYER_BIN'])
|
|
237
|
+
return { command: env['JINN_LAYER_BIN'], prefixArgs: [] };
|
|
238
|
+
if (existsSync(sibling))
|
|
239
|
+
return { command: process.execPath, prefixArgs: [sibling] };
|
|
240
|
+
return { command: 'jinn-layer', prefixArgs: [] };
|
|
241
|
+
}
|
|
242
|
+
async function materializeSelectedCaptures(args, env = process.env) {
|
|
243
|
+
const selectedIds = new Set([
|
|
244
|
+
...(args.sessionIds ?? []),
|
|
245
|
+
...(args.traceIds ?? []).map((id) => id.replace(/^local-capture:/, '')),
|
|
246
|
+
].filter((id) => id.trim() !== ''));
|
|
247
|
+
if (selectedIds.size === 0)
|
|
248
|
+
return {};
|
|
249
|
+
const source = distillSourceDirs(args, env);
|
|
250
|
+
const captures = (await loadRecentDistillSources({
|
|
251
|
+
...source,
|
|
252
|
+
limit: args.limit ?? DEFAULT_DISTILL_CAPTURE_LIMIT,
|
|
253
|
+
}))
|
|
254
|
+
.filter((capture) => selectedIds.has(capture.session.sessionId));
|
|
255
|
+
if (captures.length === 0) {
|
|
256
|
+
return { error: `No selected traces found in ${source.episodesDir}` };
|
|
257
|
+
}
|
|
258
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'jinn-distill-selected-'));
|
|
259
|
+
for (const capture of captures) {
|
|
260
|
+
writeFileSync(join(tempDir, `${safeFilePart(capture.session.sessionId)}.json`), `${JSON.stringify(capture, null, 2)}\n`, { mode: 0o600 });
|
|
261
|
+
}
|
|
262
|
+
return { capturesDir: tempDir, tempDir, traceCount: captures.length };
|
|
263
|
+
}
|
|
264
|
+
function safeFilePart(value) {
|
|
265
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128);
|
|
266
|
+
return safe || 'capture';
|
|
267
|
+
}
|
|
268
|
+
function collectChild(child) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
let stdout = '';
|
|
271
|
+
let stderr = '';
|
|
272
|
+
child.stdout?.on('data', (chunk) => {
|
|
273
|
+
stdout += chunk.toString();
|
|
274
|
+
});
|
|
275
|
+
child.stderr?.on('data', (chunk) => {
|
|
276
|
+
stderr += chunk.toString();
|
|
277
|
+
});
|
|
278
|
+
child.once('error', reject);
|
|
279
|
+
child.once('close', (code) => resolve({ exitCode: code, stdout, stderr }));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `distill` where-it-runs mode — the persistent consent setting (issue #1490).
|
|
3
|
+
*
|
|
4
|
+
* A three-value setting: `local` runs a frontier pass here now, `defer` holds
|
|
5
|
+
* the operator's captures locally and runs nothing, `off` stops reserving
|
|
6
|
+
* captures for distillation at all. An absent (or unrecognised) setting reads
|
|
7
|
+
* as `unset`, which triggers the first-run consent flow.
|
|
8
|
+
*
|
|
9
|
+
* The mode is a real fact on disk, not an interactive-only prompt: the flag
|
|
10
|
+
* (`distill --where <mode>`) and the prompt write the same `{ where }` value, so
|
|
11
|
+
* a script can set it without a TTY (mirrors `cli/commands/auth.ts`, the one
|
|
12
|
+
* interactive-prompt precedent in the client). It lives next to the harness
|
|
13
|
+
* layer's other operator state (captures, skills, ledger) under
|
|
14
|
+
* `~/.jinn-client/harness-layer/`.
|
|
15
|
+
*/
|
|
16
|
+
/** The three where-it-runs modes the operator can record. */
|
|
17
|
+
export type DistillMode = 'local' | 'defer' | 'off';
|
|
18
|
+
/** A recorded mode, or `unset` when none is on disk (→ first-run consent). */
|
|
19
|
+
export type DistillModeState = DistillMode | 'unset';
|
|
20
|
+
/** The default mode file, alongside the layer's other operator state. */
|
|
21
|
+
export declare const DEFAULT_DISTILL_MODE_PATH: string;
|
|
22
|
+
/**
|
|
23
|
+
* The pre-rename (single-l) mode file (#1532), read as a fallback when the
|
|
24
|
+
* path being read is absent — a mode recorded before the rename is still
|
|
25
|
+
* honoured. Never written.
|
|
26
|
+
*/
|
|
27
|
+
export declare const LEGACY_DISTILL_MODE_PATH: string;
|
|
28
|
+
/** The two distiller providers the persisted default may name. */
|
|
29
|
+
export type DistillDefaultProvider = 'claude' | 'codex';
|
|
30
|
+
/** The persisted per-axis distiller defaults, sibling to `where`. */
|
|
31
|
+
export interface DistillDefaults {
|
|
32
|
+
distiller?: DistillDefaultProvider;
|
|
33
|
+
distillerModel?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the recorded mode. A missing file, malformed JSON, or a `where` value
|
|
37
|
+
* that is not one of the three modes all read as `unset` — the fail-safe that
|
|
38
|
+
* routes a bare `distill` to the first-run consent rather than to a run.
|
|
39
|
+
*
|
|
40
|
+
* When `path` is absent, the legacy single-l file is consulted (#1532) so a
|
|
41
|
+
* mode recorded before the rename still counts. Defaults to the legacy path
|
|
42
|
+
* only when reading the default path — a custom `path` opts out unless a
|
|
43
|
+
* `legacyPath` is passed explicitly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readDistillMode(path?: string, legacyPath?: string | undefined): DistillModeState;
|
|
46
|
+
/**
|
|
47
|
+
* Persist the mode as the design's `{ where }` shape, creating the parent
|
|
48
|
+
* directory if needed. The flag and the prompt both call this, so the two paths
|
|
49
|
+
* write identically.
|
|
50
|
+
*/
|
|
51
|
+
export declare function writeDistillMode(mode: DistillMode, path?: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Read the persisted distiller defaults. Same fail-safe as `readDistillMode`:
|
|
54
|
+
* an unrecognised `distiller` or a non-string `distillerModel` reads as absent,
|
|
55
|
+
* so a corrupt field falls back to the provider default rather than erroring.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readDistillDefaults(path?: string): DistillDefaults;
|
|
58
|
+
/**
|
|
59
|
+
* Merge the given distiller-default patch into the doc, preserving `where` and
|
|
60
|
+
* any default not named in the patch. Writes the same pretty JSON shape.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeDistillDefaults(patch: DistillDefaults, path?: string): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `distill` where-it-runs mode — the persistent consent setting (issue #1490).
|
|
3
|
+
*
|
|
4
|
+
* A three-value setting: `local` runs a frontier pass here now, `defer` holds
|
|
5
|
+
* the operator's captures locally and runs nothing, `off` stops reserving
|
|
6
|
+
* captures for distillation at all. An absent (or unrecognised) setting reads
|
|
7
|
+
* as `unset`, which triggers the first-run consent flow.
|
|
8
|
+
*
|
|
9
|
+
* The mode is a real fact on disk, not an interactive-only prompt: the flag
|
|
10
|
+
* (`distill --where <mode>`) and the prompt write the same `{ where }` value, so
|
|
11
|
+
* a script can set it without a TTY (mirrors `cli/commands/auth.ts`, the one
|
|
12
|
+
* interactive-prompt precedent in the client). It lives next to the harness
|
|
13
|
+
* layer's other operator state (captures, skills, ledger) under
|
|
14
|
+
* `~/.jinn-client/harness-layer/`.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
/** The default mode file, alongside the layer's other operator state. */
|
|
20
|
+
export const DEFAULT_DISTILL_MODE_PATH = join(homedir(), '.jinn-client', 'harness-layer', 'distill.json');
|
|
21
|
+
/**
|
|
22
|
+
* The pre-rename (single-l) mode file (#1532), read as a fallback when the
|
|
23
|
+
* path being read is absent — a mode recorded before the rename is still
|
|
24
|
+
* honoured. Never written.
|
|
25
|
+
*/
|
|
26
|
+
export const LEGACY_DISTILL_MODE_PATH = join(homedir(), '.jinn-client', 'harness-layer', 'distil.json');
|
|
27
|
+
const MODES = ['local', 'defer', 'off'];
|
|
28
|
+
function isDistillMode(value) {
|
|
29
|
+
return typeof value === 'string' && MODES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
const PROVIDERS = ['claude', 'codex'];
|
|
32
|
+
function isDefaultProvider(value) {
|
|
33
|
+
return typeof value === 'string' && PROVIDERS.includes(value);
|
|
34
|
+
}
|
|
35
|
+
/** Parse the whole doc off disk once; a missing/malformed file is `{}`. */
|
|
36
|
+
function readDoc(path) {
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
return {};
|
|
39
|
+
try {
|
|
40
|
+
const doc = JSON.parse(readFileSync(path, 'utf-8'));
|
|
41
|
+
return typeof doc === 'object' && doc !== null ? doc : {};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read the recorded mode. A missing file, malformed JSON, or a `where` value
|
|
49
|
+
* that is not one of the three modes all read as `unset` — the fail-safe that
|
|
50
|
+
* routes a bare `distill` to the first-run consent rather than to a run.
|
|
51
|
+
*
|
|
52
|
+
* When `path` is absent, the legacy single-l file is consulted (#1532) so a
|
|
53
|
+
* mode recorded before the rename still counts. Defaults to the legacy path
|
|
54
|
+
* only when reading the default path — a custom `path` opts out unless a
|
|
55
|
+
* `legacyPath` is passed explicitly.
|
|
56
|
+
*/
|
|
57
|
+
export function readDistillMode(path = DEFAULT_DISTILL_MODE_PATH, legacyPath = path === DEFAULT_DISTILL_MODE_PATH
|
|
58
|
+
? LEGACY_DISTILL_MODE_PATH
|
|
59
|
+
: undefined) {
|
|
60
|
+
if (!existsSync(path)) {
|
|
61
|
+
if (legacyPath !== undefined && existsSync(legacyPath)) {
|
|
62
|
+
return readDistillMode(legacyPath, undefined);
|
|
63
|
+
}
|
|
64
|
+
return 'unset';
|
|
65
|
+
}
|
|
66
|
+
const doc = readDoc(path);
|
|
67
|
+
return isDistillMode(doc.where) ? doc.where : 'unset';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Persist the mode as the design's `{ where }` shape, creating the parent
|
|
71
|
+
* directory if needed. The flag and the prompt both call this, so the two paths
|
|
72
|
+
* write identically.
|
|
73
|
+
*/
|
|
74
|
+
export function writeDistillMode(mode, path = DEFAULT_DISTILL_MODE_PATH) {
|
|
75
|
+
const doc = readDoc(path);
|
|
76
|
+
doc.where = mode;
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + '\n', { encoding: 'utf-8' });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the persisted distiller defaults. Same fail-safe as `readDistillMode`:
|
|
82
|
+
* an unrecognised `distiller` or a non-string `distillerModel` reads as absent,
|
|
83
|
+
* so a corrupt field falls back to the provider default rather than erroring.
|
|
84
|
+
*/
|
|
85
|
+
export function readDistillDefaults(path = DEFAULT_DISTILL_MODE_PATH) {
|
|
86
|
+
const doc = readDoc(path);
|
|
87
|
+
const out = {};
|
|
88
|
+
if (isDefaultProvider(doc.distiller))
|
|
89
|
+
out.distiller = doc.distiller;
|
|
90
|
+
if (typeof doc.distillerModel === 'string' && doc.distillerModel !== '') {
|
|
91
|
+
out.distillerModel = doc.distillerModel;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Merge the given distiller-default patch into the doc, preserving `where` and
|
|
97
|
+
* any default not named in the patch. Writes the same pretty JSON shape.
|
|
98
|
+
*/
|
|
99
|
+
export function writeDistillDefaults(patch, path = DEFAULT_DISTILL_MODE_PATH) {
|
|
100
|
+
const doc = readDoc(path);
|
|
101
|
+
if (patch.distiller !== undefined)
|
|
102
|
+
doc.distiller = patch.distiller;
|
|
103
|
+
if (patch.distillerModel !== undefined)
|
|
104
|
+
doc.distillerModel = patch.distillerModel;
|
|
105
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
106
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + '\n', { encoding: 'utf-8' });
|
|
107
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON progress events for a `distill` run (#1533).
|
|
3
|
+
*
|
|
4
|
+
* A run is a silent multi-minute blocking call (one frontier subprocess per
|
|
5
|
+
* cluster), so any wrapper — the harness plugin's background runner, a script,
|
|
6
|
+
* CI — needs a machine-readable event stream to show progress ambiently. The
|
|
7
|
+
* emitter writes one JSON object per line to the given stream (the CLI passes
|
|
8
|
+
* stderr; stdout stays reserved for the `--json` result), stamping every line
|
|
9
|
+
* with a schema version, timestamp, and run id.
|
|
10
|
+
*
|
|
11
|
+
* Consumers MUST ignore lines that do not parse as JSON or lack the `v` field:
|
|
12
|
+
* the stream is shared with plain-text warnings (e.g. malformed-capture skips).
|
|
13
|
+
*
|
|
14
|
+
* Ordering guarantee: `run_start` → `cluster_plan` → (`cluster_start` →
|
|
15
|
+
* `heartbeat`* → `cluster_end`)* → `run_end`; `run_end` is always last and is
|
|
16
|
+
* emitted on every terminal branch, so a watcher can treat it as the single
|
|
17
|
+
* end-of-run signal.
|
|
18
|
+
*/
|
|
19
|
+
/** Anything line-writable — `process.stderr` in production, a sink in tests. */
|
|
20
|
+
export interface ProgressStream {
|
|
21
|
+
write(chunk: string): unknown;
|
|
22
|
+
}
|
|
23
|
+
/** Mint a per-invocation run id: `distill-<unixms>-<4hex>`. */
|
|
24
|
+
export declare function newRunId(): string;
|
|
25
|
+
export interface RunStartFields {
|
|
26
|
+
capturesConsidered: number;
|
|
27
|
+
toDistill: number;
|
|
28
|
+
resume: boolean;
|
|
29
|
+
distillProvider: string;
|
|
30
|
+
distillModel: string;
|
|
31
|
+
capturesDir?: string;
|
|
32
|
+
stagingDir?: string;
|
|
33
|
+
activeDir?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ClusterPlanFields {
|
|
36
|
+
clusterCount: number;
|
|
37
|
+
clusters: Array<{
|
|
38
|
+
clusterId: string;
|
|
39
|
+
index: number;
|
|
40
|
+
captureCount: number;
|
|
41
|
+
label?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export interface ClusterStartFields {
|
|
45
|
+
clusterId: string;
|
|
46
|
+
index: number;
|
|
47
|
+
total: number;
|
|
48
|
+
label?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ClusterEndFields extends ClusterStartFields {
|
|
51
|
+
outcome: 'published' | 'rejected' | 'error';
|
|
52
|
+
skillName?: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
error?: string;
|
|
55
|
+
durationMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface RunEndFields {
|
|
58
|
+
outcome: 'ok' | 'partial' | 'empty';
|
|
59
|
+
clusterCount: number;
|
|
60
|
+
published: string[];
|
|
61
|
+
rejectedCount: number;
|
|
62
|
+
errorCount: number;
|
|
63
|
+
installed: string[];
|
|
64
|
+
stagingDir?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface NdjsonProgressEmitter {
|
|
67
|
+
runStart(fields: RunStartFields): void;
|
|
68
|
+
clusterPlan(fields: ClusterPlanFields): void;
|
|
69
|
+
clusterStart(fields: ClusterStartFields): void;
|
|
70
|
+
clusterEnd(fields: ClusterEndFields): void;
|
|
71
|
+
/** Always the last event; clears any live heartbeat and stamps the run duration. */
|
|
72
|
+
runEnd(fields: RunEndFields): void;
|
|
73
|
+
}
|
|
74
|
+
export interface ProgressEmitterOpts {
|
|
75
|
+
/** Heartbeat period while a cluster's LLM call is in flight. Default 15s. */
|
|
76
|
+
heartbeatMs?: number;
|
|
77
|
+
}
|
|
78
|
+
export declare function createNdjsonProgressEmitter(stream: ProgressStream, runId: string, opts?: ProgressEmitterOpts): NdjsonProgressEmitter;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON progress events for a `distill` run (#1533).
|
|
3
|
+
*
|
|
4
|
+
* A run is a silent multi-minute blocking call (one frontier subprocess per
|
|
5
|
+
* cluster), so any wrapper — the harness plugin's background runner, a script,
|
|
6
|
+
* CI — needs a machine-readable event stream to show progress ambiently. The
|
|
7
|
+
* emitter writes one JSON object per line to the given stream (the CLI passes
|
|
8
|
+
* stderr; stdout stays reserved for the `--json` result), stamping every line
|
|
9
|
+
* with a schema version, timestamp, and run id.
|
|
10
|
+
*
|
|
11
|
+
* Consumers MUST ignore lines that do not parse as JSON or lack the `v` field:
|
|
12
|
+
* the stream is shared with plain-text warnings (e.g. malformed-capture skips).
|
|
13
|
+
*
|
|
14
|
+
* Ordering guarantee: `run_start` → `cluster_plan` → (`cluster_start` →
|
|
15
|
+
* `heartbeat`* → `cluster_end`)* → `run_end`; `run_end` is always last and is
|
|
16
|
+
* emitted on every terminal branch, so a watcher can treat it as the single
|
|
17
|
+
* end-of-run signal.
|
|
18
|
+
*/
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
/** Mint a per-invocation run id: `distill-<unixms>-<4hex>`. */
|
|
21
|
+
export function newRunId() {
|
|
22
|
+
return `distill-${Date.now()}-${randomBytes(2).toString('hex')}`;
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_HEARTBEAT_MS = 15_000;
|
|
25
|
+
export function createNdjsonProgressEmitter(stream, runId, opts = {}) {
|
|
26
|
+
const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
27
|
+
const runStartedAt = Date.now();
|
|
28
|
+
let heartbeat;
|
|
29
|
+
function emit(event, fields) {
|
|
30
|
+
stream.write(JSON.stringify({ v: 1, event, ts: new Date().toISOString(), runId, ...fields }) + '\n');
|
|
31
|
+
}
|
|
32
|
+
function clearHeartbeat() {
|
|
33
|
+
if (heartbeat !== undefined) {
|
|
34
|
+
clearInterval(heartbeat);
|
|
35
|
+
heartbeat = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
runStart(fields) {
|
|
40
|
+
emit('run_start', { ...fields });
|
|
41
|
+
},
|
|
42
|
+
clusterPlan(fields) {
|
|
43
|
+
emit('cluster_plan', { ...fields });
|
|
44
|
+
},
|
|
45
|
+
clusterStart(fields) {
|
|
46
|
+
emit('cluster_start', { ...fields });
|
|
47
|
+
clearHeartbeat();
|
|
48
|
+
const clusterStartedAt = Date.now();
|
|
49
|
+
heartbeat = setInterval(() => {
|
|
50
|
+
emit('heartbeat', {
|
|
51
|
+
clusterId: fields.clusterId,
|
|
52
|
+
index: fields.index,
|
|
53
|
+
total: fields.total,
|
|
54
|
+
elapsedMs: Date.now() - clusterStartedAt,
|
|
55
|
+
});
|
|
56
|
+
}, heartbeatMs);
|
|
57
|
+
// Liveness only — never keep the process alive for a heartbeat.
|
|
58
|
+
heartbeat.unref?.();
|
|
59
|
+
},
|
|
60
|
+
clusterEnd(fields) {
|
|
61
|
+
clearHeartbeat();
|
|
62
|
+
emit('cluster_end', { ...fields });
|
|
63
|
+
},
|
|
64
|
+
runEnd(fields) {
|
|
65
|
+
clearHeartbeat();
|
|
66
|
+
emit('run_end', { ...fields, durationMs: Date.now() - runStartedAt });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jinn-skill-distill-prompt-v1` — the layer-1-evidence → layer-2-skill
|
|
3
|
+
* distillation prompt (spec/2026-07-06-distillation-v1.md §7, D4/D10 + v0.5).
|
|
4
|
+
*
|
|
5
|
+
* Three modes keyed to the cluster's tier (§7), following the SkillRL
|
|
6
|
+
* decomposition (arXiv 2602.08234, §2.4) plus the ExpeL contrastive axis:
|
|
7
|
+
* successes → strategic patterns; evaluator-confirmed failures → failure
|
|
8
|
+
* lessons stated as DIAGNOSIS, not prescription (the verified-counterfactual
|
|
9
|
+
* rule — the evidence verifies THAT an attempt failed, not the fix); and
|
|
10
|
+
* both-polarity instances → one contrastive skill whose counterfactual IS the
|
|
11
|
+
* verified pass. Every mode emits the fixed skeleton and a trigger/anti-trigger
|
|
12
|
+
* description. Single-shot and flat for v1 (recursion/hierarchy are v3).
|
|
13
|
+
*
|
|
14
|
+
* Like `SESSION_DERIVED_DISTILL_PROMPT_V1`, this is a foundation reference
|
|
15
|
+
* implementation, not protocol canon: a later network-task version may
|
|
16
|
+
* substitute it, and the SHA-256 below is published on every distilled skill
|
|
17
|
+
* (`metadata.jinn.distillPromptSha256`) so generated skills stay auditable.
|
|
18
|
+
*/
|
|
19
|
+
export declare const JINN_SKILL_DISTILL_PROMPT_V1 = "You distil verified agent evidence into ONE reusable Agent-Skill (a SKILL.md package).\n\nInput: a cluster of evaluator-verified traces for one coding sub-problem, and a MODE.\n\nMODE = strategic-pattern (the traces are SUCCESSES):\n- Extract the critical decision points and the generalizable behavior that made the solve work \u2014 the strategy a future agent should reuse, not the specific diff.\n\nMODE = failure-lesson (the traces are evaluator-confirmed FAILURES):\n- The evidence verifies THAT this approach failed \u2014 it does NOT verify what would have worked. State the DIAGNOSIS: the failure point and WHY the approach fails (\"this fails because \u2026\"). Do NOT prescribe a fix as fact. You MAY offer a hypothesis, but it must be explicitly marked as one (\"likely \u2026\", \"consider \u2026\") \u2014 never an imperative \"instead, do X\" or \"the correct fix is X\". A verified counterfactual is only available in contrastive mode.\n\nMODE = contrastive (the traces are BOTH a verified PASS and a confirmed FAIL of the SAME problem):\n- The delta between the pass and the fail is the signal \u2014 extract the causal decision that separates success from failure (what the passing attempt did that the failing one did not). Here the counterfactual IS verified (the pass really worked), so you MAY state it as fact.\n\nEvery skill (all modes):\n- Produce a name (lowercase-hyphen), a description, and a markdown body.\n- The description is the retrieval surface and MUST carry BOTH a trigger and an anti-trigger: \"Use when \u2026 Not for: \u2026\". The anti-trigger names the nearby-but-situationally-wrong case the skill must NOT fire on. For a failure-lesson the trigger is the RISKY situation and the anti-trigger the safe lookalike.\n- The body MUST use EXACTLY these five sections, each non-empty, in this order:\n ## When to use\n ## Strategy\n ## Steps\n ## Pitfalls\n ## Verify\n- Generalize: name the transferable rule, not the instance. Do NOT copy verbatim diff hunks, file paths, symbol names, instance ids, or PR numbers \u2014 those are contamination and are rejected downstream.\n- Never include secrets, keys, tokens, or credentials. A skill has no legitimate need to carry raw key material.\n- Use placeholder paths (/path/to/project) and invented example identifiers. Never real home directories (/Users/<name>, /home/<name>), real email addresses, or machine-specific paths \u2014 the output scrub redacts those shapes, and ANY redaction drops the whole skill (fail-closed), deterministically, on every retry.\n- Be concise. A skill that restates the raw trace has not earned its place over just retrieving the trace.";
|
|
20
|
+
export declare const JINN_SKILL_DISTILL_PROMPT_V1_SHA256 = "44bf8cad03c6d3c17dd726e0f0b9a3375703028bc4be08d15804442626c6c195";
|
|
21
|
+
/**
|
|
22
|
+
* `jinn-skill-meta-distill-prompt-v1` — the stage-2 cross-instance meta-distill
|
|
23
|
+
* prompt (issue #1463). Input is a BATCH of stage-1 skills that already share a
|
|
24
|
+
* polarity (all strategic-pattern, all failure-lesson, or all contrastive),
|
|
25
|
+
* each labelled with an opaque source id (s1, s2, …). The task is to find the
|
|
26
|
+
* ONE rule that recurs across the batch and is corroborated by AT LEAST TWO
|
|
27
|
+
* DISTINCT instances, and to name which sources corroborate it.
|
|
28
|
+
*
|
|
29
|
+
* Like the v1 distill prompt, this is a foundation reference, not protocol
|
|
30
|
+
* canon; its SHA-256 is published on every meta-distilled skill
|
|
31
|
+
* (`metadata.jinn.distillPromptSha256`).
|
|
32
|
+
*/
|
|
33
|
+
export declare const JINN_SKILL_META_DISTILL_PROMPT_V1 = "You distil a BATCH of already-distilled Agent-Skills into ONE higher-order cross-instance Agent-Skill (a SKILL.md package).\n\nInput: a batch of skills, each labelled with an opaque source id (s1, s2, \u2026). Every skill in the batch shares a POLARITY (given below), and each came from a DIFFERENT coding sub-problem instance.\n\nMODE = cross-instance:\n- Find the ONE recurring rule that generalises across the batch \u2014 the pattern, lesson, or delta that shows up in two or more of the sources for DIFFERENT instances. A rule that appears in only a single source is NOT cross-instance evidence and must not be emitted.\n- Corroboration is the signal: emit a skill only for a rule that at least two DISTINCT sources support. List those source ids in \"supports\".\n\nPOLARITY = strategic-pattern: the sources are recurring success strategies \u2014 extract the shared generalizable behavior.\nPOLARITY = failure-lesson: the sources are recurring failure diagnoses. State the shared DIAGNOSIS (\"this class of approach fails because \u2026\"). Do NOT prescribe a fix as fact \u2014 no imperative \"instead, do X\" or \"the correct fix is X\"; a hypothesis MUST be marked as one (\"likely \u2026\", \"consider \u2026\"). A verified counterfactual is only available in contrastive polarity.\nPOLARITY = contrastive: the sources are recurring pass\u2194fail deltas \u2014 extract the shared causal decision that separates success from failure; here the counterfactual IS verified, so you MAY state it as fact.\n\nEvery skill:\n- Produce a name (lowercase-hyphen), a description, a markdown body, and a \"supports\" list of the source ids that corroborate the rule.\n- The description is the retrieval surface and MUST carry BOTH a trigger and an anti-trigger: \"Use when \u2026 Not for: \u2026\".\n- The body MUST use EXACTLY these five sections, each non-empty, in this order:\n ## When to use\n ## Strategy\n ## Steps\n ## Pitfalls\n ## Verify\n- Generalize: name the transferable rule, not any single instance. Do NOT copy verbatim diff hunks, file paths, symbol names, instance ids, or PR numbers \u2014 those are contamination and are rejected downstream.\n- Never include secrets, keys, tokens, or credentials. Use placeholder paths (/path/to/project) and invented example identifiers; never real home directories, emails, or machine-specific paths \u2014 the output scrub drops the whole skill (fail-closed) on any redaction.\n- Be concise. A cross-instance skill that merely concatenates its sources has not earned its place.";
|
|
34
|
+
export declare const JINN_SKILL_META_DISTILL_PROMPT_V1_SHA256 = "6d21ed583cb8f3f3f92449c6b67a247be287a767517a9f83ec891e7f737b388b";
|