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
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import atexit
12
12
  import dataclasses
13
+ import hashlib
13
14
  import json
14
15
  import os
15
16
  import shutil
@@ -35,11 +36,16 @@ from orizu_gepa.optimizer import TextGepaConfig
35
36
 
36
37
  _SELECTION_ENV = "ORIZU_CANDIDATE_PROPOSER"
37
38
  _SELECTION_VALUE = "skilled-proposer"
39
+ _CONFIG_ENV = "ORIZU_SKILLED_PROPOSER_CONFIG"
38
40
  _SUPPORTED_ROLES = {"system", "user", "assistant"}
39
41
  _ALLOWED_DSPY_REQUEST_FIELDS = frozenset({"temperature", "max_tokens", "cache"})
40
42
  _DspyBaseLM = dspy.BaseLM if dspy is not None else object
41
43
 
42
44
 
45
+ class _ProposalObservabilityEventFailure(RuntimeError):
46
+ pass
47
+
48
+
43
49
  def _require_dspy() -> Any:
44
50
  if dspy is None:
45
51
  raise RuntimeError("ALI_1505_DSPY_UNAVAILABLE")
@@ -90,6 +96,8 @@ class ProposalObservability:
90
96
  self.durable_failure_root = durable_failure_root
91
97
  self.events: list[dict[str, Any]] = []
92
98
  self.budget = budget or ProposalCallBudget()
99
+ self._event_failure_count = 0
100
+ self._provider_failure_count = 0
93
101
  self._temporary_root: Path | None = None
94
102
 
95
103
  @classmethod
@@ -151,15 +159,50 @@ class ProposalObservability:
151
159
  detail: str,
152
160
  usage: dict[str, int] | None = None,
153
161
  ) -> None:
162
+ self._provider_failure_count += 1
154
163
  self.budget.record(usage)
155
164
  self._write_durable_failure(code=code, detail=detail, correlation_id=event.get("correlation_id"))
156
165
  self._append_event(event)
157
166
 
158
167
  def record_event_failure(self, error: Exception, *, correlation_id: str) -> None:
168
+ self._event_failure_count += 1
159
169
  self._write_durable_failure(
160
170
  code="proposal_observability_event_failed", detail=str(error), correlation_id=correlation_id,
161
171
  )
162
172
 
173
+ def record_proposer_failure(self, error: Exception) -> None:
174
+ self._write_durable_failure(code="proposal_generation_failed", detail=str(error))
175
+
176
+ def record_component_fallback(self, *, component: str, error: Exception) -> None:
177
+ event = {
178
+ "type": "proposal_component_fallback",
179
+ "component": component,
180
+ "policy": "keep",
181
+ "errorType": type(error).__name__,
182
+ "errorMessage": str(error),
183
+ }
184
+ try:
185
+ self._append_event(event)
186
+ except OSError as event_error:
187
+ self._write_durable_failure(
188
+ code="proposal_observability_event_failed", detail=str(event_error),
189
+ )
190
+ raise _ProposalObservabilityEventFailure(
191
+ "proposal_observability_event_failed",
192
+ ) from event_error
193
+
194
+ def write_proposer_identity(self, identity: dict[str, Any]) -> str:
195
+ root = self.event_log_root.parent
196
+ path = root / "proposer.json"
197
+ try:
198
+ root.mkdir(parents=True, exist_ok=True)
199
+ path.write_text(json.dumps(identity, sort_keys=True) + "\n", encoding="utf-8")
200
+ except OSError as error:
201
+ code = "skilled_proposer_identity_write_failed"
202
+ self._write_durable_failure(code=code, detail=str(error))
203
+ raise RuntimeError(code) from error
204
+ return str(path)
205
+
163
206
  def terminal_stats(self) -> dict[str, Any]:
164
207
  return {"call_count": self.budget.call_count, "usage": dict(self.budget.usage)}
165
208
 
@@ -182,6 +225,14 @@ class ProposalObservability:
182
225
  def has_safe_boundary_stop(self) -> bool:
183
226
  return self.budget.has_safe_boundary_stop()
184
227
 
228
+ @property
229
+ def provider_failure_count(self) -> int:
230
+ return self._provider_failure_count
231
+
232
+ @property
233
+ def event_failure_count(self) -> int:
234
+ return self._event_failure_count
235
+
185
236
 
186
237
  def _reject(message: str, *, code: str = "proposal_dspy_request_rejected") -> Any:
187
238
  return _require_dspy().LMError(message, code=code)
@@ -311,9 +362,11 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
311
362
  )
312
363
  except Exception as event_error:
313
364
  self._observability.record_event_failure(event_error, correlation_id=correlation_id)
