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.
Files changed (41) hide show
  1. package/README.md +9 -6
  2. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  3. package/dist/gepa-engine-dispatch.js +20 -1
  4. package/dist/gepa-python-paths.d.ts.map +1 -1
  5. package/dist/gepa-python-paths.js +25 -6
  6. package/dist/help.d.ts.map +1 -1
  7. package/dist/help.js +36 -14
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -39
  10. package/dist/instruction-set-loader.d.ts.map +1 -1
  11. package/dist/instruction-set-loader.js +57 -5
  12. package/dist/instruction-sets-cli.d.ts +5 -2
  13. package/dist/instruction-sets-cli.d.ts.map +1 -1
  14. package/dist/instruction-sets-cli.js +134 -24
  15. package/dist/skilled-proposer-config.d.ts +2 -0
  16. package/dist/skilled-proposer-config.d.ts.map +1 -0
  17. package/dist/skilled-proposer-config.js +212 -0
  18. package/dist/skilled-proposer-launch.d.ts +4 -0
  19. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  20. package/dist/skilled-proposer-launch.js +65 -1
  21. package/package.json +1 -1
  22. package/src/gepa-engine-dispatch.ts +21 -1
  23. package/src/gepa-python-paths.ts +24 -6
  24. package/src/help.ts +36 -14
  25. package/src/index.ts +6 -53
  26. package/src/instruction-set-loader.ts +55 -5
  27. package/src/instruction-sets-cli.ts +173 -19
  28. package/src/skilled-proposer-config.ts +191 -0
  29. package/src/skilled-proposer-launch.ts +59 -1
  30. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
  31. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
  32. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
  33. package/vendor/skills/orizu-cli/SKILL.md +50 -16
  34. package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
  35. package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
  36. package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
  37. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
  38. package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
  39. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
  40. package/vendor/skills/orizu-cli/references/primer.md +21 -5
  41. 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, name) {
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 && manifest.name === name;
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
- safeSegment(set.name);
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, set.name);
93
- if (existsSync(destination) && !existingDestinationIsSynced(destination, set.name))
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
- const temp = join(out, `.${set.name}.${randomUUID()}.tmp`);
161
+ }
162
+ const priorDestinations = [destination];
163
+ const temp = join(out, `.${directoryName}.${randomUUID()}.tmp`);
96
164
  const move = fileOps.renameSync || renameSync;
97
- let backup = null;
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 = { manifestVersion: 1, name: set.name, ...(Object.hasOwn(set, 'description') ? { description: set.description ?? null } : {}), shape: set.shape, components, default: defaultMaterial, profiles, ...(set.filteredTo ? { filteredTo: [...set.filteredTo].sort() } : {}) };
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
- backup = join(out, `.${set.name}.${randomUUID()}.bak`);
114
- if (existsSync(destination))
115
- move(destination, backup);
116
- try {
117
- move(temp, destination);
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
- removeOwnScratch([backup]);
200
+ move(temp, destination);
125
201
  }
126
202
  catch (error) {
127
203
  removeOwnScratch([temp]);
128
- if (backup && existsSync(backup) && !existsSync(destination))
129
- move(backup, destination);
130
- removeOwnScratch([backup]);
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 ${join(output, set.name)}`);
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,2 @@
1
+ export declare function normalizedSkilledProposerConfig(value: string): string;
2
+ //# sourceMappingURL=skilled-proposer-config.d.ts.map
@@ -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":"AAIA,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;AAwCD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,CAAC,UAAU,GAC7B,qBAAqB,CAwBvB"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
  }
@@ -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
- // The checked-in pinned wheel is directly importable from PYTHONPATH in a
40
- // source checkout. Published packages intentionally omit that archive and
41
- // must resolve only the verified tree materialized by prepack.
42
- const candidates = prefersLiveSource() ? [sourceArchive, vendor] : [vendor]
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
- return resolveGepaPythonPath(candidates)
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[] {