orizu 0.5.21 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/gepa-engine-dispatch.d.ts.map +1 -1
- package/dist/gepa-engine-dispatch.js +20 -1
- package/dist/gepa-python-paths.d.ts.map +1 -1
- package/dist/gepa-python-paths.js +25 -6
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +36 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -39
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +57 -5
- package/dist/instruction-sets-cli.d.ts +5 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +134 -24
- package/dist/skilled-proposer-config.d.ts +2 -0
- package/dist/skilled-proposer-config.d.ts.map +1 -0
- package/dist/skilled-proposer-config.js +212 -0
- package/dist/skilled-proposer-launch.d.ts +4 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +65 -1
- package/package.json +1 -1
- package/src/gepa-engine-dispatch.ts +21 -1
- package/src/gepa-python-paths.ts +24 -6
- package/src/help.ts +36 -14
- package/src/index.ts +6 -53
- package/src/instruction-set-loader.ts +55 -5
- package/src/instruction-sets-cli.ts +173 -19
- package/src/skilled-proposer-config.ts +191 -0
- package/src/skilled-proposer-launch.ts +59 -1
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
- package/vendor/skills/orizu-cli/SKILL.md +50 -16
- package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
- package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
- package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
- package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
- package/vendor/skills/orizu-cli/references/primer.md +21 -5
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +132 -85
|
@@ -33,6 +33,15 @@ async function responsePayload(response, action) {
|
|
|
33
33
|
const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u;
|
|
34
34
|
function safeSegment(value) { if (!SAFE_SEGMENT.test(value) || value === '.' || value === '..' || value.startsWith('.'))
|
|
35
35
|
throw new Error('instruction_set_path_unsafe'); }
|
|
36
|
+
function isSafeSegment(value) {
|
|
37
|
+
try {
|
|
38
|
+
safeSegment(value);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
function slug(identity) { return identity.replaceAll('/', '__').replace(/[^A-Za-z0-9._-]/gu, '_'); }
|
|
37
46
|
function stable(value) { return `${JSON.stringify(value, null, 2)}\n`; }
|
|
38
47
|
function sha256(value) { return createHash('sha256').update(value, 'utf8').digest('hex'); }
|
|
@@ -65,19 +74,62 @@ function removeOwnScratch(paths) {
|
|
|
65
74
|
if (path && existsSync(path))
|
|
66
75
|
rmSync(path, { recursive: true, force: true });
|
|
67
76
|
}
|
|
68
|
-
function existingDestinationIsSynced(destination,
|
|
77
|
+
function existingDestinationIsSynced(destination, set) {
|
|
78
|
+
if (set.slug &&
|
|
79
|
+
set.projectId === undefined &&
|
|
80
|
+
set.instructionSetId === undefined) {
|
|
81
|
+
return existingDestinationIsLegacySynced(destination, set.name, set.slug);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (!statSync(destination).isDirectory())
|
|
85
|
+
return false;
|
|
86
|
+
const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8'));
|
|
87
|
+
return manifest.manifestVersion === 1 && (set.slug
|
|
88
|
+
? manifest.slug === set.slug &&
|
|
89
|
+
manifest.projectId === set.projectId &&
|
|
90
|
+
manifest.instructionSetId === set.instructionSetId
|
|
91
|
+
: manifest.name === set.name);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function existingDestinationIsLegacySynced(destination, name, slug) {
|
|
69
98
|
try {
|
|
70
99
|
if (!statSync(destination).isDirectory())
|
|
71
100
|
return false;
|
|
72
101
|
const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8'));
|
|
73
|
-
return manifest.manifestVersion === 1 &&
|
|
102
|
+
return manifest.manifestVersion === 1 &&
|
|
103
|
+
manifest.name === name &&
|
|
104
|
+
manifest.slug === slug;
|
|
74
105
|
}
|
|
75
106
|
catch {
|
|
76
107
|
return false;
|
|
77
108
|
}
|
|
78
109
|
}
|
|
110
|
+
function removePublishedBackups(paths, remove) {
|
|
111
|
+
let firstError = null;
|
|
112
|
+
for (const path of paths) {
|
|
113
|
+
if (!existsSync(path))
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
remove(path, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
firstError ??= error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (firstError)
|
|
123
|
+
throw new Error('instruction_set_sync_cleanup_failed');
|
|
124
|
+
}
|
|
79
125
|
export function syncToDisk(out, set, fileOps = { writeFileSync }) {
|
|
80
|
-
|
|
126
|
+
const directoryName = set.slug || set.name;
|
|
127
|
+
safeSegment(directoryName);
|
|
128
|
+
const hasProjectId = set.projectId !== undefined;
|
|
129
|
+
const hasInstructionSetId = set.instructionSetId !== undefined;
|
|
130
|
+
if (set.slug && hasProjectId !== hasInstructionSetId) {
|
|
131
|
+
throw new Error('instruction_set_sync_identity_missing');
|
|
132
|
+
}
|
|
81
133
|
for (const key of set.shape)
|
|
82
134
|
safeSegment(key);
|
|
83
135
|
const profileSlugs = new Set();
|
|
@@ -89,12 +141,28 @@ export function syncToDisk(out, set, fileOps = { writeFileSync }) {
|
|
|
89
141
|
profileSlugs.add(value);
|
|
90
142
|
}
|
|
91
143
|
mkdirSync(out, { recursive: true });
|
|
92
|
-
const destination = join(out,
|
|
93
|
-
|
|
144
|
+
const destination = join(out, directoryName);
|
|
145
|
+
const legacyDestination = set.slug && set.name !== set.slug && isSafeSegment(set.name)
|
|
146
|
+
? join(out, set.name)
|
|
147
|
+
: null;
|
|
148
|
+
if (existsSync(destination)) {
|
|
149
|
+
if (set.slug && existingDestinationIsLegacySynced(destination, set.name)) {
|
|
150
|
+
throw new Error('instruction_set_sync_legacy_identity_required');
|
|
151
|
+
}
|
|
152
|
+
if (!existingDestinationIsSynced(destination, set)) {
|
|
153
|
+
throw new Error('instruction_set_sync_destination_not_synced');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (legacyDestination && existsSync(legacyDestination)) {
|
|
157
|
+
if (existingDestinationIsLegacySynced(legacyDestination, set.name)) {
|
|
158
|
+
throw new Error('instruction_set_sync_legacy_identity_required');
|
|
159
|
+
}
|
|
94
160
|
throw new Error('instruction_set_sync_destination_not_synced');
|
|
95
|
-
|
|
161
|
+
}
|
|
162
|
+
const priorDestinations = [destination];
|
|
163
|
+
const temp = join(out, `.${directoryName}.${randomUUID()}.tmp`);
|
|
96
164
|
const move = fileOps.renameSync || renameSync;
|
|
97
|
-
|
|
165
|
+
const backups = [];
|
|
98
166
|
mkdirSync(temp);
|
|
99
167
|
try {
|
|
100
168
|
const defaultMaterial = writeMaterial(temp, set.default, set.shape, 'default', fileOps);
|
|
@@ -108,28 +176,40 @@ export function syncToDisk(out, set, fileOps = { writeFileSync }) {
|
|
|
108
176
|
? Object.entries(profile.production.files).map(([key, path]) => ({ key, modelConfig: profile.modelConfigIdentity, path, syncedContentSha256: profile.production.syncedContentSha256[key] }))
|
|
109
177
|
: []),
|
|
110
178
|
];
|
|
111
|
-
const manifest = {
|
|
179
|
+
const manifest = {
|
|
180
|
+
manifestVersion: 1,
|
|
181
|
+
...(set.projectId ? { projectId: set.projectId } : {}),
|
|
182
|
+
...(set.instructionSetId ? { instructionSetId: set.instructionSetId } : {}),
|
|
183
|
+
name: set.name,
|
|
184
|
+
...(set.slug ? { slug: set.slug } : {}),
|
|
185
|
+
...(Object.hasOwn(set, 'description') ? { description: set.description ?? null } : {}),
|
|
186
|
+
shape: set.shape,
|
|
187
|
+
components,
|
|
188
|
+
default: defaultMaterial,
|
|
189
|
+
profiles,
|
|
190
|
+
...(set.filteredTo ? { filteredTo: [...set.filteredTo].sort() } : {}),
|
|
191
|
+
};
|
|
112
192
|
fileOps.writeFileSync(join(temp, 'manifest.json'), stable(manifest));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
move(
|
|
118
|
-
|
|
119
|
-
catch (error) {
|
|
120
|
-
if (existsSync(backup))
|
|
121
|
-
move(backup, destination);
|
|
122
|
-
throw error;
|
|
193
|
+
for (const prior of priorDestinations) {
|
|
194
|
+
if (!existsSync(prior))
|
|
195
|
+
continue;
|
|
196
|
+
const backup = join(out, `.${directoryName}.${randomUUID()}.bak`);
|
|
197
|
+
move(prior, backup);
|
|
198
|
+
backups.push({ original: prior, backup });
|
|
123
199
|
}
|
|
124
|
-
|
|
200
|
+
move(temp, destination);
|
|
125
201
|
}
|
|
126
202
|
catch (error) {
|
|
127
203
|
removeOwnScratch([temp]);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
204
|
+
for (const { original, backup } of [...backups].reverse()) {
|
|
205
|
+
if (existsSync(backup) && !existsSync(original))
|
|
206
|
+
move(backup, original);
|
|
207
|
+
}
|
|
208
|
+
removeOwnScratch(backups.map(entry => entry.backup));
|
|
131
209
|
throw error;
|
|
132
210
|
}
|
|
211
|
+
removePublishedBackups(backups.map(entry => entry.backup), fileOps.rmSync || rmSync);
|
|
212
|
+
return destination;
|
|
133
213
|
}
|
|
134
214
|
export async function instructionSetsCommand(args, io) {
|
|
135
215
|
const positionals = positionalArgs(args);
|
|
@@ -148,6 +228,36 @@ export async function instructionSetsCommand(args, io) {
|
|
|
148
228
|
writeManifest = loadInstructionSetManifest(reference);
|
|
149
229
|
}
|
|
150
230
|
const project = await io.resolveProjectSlug(argValue(args, '--project'));
|
|
231
|
+
if (subcommand === 'scorers') {
|
|
232
|
+
const action = reference;
|
|
233
|
+
const set = positionals[2];
|
|
234
|
+
const componentKey = argValue(args, '--key');
|
|
235
|
+
const scorerVersionId = argValue(args, '--scorer-version');
|
|
236
|
+
if ((action !== 'set-headline' && action !== 'add') ||
|
|
237
|
+
!set ||
|
|
238
|
+
!componentKey ||
|
|
239
|
+
!scorerVersionId) {
|
|
240
|
+
throw new Error('Usage: orizu instructions scorers set-headline|add <set> --key <component-key> --scorer-version <id> --project <team/project> [--dataset-version <id> --split-set <id> --split <name>] [--json]');
|
|
241
|
+
}
|
|
242
|
+
const payload = await responsePayload(await authedFetch(`/api/cli/instruction-sets/${encodeURIComponent(set)}/scorers?project=${encodeURIComponent(project)}`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'content-type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
componentKey,
|
|
247
|
+
scorerVersionId,
|
|
248
|
+
role: action === 'set-headline' ? 'headline' : 'tracked',
|
|
249
|
+
datasetVersionId: argValue(args, '--dataset-version') || undefined,
|
|
250
|
+
splitSetId: argValue(args, '--split-set') || undefined,
|
|
251
|
+
splitName: argValue(args, '--split') || undefined,
|
|
252
|
+
}),
|
|
253
|
+
}), `Instruction sets scorers ${action}`);
|
|
254
|
+
if (io.json)
|
|
255
|
+
io.print(JSON.stringify(payload));
|
|
256
|
+
else {
|
|
257
|
+
io.print(`${action === 'set-headline' ? 'Set headline' : 'Added'} scorer ${sanitizeTerminalText(scorerVersionId)} for ${sanitizeTerminalText(set)} / ${sanitizeTerminalText(componentKey)}`);
|
|
258
|
+
}
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
151
261
|
if (subcommand === 'default') {
|
|
152
262
|
const operation = reference;
|
|
153
263
|
const set = positionals[2];
|
|
@@ -318,11 +428,11 @@ export async function instructionSetsCommand(args, io) {
|
|
|
318
428
|
if (!set || (set.slug !== reference && set.name !== reference) || (modelConfig && (!Array.isArray(set.filteredTo) || set.filteredTo.length !== 1 || set.filteredTo[0] !== modelConfig))) {
|
|
319
429
|
throw new Error('instruction_set_sync_response_mismatch');
|
|
320
430
|
}
|
|
321
|
-
syncToDisk(output, set);
|
|
431
|
+
const destination = syncToDisk(output, set);
|
|
322
432
|
if (io.json)
|
|
323
433
|
io.print(JSON.stringify(payload));
|
|
324
434
|
else
|
|
325
|
-
io.print(`Synced ${set.name} to ${
|
|
435
|
+
io.print(`Synced ${set.name} to ${destination}`);
|
|
326
436
|
return 0;
|
|
327
437
|
}
|
|
328
438
|
if (subcommand === 'show' && (!reference || reference.startsWith('--')))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skilled-proposer-config.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-config.ts"],"names":[],"mappings":"AAoJA,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA0CrE"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const CONFIG_KEYS = new Set(['schemaVersion', 'skills', 'additionalInstructionsFile', 'baseInstructionsFile',
|
|
6
|
+
'maxWords', 'maxTokens', 'maxExamples', 'onError',
|
|
7
|
+
]);
|
|
8
|
+
const PATH_SKILL_KEYS = new Set(['path', 'name', 'description']);
|
|
9
|
+
const INLINE_SKILL_KEYS = new Set(['name', 'description', 'inline']);
|
|
10
|
+
function configError(field, detail) { throw new Error(`--candidate-proposer-config ${field} ${detail}`); }
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function rejectUnknownKeys(value, allowed, field = '') {
|
|
15
|
+
const unknown = Object.keys(value).find(key => !allowed.has(key));
|
|
16
|
+
if (unknown)
|
|
17
|
+
configError(field ? `${field}.${unknown}` : unknown, 'is not supported');
|
|
18
|
+
}
|
|
19
|
+
function sha256(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
|
|
20
|
+
function decodeUtf8(bytes, field) {
|
|
21
|
+
try {
|
|
22
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
configError(field, 'must contain valid UTF-8 text');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readRegularFile(path, field) {
|
|
29
|
+
try {
|
|
30
|
+
if (!statSync(path).isFile())
|
|
31
|
+
configError(field, 'must resolve to a file');
|
|
32
|
+
return readFileSync(path);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config'))
|
|
36
|
+
throw error;
|
|
37
|
+
configError(field, `is missing or unreadable: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function logicalSource(configDirectory, path) {
|
|
41
|
+
return relative(configDirectory, path).split(sep).join('/') || basename(path);
|
|
42
|
+
}
|
|
43
|
+
function normalizedText(bytes, source, field) {
|
|
44
|
+
const content = decodeUtf8(bytes, field);
|
|
45
|
+
if (!content.trim())
|
|
46
|
+
configError(field, 'must not be empty');
|
|
47
|
+
return { source, content, sha256: sha256(bytes), byteLength: bytes.byteLength };
|
|
48
|
+
}
|
|
49
|
+
function parseFrontmatter(content, field) {
|
|
50
|
+
const lines = content.split(/\r?\n/);
|
|
51
|
+
if (lines[0]?.replace(/^\uFEFF/, '').trim() !== '---')
|
|
52
|
+
return { body: content };
|
|
53
|
+
const end = lines.slice(1).findIndex(line => line.trim() === '---');
|
|
54
|
+
if (end < 0)
|
|
55
|
+
configError(field, 'has malformed frontmatter without a closing ---');
|
|
56
|
+
const metadata = {};
|
|
57
|
+
for (const raw of lines.slice(1, end + 1)) {
|
|
58
|
+
if (/^[ \t]/.test(raw) || !raw.includes(':'))
|
|
59
|
+
continue;
|
|
60
|
+
const colon = raw.indexOf(':');
|
|
61
|
+
metadata[raw.slice(0, colon).trim()] = raw.slice(colon + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
name: metadata.name || undefined,
|
|
65
|
+
description: metadata.description || undefined,
|
|
66
|
+
body: lines.slice(end + 2).join('\n'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function wellFormedString(value, field) {
|
|
70
|
+
if (!value.isWellFormed()) {
|
|
71
|
+
configError(field, 'must contain well-formed Unicode text');
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function nonEmptyString(value, field) {
|
|
76
|
+
if (typeof value !== 'string' || !value.trim())
|
|
77
|
+
configError(field, 'must be a non-empty string');
|
|
78
|
+
return wellFormedString(value, field);
|
|
79
|
+
}
|
|
80
|
+
function nullableDescription(value, field) {
|
|
81
|
+
if (value === null)
|
|
82
|
+
return null;
|
|
83
|
+
if (typeof value !== 'string')
|
|
84
|
+
configError(field, 'must be a string or null');
|
|
85
|
+
return wellFormedString(value, field) || null;
|
|
86
|
+
}
|
|
87
|
+
function normalizePathSkill(entry, index, configDirectory) {
|
|
88
|
+
const field = `skills[${index}]`;
|
|
89
|
+
rejectUnknownKeys(entry, PATH_SKILL_KEYS, field);
|
|
90
|
+
const configuredPath = nonEmptyString(entry.path, `${field}.path`);
|
|
91
|
+
let resolvedPath = resolve(configDirectory, configuredPath);
|
|
92
|
+
try {
|
|
93
|
+
if (statSync(resolvedPath).isDirectory()) {
|
|
94
|
+
resolvedPath = resolve(resolvedPath, 'SKILL.md');
|
|
95
|
+
try {
|
|
96
|
+
if (!statSync(resolvedPath).isFile())
|
|
97
|
+
configError(`${field}.path`, 'directory must contain SKILL.md');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config'))
|
|
101
|
+
throw error;
|
|
102
|
+
configError(`${field}.path`, 'directory must contain SKILL.md');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error && error.message.startsWith('--candidate-proposer-config'))
|
|
108
|
+
throw error;
|
|
109
|
+
configError(`${field}.path`, 'is missing or unreadable');
|
|
110
|
+
}
|
|
111
|
+
const text = normalizedText(readRegularFile(resolvedPath, `${field}.path`), logicalSource(configDirectory, resolvedPath), `${field}.path`);
|
|
112
|
+
const frontmatter = parseFrontmatter(text.content, `${field}.path`);
|
|
113
|
+
if (!frontmatter.body.trim())
|
|
114
|
+
configError(`${field}.path`, 'must contain non-empty skill content');
|
|
115
|
+
const fallbackName = extname(resolvedPath)
|
|
116
|
+
? basename(resolvedPath, extname(resolvedPath))
|
|
117
|
+
: basename(resolvedPath);
|
|
118
|
+
const name = entry.name === undefined
|
|
119
|
+
? frontmatter.name ?? (basename(resolvedPath) === 'SKILL.md' ? basename(dirname(resolvedPath)) : fallbackName)
|
|
120
|
+
: nonEmptyString(entry.name, `${field}.name`);
|
|
121
|
+
const description = entry.description === undefined
|
|
122
|
+
? frontmatter.description ?? null
|
|
123
|
+
: nullableDescription(entry.description, `${field}.description`);
|
|
124
|
+
return { name, description, ...text };
|
|
125
|
+
}
|
|
126
|
+
function normalizeInlineSkill(entry, index) {
|
|
127
|
+
const field = `skills[${index}]`;
|
|
128
|
+
rejectUnknownKeys(entry, INLINE_SKILL_KEYS, field);
|
|
129
|
+
const name = nonEmptyString(entry.name, `${field}.name`);
|
|
130
|
+
const description = nonEmptyString(entry.description, `${field}.description`);
|
|
131
|
+
const content = nonEmptyString(entry.inline, `${field}.inline`);
|
|
132
|
+
const bytes = Buffer.from(content, 'utf8');
|
|
133
|
+
return { name, description, source: `inline:${name}`, content,
|
|
134
|
+
sha256: sha256(bytes), byteLength: bytes.byteLength };
|
|
135
|
+
}
|
|
136
|
+
function normalizeSkill(value, index, configDirectory) {
|
|
137
|
+
const field = `skills[${index}]`;
|
|
138
|
+
if (!isRecord(value))
|
|
139
|
+
configError(field, 'must be an object');
|
|
140
|
+
const hasPath = Object.hasOwn(value, 'path');
|
|
141
|
+
const hasInline = Object.hasOwn(value, 'inline');
|
|
142
|
+
if (hasPath === hasInline)
|
|
143
|
+
configError(field, 'must contain exactly one of path or inline');
|
|
144
|
+
return hasPath ? normalizePathSkill(value, index, configDirectory) : normalizeInlineSkill(value, index);
|
|
145
|
+
}
|
|
146
|
+
function optionalPositiveInteger(value, field) {
|
|
147
|
+
if (value === undefined || value === null)
|
|
148
|
+
return null;
|
|
149
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
150
|
+
configError(field, 'must be a positive integer or null');
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function normalizeInstructionFile(value, field, configDirectory) {
|
|
154
|
+
if (value === undefined || value === null)
|
|
155
|
+
return null;
|
|
156
|
+
const configuredPath = nonEmptyString(value, field);
|
|
157
|
+
const resolvedPath = resolve(configDirectory, configuredPath);
|
|
158
|
+
return normalizedText(readRegularFile(resolvedPath, field), logicalSource(configDirectory, resolvedPath), field);
|
|
159
|
+
}
|
|
160
|
+
function lockedPackageVersion() {
|
|
161
|
+
const lockPath = fileURLToPath(new URL('../requirements/skilled-proposer.lock', import.meta.url));
|
|
162
|
+
const match = /^skilled-proposer==([^\s\\]+)/m.exec(readFileSync(lockPath, 'utf8'));
|
|
163
|
+
if (!match)
|
|
164
|
+
throw new Error('ALI_1505_SKILLED_PROPOSER_LOCK_VERSION_MISSING');
|
|
165
|
+
return match[1];
|
|
166
|
+
}
|
|
167
|
+
export function normalizedSkilledProposerConfig(value) {
|
|
168
|
+
const suppliedPath = nonEmptyString(value.startsWith('@') ? value.slice(1) : value, 'path');
|
|
169
|
+
const configPath = resolve(suppliedPath);
|
|
170
|
+
const configBytes = readRegularFile(configPath, 'path');
|
|
171
|
+
const raw = decodeUtf8(configBytes, 'path').replace(/^\uFEFF/, '');
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(raw);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
configError('path', `must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
if (!isRecord(parsed))
|
|
180
|
+
configError('path', 'must contain a JSON object');
|
|
181
|
+
rejectUnknownKeys(parsed, CONFIG_KEYS);
|
|
182
|
+
if (parsed.schemaVersion !== 1)
|
|
183
|
+
configError('schemaVersion', 'must be 1');
|
|
184
|
+
if (!Array.isArray(parsed.skills))
|
|
185
|
+
configError('skills', 'must be an array');
|
|
186
|
+
const configDirectory = dirname(configPath);
|
|
187
|
+
const skills = parsed.skills.map((skill, index) => normalizeSkill(skill, index, configDirectory));
|
|
188
|
+
const names = new Set();
|
|
189
|
+
for (const skill of skills) {
|
|
190
|
+
if (names.has(skill.name))
|
|
191
|
+
configError('skills', `contains duplicate resolved name ${skill.name}`);
|
|
192
|
+
names.add(skill.name);
|
|
193
|
+
}
|
|
194
|
+
const onError = parsed.onError ?? 'keep';
|
|
195
|
+
if (onError !== 'keep' && onError !== 'raise')
|
|
196
|
+
configError('onError', 'must be keep or raise');
|
|
197
|
+
const normalized = {
|
|
198
|
+
schemaVersion: 1,
|
|
199
|
+
implementation: 'cmpnd-ai/skilled-proposer',
|
|
200
|
+
packageVersion: lockedPackageVersion(),
|
|
201
|
+
skills,
|
|
202
|
+
additionalInstructions: normalizeInstructionFile(parsed.additionalInstructionsFile, 'additionalInstructionsFile', configDirectory),
|
|
203
|
+
baseInstructions: normalizeInstructionFile(parsed.baseInstructionsFile, 'baseInstructionsFile', configDirectory),
|
|
204
|
+
maxWords: optionalPositiveInteger(parsed.maxWords, 'maxWords'),
|
|
205
|
+
maxTokens: optionalPositiveInteger(parsed.maxTokens, 'maxTokens'),
|
|
206
|
+
maxExamples: optionalPositiveInteger(parsed.maxExamples, 'maxExamples'),
|
|
207
|
+
onError,
|
|
208
|
+
};
|
|
209
|
+
const configFileSha256 = sha256(configBytes);
|
|
210
|
+
const configSha256 = sha256(Buffer.from(JSON.stringify({ configFileSha256, ...normalized }), 'utf8'));
|
|
211
|
+
return JSON.stringify({ ...normalized, configFileSha256, configSha256 });
|
|
212
|
+
}
|
|
@@ -3,5 +3,9 @@ export interface SkilledProposerLaunch {
|
|
|
3
3
|
python: string;
|
|
4
4
|
environment: NodeJS.ProcessEnv;
|
|
5
5
|
}
|
|
6
|
+
export declare function spawnSkilledProposerChild(command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv): Promise<{
|
|
7
|
+
status: number | null;
|
|
8
|
+
error?: Error;
|
|
9
|
+
}>;
|
|
6
10
|
export declare function prepareSkilledProposerLaunch(python: string, engine: GepaEngine, environment: NodeJS.ProcessEnv): SkilledProposerLaunch;
|
|
7
11
|
//# sourceMappingURL=skilled-proposer-launch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAa3D,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAC,UAAU,CAAA;CAC/B;AAiBD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU;YA4B9C,MAAM,GAAG,IAAI;YAAU,KAAK;GAUlE;AAwCD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,CAAC,UAAU,GAC7B,qBAAqB,CAwBvB"}
|
|
@@ -1,8 +1,72 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
2
4
|
import { join } from 'path';
|
|
3
5
|
import { fileURLToPath } from 'url';
|
|
4
6
|
import { bundledOfficialGepaPythonPath } from './gepa-python-paths.js';
|
|
5
7
|
import { sanitizeTerminalText } from './json-response.js';
|
|
8
|
+
function materializeConfigPayload(engine, environment) {
|
|
9
|
+
const payload = environment.ORIZU_SKILLED_PROPOSER_CONFIG;
|
|
10
|
+
if (engine !== 'official' || environment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer' || payload === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), 'orizu-skilled-proposer-payload-'));
|
|
13
|
+
try {
|
|
14
|
+
const path = join(root, 'config.json');
|
|
15
|
+
writeFileSync(path, payload, { encoding: 'utf8', mode: 0o600 });
|
|
16
|
+
environment.ORIZU_SKILLED_PROPOSER_CONFIG = `@${path}`;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
rmSync(root, { recursive: true, force: true });
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
return () => rmSync(root, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
export async function spawnSkilledProposerChild(command, args, engine, environment) {
|
|
25
|
+
const childEnvironment = { ...environment };
|
|
26
|
+
const shouldMaterialize = engine === 'official'
|
|
27
|
+
&& childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
28
|
+
&& childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined;
|
|
29
|
+
if (!shouldMaterialize)
|
|
30
|
+
return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment });
|
|
31
|
+
let child;
|
|
32
|
+
let cleanup = () => { };
|
|
33
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'];
|
|
34
|
+
const handlers = signals.map(signal => ({
|
|
35
|
+
signal,
|
|
36
|
+
handle: () => {
|
|
37
|
+
try {
|
|
38
|
+
cleanup();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
try {
|
|
42
|
+
child?.kill(signal);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
for (const entry of handlers)
|
|
46
|
+
process.off(entry.signal, entry.handle);
|
|
47
|
+
process.kill(process.pid, signal);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
for (const entry of handlers)
|
|
53
|
+
process.once(entry.signal, entry.handle);
|
|
54
|
+
try {
|
|
55
|
+
cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup;
|
|
56
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
57
|
+
return await new Promise(resolve => {
|
|
58
|
+
let error;
|
|
59
|
+
child = spawn(command, args, { stdio: 'inherit', env: childEnvironment });
|
|
60
|
+
child.once('error', cause => { error = cause; });
|
|
61
|
+
child.once('close', status => resolve(error ? { status, error } : { status }));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
for (const entry of handlers)
|
|
66
|
+
process.off(entry.signal, entry.handle);
|
|
67
|
+
cleanup();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
6
70
|
function ensureSelectedSkilledProposerVenv(python, vendoredGepaPath) {
|
|
7
71
|
const manager = fileURLToPath(new URL('../scripts/ensure-skilled-proposer-venv.mjs', import.meta.url));
|
|
8
72
|
const lock = fileURLToPath(new URL('../requirements/skilled-proposer.lock', import.meta.url));
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs'
|
|
2
2
|
|
|
3
|
+
import { normalizedSkilledProposerConfig } from './skilled-proposer-config.js'
|
|
4
|
+
|
|
3
5
|
export type GepaEngine = 'official' | 'legacy'
|
|
4
6
|
|
|
5
7
|
export interface GepaEngineDispatch {
|
|
@@ -179,6 +181,7 @@ const CONTROLLED_ENVIRONMENT_NAMES = new Set([
|
|
|
179
181
|
// translation prevents an inherited environment from selecting a proposer
|
|
180
182
|
// on an otherwise byte-for-byte normal official-GEPA launch.
|
|
181
183
|
'ORIZU_CANDIDATE_PROPOSER',
|
|
184
|
+
'ORIZU_SKILLED_PROPOSER_CONFIG',
|
|
182
185
|
'ORIZU_COMPONENT_SELECTOR',
|
|
183
186
|
])
|
|
184
187
|
|
|
@@ -208,6 +211,7 @@ function translateOfficialOptions(args: string[], project: string, environment:
|
|
|
208
211
|
connectorEnvironment.ORIZU_PROJECT = project
|
|
209
212
|
|
|
210
213
|
let metadata: string | undefined
|
|
214
|
+
let candidateProposerConfig: string | undefined
|
|
211
215
|
let hasInstructionSet = false
|
|
212
216
|
let hasCandidateVersion = false
|
|
213
217
|
const budgetFlags: string[] = []
|
|
@@ -240,6 +244,12 @@ function translateOfficialOptions(args: string[], project: string, environment:
|
|
|
240
244
|
index = parsed.nextIndex
|
|
241
245
|
continue
|
|
242
246
|
}
|
|
247
|
+
if (flag === '--candidate-proposer-config') {
|
|
248
|
+
const parsed = optionValueOrThrow(args, index, flag)
|
|
249
|
+
candidateProposerConfig = parsed.value
|
|
250
|
+
index = parsed.nextIndex
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
243
253
|
const option = VALUE_OPTIONS_BY_FLAG.get(flag)
|
|
244
254
|
if (option) {
|
|
245
255
|
const parsed = optionValueOrThrow(args, index, flag, option.allowEmpty)
|
|
@@ -297,6 +307,16 @@ function translateOfficialOptions(args: string[], project: string, environment:
|
|
|
297
307
|
&& connectorEnvironment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer') {
|
|
298
308
|
throw new Error('--proposal-max-calls and --proposal-max-tokens require --candidate-proposer skilled-proposer')
|
|
299
309
|
}
|
|
310
|
+
if (candidateProposerConfig !== undefined) {
|
|
311
|
+
if (connectorEnvironment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer') {
|
|
312
|
+
throw new Error('--candidate-proposer-config requires --candidate-proposer skilled-proposer')
|
|
313
|
+
}
|
|
314
|
+
connectorEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG = normalizedSkilledProposerConfig(candidateProposerConfig)
|
|
315
|
+
}
|
|
316
|
+
if (connectorEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
317
|
+
&& connectorEnvironment.ORIZU_REFLECTION_PROMPT_TEMPLATE !== undefined) {
|
|
318
|
+
throw new Error('--reflection-prompt-template is incompatible with --candidate-proposer skilled-proposer')
|
|
319
|
+
}
|
|
300
320
|
|
|
301
321
|
const reflectionModel = connectorEnvironment.ORIZU_REFLECTION_MODEL ?? 'anthropic/claude-opus-4-7'
|
|
302
322
|
if (!reflectionModel.startsWith('openai/') && connectorEnvironment.ORIZU_REFLECTION_MAX_TOKENS === undefined) {
|
|
@@ -321,7 +341,7 @@ function validatedMetadata(raw: string | undefined): string {
|
|
|
321
341
|
export function dispatchGepaEngine(args: string[], project: string, environment: NodeJS.ProcessEnv): GepaEngineDispatch {
|
|
322
342
|
const selected = removeEngine(args)
|
|
323
343
|
if (selected.engine === 'legacy') {
|
|
324
|
-
for (const flag of ['--instruction-set', '--model-config', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--proposal-max-calls', '--proposal-max-tokens']) {
|
|
344
|
+
for (const flag of ['--instruction-set', '--model-config', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--candidate-proposer-config', '--proposal-max-calls', '--proposal-max-tokens']) {
|
|
325
345
|
if (selected.args.some(argument => argument === flag || argument.startsWith(`${flag}=`))) {
|
|
326
346
|
throw new Error(`${flag} is supported by the official GEPA engine only`)
|
|
327
347
|
}
|
package/src/gepa-python-paths.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'fs'
|
|
2
|
+
import { execFileSync } from 'child_process'
|
|
2
3
|
import { fileURLToPath } from 'url'
|
|
3
4
|
|
|
4
5
|
import { isOrizuSourceCheckout } from '../scripts/orizu-source-checkout.mjs'
|
|
@@ -34,14 +35,31 @@ export function bundledOrizuGepaConnectorPythonPath(): string | null {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function bundledOfficialGepaPythonPath(): string | null {
|
|
37
|
-
const sourceArchive = fileURLToPath(new URL('../gepa-python-source.zip', import.meta.url))
|
|
38
38
|
const vendor = fileURLToPath(new URL('../vendor/gepa-python/src', import.meta.url))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if (prefersLiveSource()) {
|
|
40
|
+
const requiredVendorFiles = [
|
|
41
|
+
fileURLToPath(new URL('../vendor/gepa-python/manifest.json', import.meta.url)),
|
|
42
|
+
fileURLToPath(new URL('../vendor/gepa-python/src/gepa/__init__.py', import.meta.url)),
|
|
43
|
+
fileURLToPath(new URL('../vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py', import.meta.url)),
|
|
44
|
+
]
|
|
45
|
+
if (!requiredVendorFiles.every(existsSync)) {
|
|
46
|
+
const vendoringScript = fileURLToPath(new URL('../scripts/vendor-gepa-python.mjs', import.meta.url))
|
|
47
|
+
try {
|
|
48
|
+
execFileSync(process.execPath, [vendoringScript], { stdio: 'pipe' })
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const details = error instanceof Error ? error.message : String(error)
|
|
51
|
+
throw new Error(`Unable to materialize bundled official GEPA: ${details}`)
|
|
52
|
+
}
|
|
53
|
+
if (!requiredVendorFiles.every(existsSync)) {
|
|
54
|
+
throw new Error('Unable to materialize bundled official GEPA: extracted tree is incomplete')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
// zipimport cannot resolve GEPA's namespace-package adapters on every
|
|
60
|
+
// supported Python minor. Both source and published CLIs execute only the
|
|
61
|
+
// verified tree materialized from the pinned wheel.
|
|
62
|
+
return resolveGepaPythonPath([vendor])
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
export function getGepaPythonPathEntries(existingPythonPath: string | undefined): string[] {
|