314
- raise RuntimeError("proposal_observability_event_failed") from event_error
365
+ raise _ProposalObservabilityEventFailure(
366
+ "proposal_observability_event_failed",
367
+ ) from event_error
315
368
  raise dspy_module.LMError(
316
- f"proposal_provider_transport_failed: {error}",
369
+ "proposal_provider_transport_failed",
317
370
  code="proposal_provider_transport_failed",
318
371
  model=self.model,
319
372
  provider="orizu",
@@ -337,7 +390,9 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
337
390
  self._observability.record_success(success_event, usage)
338
391
  except Exception as error:
339
392
  self._observability.record_event_failure(error, correlation_id=correlation_id)
340
- raise RuntimeError("proposal_observability_event_failed") from error
393
+ raise _ProposalObservabilityEventFailure(
394
+ "proposal_observability_event_failed",
395
+ ) from error
341
396
  return dspy_module.LMResponse.from_text(completion.text, model=self.model, usage=usage, cache_hit=False)
342
397
 
343
398
 
@@ -359,6 +414,268 @@ def make_skilled_proposer_bridge(
359
414
  )
360
415
 
361
416
 
417
+ @dataclass(frozen=True)
418
+ class _NormalizedText:
419
+ source: str
420
+ content: str
421
+ sha256: str
422
+ byte_length: int
423
+
424
+
425
+ @dataclass(frozen=True)
426
+ class _NormalizedSkill(_NormalizedText):
427
+ name: str
428
+ description: str | None
429
+
430
+
431
+ @dataclass(frozen=True)
432
+ class _SkilledProposerConfig:
433
+ config_file_sha256: str
434
+ implementation: str
435
+ package_version: str
436
+ config_sha256: str
437
+ skills: tuple[_NormalizedSkill, ...]
438
+ additional_instructions: _NormalizedText | None
439
+ base_instructions: _NormalizedText | None
440
+ max_words: int | None
441
+ max_tokens: int | None
442
+ max_examples: int | None
443
+ on_error: str
444
+
445
+ def identity(self) -> dict[str, Any]:
446
+ return {
447
+ "implementation": self.implementation,
448
+ "packageVersion": self.package_version,
449
+ "configSha256": self.config_sha256,
450
+ "skills": [{
451
+ "name": skill.name,
452
+ "source": skill.source,
453
+ "sha256": skill.sha256,
454
+ "byteLength": skill.byte_length,
455
+ } for skill in self.skills],
456
+ "additionalInstructionsSha256": (
457
+ self.additional_instructions.sha256 if self.additional_instructions else None
458
+ ),
459
+ "baseInstructionsSha256": self.base_instructions.sha256 if self.base_instructions else None,
460
+ "maxWords": self.max_words,
461
+ "maxTokens": self.max_tokens,
462
+ "maxExamples": self.max_examples,
463
+ "onError": self.on_error,
464
+ }
465
+
466
+
467
+ def _config_error(code: str, field_name: str) -> Any:
468
+ raise RuntimeError(f"{code}: {field_name}")
469
+
470
+
471
+ def _strict_object(value: Any, fields: set[str], field_name: str) -> dict[str, Any]:
472
+ if not isinstance(value, dict):
473
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
474
+ unknown = sorted(set(value) - fields)
475
+ if unknown:
476
+ return _config_error(
477
+ "skilled_proposer_config_unknown_field",
478
+ f"{field_name + '.' if field_name else ''}{unknown[0]}",
479
+ )
480
+ missing = sorted(fields - set(value))
481
+ if missing:
482
+ return _config_error(
483
+ "skilled_proposer_config_missing_field",
484
+ f"{field_name + '.' if field_name else ''}{missing[0]}",
485
+ )
486
+ return value
487
+
488
+
489
+ def _required_string(value: Any, field_name: str, *, allow_empty: bool = False) -> str:
490
+ if not isinstance(value, str) or (not allow_empty and not value.strip()):
491
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
492
+ try:
493
+ value.encode("utf-8")
494
+ except UnicodeEncodeError:
495
+ return _config_error("skilled_proposer_config_invalid_unicode", field_name)
496
+ return value
497
+
498
+
499
+ def _positive_int_or_none(value: Any, field_name: str) -> int | None:
500
+ if value is None:
501
+ return None
502
+ if type(value) is not int or value <= 0:
503
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
504
+ return value
505
+
506
+
507
+ def _normalized_text(value: Any, field_name: str) -> _NormalizedText:
508
+ raw = _strict_object(value, {"source", "content", "sha256", "byteLength"}, field_name)
509
+ source = _required_string(raw["source"], f"{field_name}.source")
510
+ content = _required_string(raw["content"], f"{field_name}.content")
511
+ digest = _required_string(raw["sha256"], f"{field_name}.sha256")
512
+ byte_length = raw["byteLength"]
513
+ if (len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest)
514
+ or type(byte_length) is not int or byte_length != len(content.encode("utf-8"))):
515
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
516
+ if hashlib.sha256(content.encode()).hexdigest() != digest:
517
+ return _config_error("skilled_proposer_config_invalid_field", f"{field_name}.sha256")
518
+ return _NormalizedText(source, content, digest, byte_length)
519
+
520
+
521
+ def _optional_normalized_text(value: Any, field_name: str) -> _NormalizedText | None:
522
+ return None if value is None else _normalized_text(value, field_name)
523
+
524
+
525
+ def _normalized_text_payload(value: _NormalizedText | None) -> dict[str, Any] | None:
526
+ if value is None:
527
+ return None
528
+ return {
529
+ "source": value.source, "content": value.content,
530
+ "sha256": value.sha256, "byteLength": value.byte_length,
531
+ }
532
+
533
+
534
+ def _parse_skilled_proposer_config(raw_payload: str) -> _SkilledProposerConfig:
535
+ try:
536
+ parsed = json.loads(raw_payload)
537
+ except (json.JSONDecodeError, TypeError) as error:
538
+ raise RuntimeError("skilled_proposer_config_invalid_json") from error
539
+ fields = {
540
+ "configFileSha256", "schemaVersion", "implementation", "packageVersion", "configSha256", "skills",
541
+ "additionalInstructions", "baseInstructions", "maxWords", "maxTokens", "maxExamples", "onError",
542
+ }
543
+ raw = _strict_object(parsed, fields, "")
544
+ if (type(raw["schemaVersion"]) is not int or raw["schemaVersion"] != 1
545
+ or raw["implementation"] != "cmpnd-ai/skilled-proposer"):
546
+ return _config_error("skilled_proposer_config_invalid_field", "schemaVersion or implementation")
547
+ package_version = _required_string(raw["packageVersion"], "packageVersion")
548
+ config_file_sha256 = _required_string(raw["configFileSha256"], "configFileSha256")
549
+ config_sha256 = _required_string(raw["configSha256"], "configSha256")
550
+ for field_name, digest in (("configFileSha256", config_file_sha256), ("configSha256", config_sha256)):
551
+ if len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest):
552
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
553
+ if not isinstance(raw["skills"], list):
554
+ return _config_error("skilled_proposer_config_invalid_field", "skills")
555
+ skills: list[_NormalizedSkill] = []
556
+ for index, value in enumerate(raw["skills"]):
557
+ field_name = f"skills[{index}]"
558
+ skill_raw = _strict_object(
559
+ value, {"name", "description", "source", "content", "sha256", "byteLength"}, field_name,
560
+ )
561
+ text = _normalized_text({key: skill_raw[key] for key in (
562
+ "source", "content", "sha256", "byteLength",
563
+ )}, field_name)
564
+ name = _required_string(skill_raw["name"], f"{field_name}.name")
565
+ description = skill_raw["description"]
566
+ if description is not None:
567
+ description = _required_string(description, f"{field_name}.description", allow_empty=True)
568
+ skills.append(_NormalizedSkill(
569
+ source=text.source, content=text.content, sha256=text.sha256, byte_length=text.byte_length,
570
+ name=name, description=description,
571
+ ))
572
+ if len({skill.name for skill in skills}) != len(skills):
573
+ return _config_error("skilled_proposer_config_invalid_field", "skills duplicate names")
574
+ on_error = raw["onError"]
575
+ if on_error not in ("keep", "raise"):
576
+ return _config_error("skilled_proposer_config_invalid_field", "onError")
577
+ result = _SkilledProposerConfig(
578
+ config_file_sha256=config_file_sha256,
579
+ implementation=raw["implementation"], package_version=package_version,
580
+ config_sha256=config_sha256, skills=tuple(skills),
581
+ additional_instructions=_optional_normalized_text(raw["additionalInstructions"], "additionalInstructions"),
582
+ base_instructions=_optional_normalized_text(raw["baseInstructions"], "baseInstructions"),
583
+ max_words=_positive_int_or_none(raw["maxWords"], "maxWords"),
584
+ max_tokens=_positive_int_or_none(raw["maxTokens"], "maxTokens"),
585
+ max_examples=_positive_int_or_none(raw["maxExamples"], "maxExamples"),
586
+ on_error=on_error,
587
+ )
588
+ effective = {
589
+ "configFileSha256": result.config_file_sha256,
590
+ "schemaVersion": 1,
591
+ "implementation": result.implementation,
592
+ "packageVersion": result.package_version,
593
+ "skills": [{
594
+ "name": skill.name, "description": skill.description, "source": skill.source,
595
+ "content": skill.content, "sha256": skill.sha256, "byteLength": skill.byte_length,
596
+ } for skill in result.skills],
597
+ "additionalInstructions": _normalized_text_payload(result.additional_instructions),
598
+ "baseInstructions": _normalized_text_payload(result.base_instructions),
599
+ "maxWords": result.max_words, "maxTokens": result.max_tokens,
600
+ "maxExamples": result.max_examples, "onError": result.on_error,
601
+ }
602
+ canonical = json.dumps(effective, ensure_ascii=False, separators=(",", ":"))
603
+ if hashlib.sha256(canonical.encode("utf-8")).hexdigest() != result.config_sha256:
604
+ return _config_error("skilled_proposer_config_hash_mismatch", "configSha256")
605
+ return result
606
+
607
+
608
+ class _CompressionFailureObserver:
609
+ def __init__(self) -> None:
610
+ self.failure_count = 0
611
+ self.last_error: Exception | None = None
612
+
613
+ def on_module_start(self, **_unused: Any) -> None:
614
+ return
615
+
616
+ def on_module_end(self, *, exception: Exception | None = None, **_unused: Any) -> None:
617
+ if exception is not None:
618
+ self.failure_count += 1
619
+ self.last_error = exception
620
+
621
+
622
+ class _ObservableConfiguredProposer:
623
+ def __init__(self, proposer: Any, observability: ProposalObservability, on_error: str) -> None:
624
+ self.proposer = proposer
625
+ self.observability = observability
626
+ self.on_error = on_error
627
+ self.compression_observer = _CompressionFailureObserver()
628
+ proposer.module.compress.callbacks.append(self.compression_observer)
629
+
630
+ def __getattr__(self, name: str) -> Any:
631
+ return getattr(self.proposer, name)
632
+
633
+ def __call__(self, candidate: dict[str, str], reflective_dataset: Any,
634
+ components_to_update: list[str]) -> dict[str, str]:
635
+ results: dict[str, str] = {}
636
+ for component in components_to_update:
637
+ compression_failures_before = self.compression_observer.failure_count
638
+ event_failures_before = self.observability.event_failure_count
639
+ failures_before = self.observability.provider_failure_count
640
+ try:
641
+ proposed = self.proposer(candidate, reflective_dataset, [component])
642
+ except Exception as error:
643
+ if self.observability.event_failure_count > event_failures_before:
644
+ raise RuntimeError("proposal_observability_event_failed") from error
645
+ if isinstance(error, _ProposalObservabilityEventFailure):
646
+ raise RuntimeError("proposal_observability_event_failed") from error
647
+ if isinstance(error, _require_dspy().LMError) and self.on_error == "keep":
648
+ raise
649
+ if self.on_error == "raise":
650
+ code = getattr(error, "code", None)
651
+ if code != "proposal_provider_transport_failed":
652
+ code = "proposal_generation_failed"
653
+ self.observability.record_proposer_failure(error)
654
+ raise RuntimeError(code or "proposal_observability_event_failed") from error
655
+ self.observability.record_component_fallback(component=component, error=error)
656
+ results[component] = candidate[component]
657
+ continue
658
+ if self.observability.event_failure_count > event_failures_before:
659
+ raise RuntimeError("proposal_observability_event_failed")
660
+ provider_failed = self.observability.provider_failure_count > failures_before
661
+ compression_failed = self.compression_observer.failure_count > compression_failures_before
662
+ if compression_failed or provider_failed:
663
+ compression_error = self.compression_observer.last_error if compression_failed else None
664
+ if isinstance(compression_error, _ProposalObservabilityEventFailure):
665
+ raise RuntimeError("proposal_observability_event_failed") from compression_error
666
+ error = (_require_dspy().LMError(
667
+ "proposal_compression_provider_failed: compression provider failure was suppressed upstream",
668
+ code="proposal_compression_provider_failed",
669
+ ) if provider_failed else RuntimeError("proposal_compression_failed"))
670
+ if self.on_error == "raise":
671
+ raise error from compression_error
672
+ self.observability.record_component_fallback(component=component, error=error)
673
+ results[component] = candidate[component]
674
+ else:
675
+ results.update(proposed)
676
+ return results
677
+
678
+
362
679
  def make_skilled_proposer_from_environment(
363
680
  *,
364
681
  config: TextGepaConfig,
@@ -369,7 +686,7 @@ def make_skilled_proposer_from_environment(
369
686
  """Create the opt-in proposer only for the exact selected environment value."""
370
687
  if os.environ.get(_SELECTION_ENV) != _SELECTION_VALUE:
371
688
  return None
372
- from skilled_proposer import SkilledProposer
689
+ from skilled_proposer import Skill, SkilledProposer
373
690
 
374
691
  bridge = make_skilled_proposer_bridge(
375
692
  config=config,
@@ -378,4 +695,36 @@ def make_skilled_proposer_from_environment(
378
695
  endpoint_override=endpoint_override,
379
696
  ssl_cert_file=os.environ.get("SSL_CERT_FILE"),
380
697
  )
381
- return SkilledProposer(prompt_model=bridge)
698
+ raw_proposer_config = os.environ.get(_CONFIG_ENV)
699
+ if raw_proposer_config is None:
700
+ return SkilledProposer(prompt_model=bridge)
701
+ if raw_proposer_config.startswith("@"):
702
+ payload_path = Path(raw_proposer_config[1:])
703
+ try:
704
+ raw_proposer_config = payload_path.read_text(encoding="utf-8")
705
+ except (OSError, UnicodeError) as error:
706
+ raise RuntimeError("skilled_proposer_config_payload_unreadable") from error
707
+ try:
708
+ payload_path.unlink()
709
+ if payload_path.parent.name.startswith("orizu-skilled-proposer-payload-"):
710
+ payload_path.parent.rmdir()
711
+ except OSError:
712
+ pass
713
+ proposer_config = _parse_skilled_proposer_config(raw_proposer_config)
714
+ proposer = SkilledProposer(
715
+ skills=[Skill(
716
+ name=skill.name, description=skill.description, content=skill.content,
717
+ ) for skill in proposer_config.skills],
718
+ additional_instructions=(
719
+ proposer_config.additional_instructions.content
720
+ if proposer_config.additional_instructions else None
721
+ ),
722
+ base_instructions=(proposer_config.base_instructions.content if proposer_config.base_instructions else None),
723
+ max_words=proposer_config.max_words,
724
+ max_tokens=proposer_config.max_tokens,
725
+ max_examples=proposer_config.max_examples,
726
+ on_error="raise",
727
+ prompt_model=bridge,
728
+ )
729
+ observability.write_proposer_identity(proposer_config.identity())
730
+ return _ObservableConfiguredProposer(proposer, observability, proposer_config.on_error)
@@ -1,20 +1,20 @@
1
1
  ---
2
2
  name: orizu-cli
3
- description: Use when the user wants to improve an LLM application's performance in a measurable way, or mentions Orizu by name. Triggers include improving model or agent performance, collecting human feedback on model outputs, converting feedback into evals, building or crafting evals, running prompt optimization (including one-off prompt tweaks the user is willing to validate with evals), working with Orizu datasets, tasks, apps, prompts, judges, scorers, runners, score runs, or optimization runs, hill-climbing on metrics, or building "continually learning" agents. Orizu is a platform for building evals first, then improving LLM applications by optimizing against them. The CLI handles human feedback collection, eval creation, versioned prompt and scorer artifacts, local runner execution, score submission, and prompt optimization. Do NOT use for prompt advice when the user has explicitly said they don't want to set up evals.
3
+ description: Use when the user mentions Orizu or wants to measurably improve an LLM application, model, or agent; collect human feedback on model outputs; convert feedback into or build evals; optimize instructions (including one-off changes they will validate with evals); work with Orizu datasets, tasks, apps, instruction sets, model-config profiles, components, judges, scorers, runners, score runs, optimization runs, GEPA, or hill-climbing; migrate a prompts-era setup; or build continually learning agents. Start instruction artifacts at `orizu instructions`. Orizu builds evals first, then improves applications against them; its CLI covers feedback collection, eval creation, versioned instructions and evaluator artifacts, local runner execution, score submission, and instruction optimization. Do NOT use for instruction advice when the user explicitly does not want to set up evals.
4
4
  ---
5
5
 
6
6
  # Orizu
7
7
 
8
- Orizu improves LLM applications by building evals first, then optimizing against them. The end-to-end loop has four steps: **Upload → Annotate → Judge → Optimize**. The CLI covers human-eval collection plus the Phase 0 prompt control plane: prompts, judges, scorers, runners, local runner execution, score submission, packaged GEPA-style text optimization, and live optimization logging.
8
+ Orizu improves LLM applications by building evals first, then optimizing against them. The end-to-end loop has four steps: **Upload → Annotate → Judge → Optimize**. Start instruction work with `orizu instructions`: it is the single surface for instruction sets, model-config profiles, their components, and the set-wide default. The CLI also covers human-eval collection, judges, scorers, runners, local runner execution, score submission, packaged GEPA-style text optimization, and live optimization logging. Do not create or mutate standalone prompts; use read-only prompt compatibility only to find the instruction set that owns legacy material.
9
9
 
10
10
  For end-to-end methodology and rationale, read `references/primer.md`. The reference docs below cover specific stages in depth.
11
11
 
12
12
  ## When to step aside
13
13
 
14
14
  Skip this skill when:
15
- - The user wants a one-off prompt tweak they explicitly won't validate with evals.
15
+ - The user wants a one-off instruction edit they explicitly won't validate with evals.
16
16
  - The user has already said they don't want to set up evals.
17
- - The request is generic prompt-engineering theory unrelated to a specific app.
17
+ - The request is generic instruction-writing theory unrelated to a specific app.
18
18
 
19
19
  # Orizu CLI basics
20
20
 
@@ -54,6 +54,24 @@ Projects live inside teams. Resolve or create both before any workflow command.
54
54
 
55
55
  For team membership, app, dataset, and task command surfaces, see `references/cli-reference.md`.
56
56
 
57
+ ## Instruction artifacts: start here
58
+
59
+ An instruction set is one customer-facing identity for an agent's instructions. Its manifest carries a `name`, optional `description`, fixed, ordered `shape`, and component values. Each model config has its own profile, and each profile owns its own component values; components are never shared between profiles or sets. The set's stable slug is its CLI reference even if the display name changes. Its default serves model configs that do not have a production profile version.
60
+
61
+ Use one command surface throughout:
62
+
63
+ ```bash
64
+ orizu instructions list --project <team>/<project>
65
+ orizu instructions show <slug-or-exact-name> --project <team>/<project>
66
+ # Human/local CLI only: a human runs create/push; hosted agents may run profiles new and sync.
67
+ orizu instructions create ./orizu.instruction-set.json --project <team>/<project> --model-config <identity>
68
+ orizu instructions push ./orizu.instruction-set.json --project <team>/<project> --set <slug-or-exact-name>
69
+ orizu instructions profiles new <slug-or-exact-name> --project <team>/<project> --model-config <identity>
70
+ orizu instructions sync <slug-or-exact-name> --out ./instructions --project <team>/<project>
71
+ ```
72
+
73
+ For a prompts-era setup, read `references/instructions-after-prompts.md` before making changes. Legacy prompt read commands identify the owning instruction set and component key; continue from that set through `orizu instructions`.
74
+
57
75
  # Evals best practices
58
76
 
59
77
  Walk through the four steps in order — each depends on the output of the previous. Methodology and rationale: `references/primer.md`.
@@ -106,11 +124,11 @@ Deeper: `references/primer.md` (Step 2); `references/cli-reference.md` (apps + t
106
124
 
107
125
  ## 3. Judge — turn labels into automated evaluators
108
126
 
109
- Use the Phase 0 prompt control plane when the user wants judges stored in Orizu as versioned prompt artifacts, runner zips, scorer definitions, and submitted score results. Read **`references/prompt-control-plane.md`** before using those commands. When the user mentions prompt feedback, prompt comments, or asks what to focus on for the next prompt version, inspect comment threads with `orizu comments list --prompt <prompt-id-or-name> --project <team>/<project>` before editing or pushing.
127
+ Use `orizu instructions` for the application instruction material being evaluated. Judges remain versioned evaluator artifacts with runner zips, scorer definitions, and submitted score results; read **`references/prompt-control-plane.md`** before using those compatible evaluator commands. When the user mentions legacy prompt feedback or comments, inspect the read-only threads with `orizu comments list --prompt <prompt-id-or-name> --project <team>/<project>`, find the owning instruction set, and revise that set through its manifest.
110
128
 
111
129
  Judge construction details are still useful when authoring the evaluator itself.
112
130
 
113
- A judge is the evaluator prompt artifact. A scorer is the metric contract that names what score is being produced and how it should be displayed, compared, and used in optimizations. For UI-visible prompt performance, register a scorer and submit score runs; do not rely only on raw `runs submit`.
131
+ A judge is the evaluator artifact. A scorer is the metric contract that names what score is being produced and how it should be displayed, compared, and used in optimizations. For UI-visible instruction performance, register a scorer and submit score runs; do not rely only on raw `runs submit`.
114
132
 
115
133
  Set scorers are aggregate metric contracts. Use `orizu scorers exec` for builtin set metrics such as Cohen's kappa, accuracy, precision, recall, and F1; it consumes subject results or dependency row-scorer results, computes one aggregate, stores row evidence, and submits a canonical score run by default. If you already computed an aggregate locally, use `orizu scores submit --aggregate` with explicit `scoreValue`, `diagnostics`, `feedbackSummary`, and optional `rowEvidence`. `runners exec --scorer-version` remains a low-level row-runner compatibility command. Use a row-mode scorer, not a set scorer, for GEPA reflection because reflection needs per-row feedback.
116
134
 
@@ -131,7 +149,7 @@ Detailed walkthrough — code assertion patterns, LLM-judge prompt scaffold, tra
131
149
 
132
150
  ## 4. Optimize — hill-climb against validated judges
133
151
 
134
- Use the Phase 0 prompt control plane when the user wants an optimizer run to appear in Orizu, stream live events, or promote accepted candidates into the prompt timeline. For text-candidate GEPA-style optimization, prefer the bundled `orizu optimizations run-gepa` flow. Read **`references/prompt-control-plane.md`** before wiring those endpoints.
152
+ Use the instruction control plane when the user wants an optimizer run to appear in Orizu, stream live events, or promote accepted candidates into the target instruction-set profile. For text-candidate GEPA-style optimization, prefer the bundled `orizu optimizations run-gepa` flow. Read **`references/prompt-control-plane.md`** before wiring those endpoints.
135
153
 
136
154
  GEPA details below are still useful for local optimizer implementation. DSPy is only relevant when the customer already uses DSPy or asks for an external DSPy GEPA implementation.
137
155
 
@@ -153,9 +171,9 @@ Local execution workflow:
153
171
  Bundled `run-gepa` reflection behavior:
154
172
  - **Scorer contract:** `run-gepa` sends the scorer runner a GEPA-shaped row (`{source_row, candidate_id, candidate_output, …}`), NOT the flat-row shape used by `runners exec --scorer-version`. A judge runner built for flat-row score runs will silently score every candidate 0 unless you pass `--scorer-input-contract flat_row` (add `--scorer-candidate-field <row-field>`, e.g. `draft`, when the judge reads the candidate output from a named row field). Do not hand-write an adapter runner — the flag applies the official adapter without changing the registered runner bytes, so it composes with `--scorer-runner-dir` content-sha verification. `run-gepa` validates the contract on the seed at launch and refuses a uniformly-worst-scoring seed with a diagnosis; `--allow-degenerate-seed` overrides only when the seed genuinely deserves the worst score everywhere. Both contracts: `references/prompt-control-plane.md` ("Scorer-Runner Input Contracts").
155
173
  - Budget controls are mutually exclusive. Use at most one of `--budget auto|light|medium|heavy`, `--max-metric-calls <n>`, `--max-full-evals <n>`, `--max-iterations <n>`, or `--max-candidate-proposals <n>`. The proposal cap is official-engine-only. If none is provided, `run-gepa` uses `--budget auto`, the balanced medium preset.
156
- - **Skilled proposer:** opt in with `--candidate-proposer skilled-proposer` on the official engine; the selected run prepares or reuses its managed Python environment automatically. Bound proposal work with `--proposal-max-tokens` and `--proposal-max-calls`, independently of `--max-metric-calls`. Read `references/prompt-control-plane.md` ("Skilled proposer") for validation and recovery; readiness is proven when optimizer execution begins without an `ALI_1505_*` environment diagnostic.
157
- - The reflective LM's final text is used verbatim as the next candidate prompt. It should return only the complete updated prompt body, not analysis, labels, XML tags, or markdown fences.
158
- - Keep provider-native reasoning controls separate from the prompt text with `--reflection-provider-settings <json|@file>`.
174
+ - **Skilled proposer:** opt in with `--candidate-proposer skilled-proposer` on the official engine; the selected run prepares or reuses its managed Python environment automatically. Optionally add `--candidate-proposer-config @path` for explicit skills and guidance. The selection is incompatible with `--reflection-prompt-template`, even without a config. Bound aggregate proposal work with `--proposal-max-tokens` and `--proposal-max-calls`, independently of `--max-metric-calls`. Do not confuse that aggregate token budget with per-response `--reflection-max-tokens` or the config's proposed-instruction `maxTokens`. Read `references/prompt-control-plane.md` ("Skilled proposer") for the schema, validation, and managed-environment recovery; readiness is proven when optimizer execution begins without an `ALI_1505_*` environment diagnostic.
175
+ - The reflective LM's final text becomes the selected component's next value. It should return only the complete updated component value, not analysis, labels, XML tags, or markdown fences.
176
+ - Keep provider-native reasoning controls separate from the component text with `--reflection-provider-settings <json|@file>`.
159
177
  - Reflection max-token limits are explicit config, not implicit defaults. Use `--reflection-max-tokens <n>` when a provider requires an output cap; this maps to Anthropic `max_tokens` and OpenAI `max_output_tokens`.
160
178
  - Anthropic native Messages reflection requires `max_tokens`, so `run-gepa` with an `anthropic/...` reflection model must receive `--reflection-max-tokens <n>` and should fail fast if it is omitted. OpenAI reflection may omit `max_output_tokens` unless the user explicitly wants a cap.
161
179
  - Reflection HTTP calls retry transient provider failures by default (`--reflection-retry-attempts 3`, `--reflection-http-timeout-seconds 180`). Retryable failures include timeouts, connection reset/abort, HTTP 429, and common server/capacity statuses. If reflection still fails after retries, `run-gepa` logs `reflection_failed`, counts the proposal against budget, and continues to the next iteration.
@@ -172,7 +190,8 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
172
190
 
173
191
  - `references/primer.md` — methodology end-to-end (read first when in doubt about *why*).
174
192
  - `references/cli-reference.md` — full CLI command surface.
175
- - `references/prompt-control-plane.md` — Phase 0 prompts, judges, scorers, runners, score submission, optimizer event logging, bundled GEPA, and promotion endpoints.
193
+ - `references/prompt-control-plane.md` — instruction sets, judges, scorers, runners, score submission, optimizer event logging, bundled GEPA, and promotion endpoints.
194
+ - `references/instructions-after-prompts.md` — finding the instruction set that owns prompts-era material and consolidating legacy one-component sets.
176
195
  - `references/building-apps.md` — labeler app contract, design principles, common patterns, offline smoke test.
177
196
  - `references/building-judges.md` — judge/scorer authoring + TPR/TNR validation.
178
197
  - `references/optimization-with-gepa.md` — GEPA optimization loop, with DSPy only as external context.
@@ -193,7 +212,7 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
193
212
  - `--output-schema` JSON Schema validation surface is restricted to `type`, `required`, `properties`, `items`, `enum`.
194
213
  - Export defaults: `--format jsonl`, output `<taskId>.<format>`.
195
214
  - Task reports can be uploaded only after the task is `paused` or `completed`; rerun `tasks report set` or `tasks report upload` to replace the markdown report.
196
- - Prompt control-plane commands should use ids for dataset versions, split sets, prompt versions, scorer versions, runner versions, optimizer versions, and optimization runs.
215
+ - Instruction and evaluation control-plane commands should use ids for dataset versions, split sets, component versions where compatibility commands require them, scorer versions, runner versions, optimizer versions, and optimization runs.
197
216
  - Optimization exports default to `<run-id>.optimization.json`; prefer the existing `logs/<run-id>` directory from `run-gepa` when it is available because it contains the full local trace without needing server rehydration.
198
217
  - When ending an optimization run, attach a markdown report with `--report-file` when possible. Use `references/optimization-reports.md` for what to include.
199
218
  - Use `orizu comments list --prompt <prompt-id-or-name> --project <team>/<project> [--label <label> | --version <id>] [--json]` to list prompt-level discussion threads with open/resolved status, selected text/line context, and replies.
@@ -201,8 +220,23 @@ Detailed walkthrough — GEPA mechanics, Orizu-tracked optimization, optional DS
201
220
 
202
221
  # Where things live
203
222
 
204
- - **Orizu is the source of truth for run metadata and pointers**: production/default prompt/scorer/runner pointers, version lineage, score runs, optimization runs, and report history all live in Orizu — query them with `orizu ...` commands (e.g. `orizu prompts list`, `orizu prompts versions`, `orizu runs ...`), never by reading repo files.
223
+ - **Orizu is the source of truth for run metadata and pointers**: instruction-set defaults, profile production pointers, scorer/runner pointers, version lineage, score runs, optimization runs, and report history all live in Orizu — query them with `orizu ...` commands (for example, `orizu instructions list`, `orizu instructions show`, and `orizu runs ...`), never by reading repo files.
205
224
  - **The repo is the working set (files only)**: source, configs, `App.tsx`, schemas, and local traces under `logs/<run-id>`. It is the scratch space you edit and commit; it does NOT hold the canonical pointer/run state — that is in Orizu.
206
- - **Hosted sessions ship a pre-authenticated CLI**: inside a hosted agent sandbox the `orizu` CLI is already authenticated via `ORIZU_TOKEN_FILE` (with `ORIZU_BASE_URL` pointing at the API). You do NOT run `orizu login` — just call `orizu ...`. The bearer carries **full team-curator authority while your session is active** (ADR-008): anything a curator-role human can read or write in your team — create/read/update projects, datasets (+ rows/artifact bytes), prompts, scorers, runners/optimizers, apps, tasks, runs, score runs, reports, and promotion-manifest proposals — you can too, across the whole team. (The full per-role access matrix lives in the Orizu repo at `docs/authorization-matrix.md`.)
207
- - **Git-canonical artifacts: always write with `--session`, and "committed" means bytes-in-branch**: prompts/judges, scorers, runners, and optimizers are git-canonical (ADR-007) — their bytes must live as commits on your session branch, not only as Orizu version rows. From a hosted session, always pass `--session <session-id>` on artifact writes (`prompts push`, `judges push`, `scorers register`, `runners push`, `optimizers push`); the legacy sessionless create routes reject agent bearers with a 403 pointing you back at `--session`. The definition of done for any artifact you claim to have committed is that the bytes are actually in the branch: run `git ls-files <path>` (and check the commit) before reporting an artifact as committed — an Orizu version id alone is not proof the bytes exist anywhere.
208
- - **Human-only actions will 403**: promotion-manifest **approval / apply / reject**, production and default pointer flips, session/credential lifecycle (starting or ending hosted sessions, minting tokens, deploy keys), integration/connector credential reads (secrets reach your sandbox only as environment variables resolved at boot — never via API/DB), and team/member/installation/billing management. Do not attempt them from a hosted session — they return 403 by design. Prepare a promotion manifest and hand it to a human curator to approve and apply.
225
+ - **Hosted sessions ship a pre-authenticated CLI**: inside a hosted agent sandbox the `orizu` CLI is already authenticated via `ORIZU_TOKEN_FILE` (with `ORIZU_BASE_URL` pointing at the API). You do NOT run `orizu login` — just call `orizu ...`. The bearer carries **team-curator authority while your session is active**, except for the explicit human-only carveouts below (ADR-008 and the ALI-1558 instruction-write restriction). It can read and sync instruction sets and can perform ordinary curator work across the team. (The full per-role access matrix lives in the Orizu repo at `docs/authorization-matrix.md`.)
226
+ - **Hosted `--session` applies to git-canonical artifacts, not instruction sets**: instruction sets and profiles are control-plane canonical (ADR-023), and their CLI mutations do not accept `--session`. A hosted agent may inspect them, sync them, and prepare a complete manifest, but a human curator using the local CLI with a user token must run the six catalog mutations listed below and all pointer moves. Judges, scorers, runners, and optimizers remain git-canonical for teams with a workbench repository; their hosted writes must carry `--session <session-id>`, and their bytes must live as commits on the session branch. The remaining session-scoped legacy prompt compatibility is not a standalone instruction workflow. Before calling any artifact committed, run `git ls-files <path>` and inspect the commit; an Orizu version id alone does not prove the bytes exist in a branch.
227
+ - **Human-only actions will 403**: a hosted agent must hand the following commands
228
+ to a human curator using the local CLI with a user token. Human/local CLI only:
229
+ `orizu instructions create`, `orizu instructions push`,
230
+ `orizu instructions shape add`, `orizu instructions shape remove`,
231
+ `orizu instructions archive`, and `orizu instructions restore`.
232
+ Human/local CLI only: The measured response for `orizu instructions create` and `orizu instructions push` is `Sessionless registration is not available to the hosted agent: this artifact class is git-canonical (ADR-007) and its bytes must be committed to the session branch. Re-run with --session <session-id>.` Those commands have no `--session` flag, so do not retry them.
233
+ Human/local CLI only: The measured response for `orizu instructions shape add` and `orizu instructions shape remove` is `Agents cannot change instruction-set shapes`.
234
+ Human/local CLI only: The measured response for `orizu instructions archive` and `orizu instructions restore` is `Agent sessions cannot perform this action`.
235
+ Promotion-manifest **approval / apply / reject**, production and default
236
+ pointer flips, session/credential lifecycle (starting or ending hosted
237
+ sessions, minting tokens, deploy keys), integration/connector credential
238
+ reads (secrets reach your sandbox only as environment variables resolved at
239
+ boot — never via API/DB), and team/member/installation/billing management
240
+ are also human-only. Do not attempt them from a hosted session. Prepare the
241
+ manifest or proposal and hand it to a human curator to run, approve, or
242
+ apply.
@@ -1,6 +1,6 @@
1
1
  # Building Judges And Scorers
2
2
 
3
- How to turn human-labeled annotations into automated evaluators. Use this reference for authoring and validating the evaluator logic; use `prompt-control-plane.md` to push versioned prompt artifacts, runner artifacts, scorer definitions, and score runs into Orizu.
3
+ How to turn human-labeled annotations into automated evaluators. Use this reference for authoring and validating the evaluator logic; use `prompt-control-plane.md` to push versioned judge artifacts, runner artifacts, scorer definitions, and score runs into Orizu.
4
4
 
5
5
  ## Inputs
6
6
 
@@ -138,7 +138,7 @@ Run the validated judge over future outputs to score them. Wire it into:
138
138
  - CI/CD on a curated test set (block merges that drop pass rate)
139
139
  - Online monitoring on sampled traffic (alert on regressions)
140
140
  - Optimization loop as a metric (`optimization-with-gepa.md`)
141
- - Orizu scorers and score runs (`prompt-control-plane.md`) so prompt detail, changelog, scorer detail, and optimization surfaces show comparable performance.
141
+ - Orizu scorers and score runs (`prompt-control-plane.md`) so instruction-set detail, changelog, scorer detail, and optimization surfaces show comparable performance.
142
142
 
143
143
  ## Set scorers for judge-vs-gold agreement
144
144