Skip to content

feat: SkillOpt — benchmark-driven prompt optimization via Harbor + YAML surface - #1013

Open
osilkin98 wants to merge 43 commits into
mainfrom
factory/run-0f311a4e
Open

feat: SkillOpt — benchmark-driven prompt optimization via Harbor + YAML surface#1013
osilkin98 wants to merge 43 commits into
mainfrom
factory/run-0f311a4e

Conversation

@osilkin98

@osilkin98 osilkin98 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements a SkillOpt-style training loop that optimizes agent prompt instructions via workflow YAML annotations, with benchmark execution through Harbor containers using factory workflow run. Includes SearchQA as the reference benchmark, validated against Microsoft's upstream SkillOpt results.

Architecture

The system has two loops:

Inner loop (benchmark execution via Harbor):

  • factory workflow run searchqa executes inside a Harbor container
  • The workflow reads the optimized skill from SEARCHQA_SKILL_B64 env var + appends fixed task instructions
  • Harbor's verifier scores the answer against gold via EM, writes reward.json
  • Results collected back to the trainer with per-item predicted/gold answers

Outer loop (SkillOpt optimizer):

  • Reflects on rollout traces (minibatch failure + success analysis via LLM)
  • Hierarchically merges edit proposals with failure priority
  • Clips edits to learning rate budget (operation count)
  • Applies edits to YAML prompt slots, re-renders SKILL.md via deterministic workflow engine
  • Gates on full 200-item validation set (strict improvement required)
  • Epoch-level slow update compares skill versions longitudinally

Key components

Workflow YAML surface (factory/skillopt/yaml_surface.py):

  • Only task_prompt_* slots are mutable; graph structure is frozen
  • yaml_to_workflow() converts edited YAML → valid Pydantic Workflow for factory workflow run
  • workflow_to_yaml() for the reverse path
  • 7 end-to-end round-trip tests verify exact value preservation

Harbor integration (factory/skillopt/adapters/searchqa.py):

  • Calls run-harbor.sh searchqa --all with SEARCHQA_SKILL_B64 env var
  • SEARCHQA_SPLIT env var switches between train/val Harbor task directories
  • Verifier outputs (predicted/gold answers) enriched into RolloutResult for reflect phase
  • FACTORY_GIT_REF ensures containers install from current branch

SearchQA benchmark (benchmarks/searchqa-harbor/):

  • 400 train + 200 val Harbor tasks from lucadiliello/searchqa (Jeopardy QA)
  • Each task: task.toml + instruction.md + Dockerfile + test.sh verifier
  • Verifier extracts <answer> tags, compares to gold with SQuAD-style EM normalization, writes /logs/verifier/reward.json
  • Single-node workflow: builder agent reads task, answers, writes /workspace/answer.txt

Slow update (factory/skillopt/slow_update.py):

  • Epoch-level longitudinal comparison (previous vs current skill on same samples)
  • Categorizes items as improved/regressed/persistent_fail/stable_success
  • Generates strategic guidance in a protected region that step-level edits cannot touch

Mapping to InnerLoop/CycleAnalyzer proposal (PR #1051)

Our system maps to PR #1051's abstractions:

PR #1051 concept Our implementation
InnerLoop.step() adapter.rollout()run-harbor.shfactory workflow run in container
CycleRecord.score_end Aggregate EM from Harbor result JSON
Evaluator.parse() Read Harbor reward.json + verifier test-stdout.txt
directives SEARCHQA_SKILL_B64 env var (edited skill → container)
CycleAnalyzer _collect_results() + _extract_verifier_outputs() from Harbor jobs dir

Three extensions needed in PR #1051 for this use case:

  1. Configurable execution backendInnerLoop needs to support run-harbor.sh alongside factory ceo
  2. Per-item results in CycleRecord — outer optimizer needs individual question results for reflection, not just aggregate scores
  3. Env var directives — outer loop passes edited skill via env var, not file-based messages

Training results

SearchQA (Haiku student, Opus optimizer, direct claude -p baseline):

SkillOpt Upstream Factory SkillOpt
Baseline (no skill) 70.7% 76.6%
Best test score (1400 items) 78.9% 84.4%
Improvement +8.2 pp +7.9 pp
Steps to best 45 / 50 2 / 40

Harbor-based training (1 epoch, in progress): validates the full loop through factory workflow run inside containers.

Module structure

File Role
types.py Edit, Patch, RolloutResult, RawPatch, GateResult, SlowUpdateResult
yaml_surface.py YAML loading, slot extraction, structural validation, round-trip conversion
adapter.py Abstract EnvAdapter base class
adapters/swebench.py Harbor SWE-bench adapter
adapters/searchqa.py Harbor SearchQA adapter (train/val split, verifier output enrichment)
reflect.py Minibatch reflection with parallel failure + success analysts
aggregate.py Hierarchical tree-structured patch merge
clip.py Index-based LLM edit ranking
skill.py Edit application with protected regions
gate.py Three-outcome validation gate
slow_update.py Epoch-level longitudinal comparison + strategic guidance
trainer.py DL-style training loop with epochs, LR, rejected-edit buffer, slow update
prompts/ 7 prompt templates (analyst error/success, merge x3, ranking, slow update)

SearchQA Harbor benchmark

File Role
benchmarks/searchqa-harbor/{train,val}/ 400 + 200 Harbor task directories
benchmarks/searchqa/generate_harbor_tasks.py Generates tasks from JSONL dataset
benchmarks/searchqa/evaluator.py SQuAD-style EM/F1/sub_em scoring
benchmarks/factory_harbor_agent.py SearchQAFactoryCeo agent class
factory/workflow/contributed/searchqa/workflow.py Single-node workflow with env var skill injection

Test plan

  • 150 unit tests (types, skill ops, gate, clip, adapter, trainer, yaml surface, slow update, round-trip)
  • ruff check clean
  • mypy --strict clean
  • SearchQA single task resolves through Harbor with optimized skill
  • SearchQA single task fails with blank seed, resolves with trained skill (U2 example)
  • YAML round-trip: 7 end-to-end tests including real swebench workflow
  • yaml_to_workflow() produces valid Workflow accepted by WorkflowExecutor
  • Full training loop (direct baseline): 0.725 → 0.83 val EM, 0.766 → 0.844 test EM
  • Full training loop through Harbor (1 epoch, in progress)

🤖 Generated with Claude Code

RobotSail and others added 5 commits July 16, 2026 18:02
…ation loop

Implements the core SkillOpt training loop as a standalone module at
factory/skillopt/, mapping Microsoft Research's SkillOpt approach to the
factory's benchmark infrastructure.

The loop has 6 stages:
- REFLECT: Per-trace LLM analysis via claude -p (reflect.py)
- AGGREGATE: Consolidate reflections into edit proposals (aggregate.py)
- SELECT: Textual learning rate to bound edit magnitude (select.py)
- UPDATE: Apply edits to workflow prompt_template strings (update.py)
- EVALUATE: Re-run benchmark and compare before/after scores (loop.py)
- ACCEPT/REVERT: Keep improvements, revert regressions (loop.py)

Entry point: python -m factory.skillopt --benchmark swebench \
  --workflow factory/workflow/contributed/swebench/workflow.py \
  --results-dir benchmarks/results/

Closes #1008

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ecture)

Complete rewrite of factory/skillopt/ to match Microsoft's SkillOpt
architecture. The old PoC optimized prompt_template strings in Python
source; the new architecture optimizes SKILL.md files directly with
minibatch reflection, hierarchical tree-structured merging, LLM-driven
edit ranking, and a DL-style training loop with rejected-edit buffer.

New modules: types.py, adapter.py, skill.py, gate.py, clip.py, trainer.py
New dirs: adapters/ (swebench + stubs), prompts/ (6 analyst/merge/ranking prompts)
Rewritten: reflect.py (minibatch), aggregate.py (hierarchical merge), __main__.py
Deleted: models.py, select.py, update.py, loop.py

Closes #1008

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The adapter was calling run-harbor.sh without the required --all or --task
flags, and didn't support limiting the number of tasks. Now:

- rollout() passes --all --limit N --timeout 7200 --preserve
- build_train_env/build_eval_env return the limit count directly (instances
  come from the Harbor dataset, not a pre-populated list)
- Results collected from benchmarks/results/ (written by run-harbor.sh cleanup)
- Trace dumps fetched from Langfuse for the reflect phase via langfuse_client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Sentrux Quality Report

Absolute

Scanning ....
[scan] git ls-files: 433 total, 421 kept, 12 dropped (ext:12, meta:0, big:0)
[build_project_map] 421 files, 73 unique dirs, 67 cache misses, 3.4ms
[resolve] 671 resolved, 1013 unresolved (of 1684 total specs)
[resolve_imports] project_map 3.5ms, suffix_idx 0.8ms, suffix_resolve 12.6ms, total 16.8ms
[build_graphs] 421 files | maps 1.5ms, imports 17.0ms, calls+inherit 3.3ms, total 21.8ms | 670 import, 5023 call, 8 inherit edges
sentrux check — 3 rules checked

Quality: 4564

✗ [Error] max_cc: 5 function(s) exceed max cyclomatic complexity of 30
    factory/cli/ceo.py:cmd_ceo (cc=99)
    factory/study.py:study_project_local (cc=42)
    factory/cli/_wizard.py:_welcome_wizard (cc=39)
    factory/cli/ceo.py:cmd_run (cc=37)
    factory/workflow/validation.py:validate_workflow (cc=31)

✗ 1 violation(s) found

Diff (vs base branch)

Scanning ....
[scan] git ls-files: 433 total, 421 kept, 12 dropped (ext:12, meta:0, big:0)
[build_project_map] 421 files, 73 unique dirs, 67 cache misses, 3.6ms
[resolve] 671 resolved, 1013 unresolved (of 1684 total specs)
[resolve_imports] project_map 3.7ms, suffix_idx 0.8ms, suffix_resolve 13.0ms, total 17.5ms
[build_graphs] 421 files | maps 1.6ms, imports 17.6ms, calls+inherit 3.5ms, total 22.6ms | 670 import, 5023 call, 8 inherit edges
sentrux gate — structural regression check

Quality:      4552 -> 4564
Coupling:     0.80 → 0.79
Cycles:       5 → 5
God files:    2 → 3

Distance from Main Sequence: 0.39

✗ DEGRADED
  ✗ God files increased: 2 → 3
  ✗ Complex functions increased: 32 → 35

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.22296% with 406 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.36%. Comparing base (2aa5770) to head (b9f5f00).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
factory/skillopt/adapters/featurebench.py 22.61% 130 Missing ⚠️
factory/skillopt/trainer.py 57.26% 100 Missing ⚠️
factory/skillopt/yaml_surface.py 50.35% 69 Missing ⚠️
factory/skillopt/adapters/swebench.py 73.80% 44 Missing ⚠️
factory/skillopt/reflect.py 80.98% 27 Missing ⚠️
factory/skillopt/aggregate.py 83.52% 14 Missing ⚠️
factory/skillopt/clip.py 75.00% 12 Missing ⚠️
factory/skillopt/__main__.py 78.04% 9 Missing ⚠️
factory/skillopt/skill.py 98.48% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (66.22%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1013      +/-   ##
==========================================
- Coverage   88.98%   87.36%   -1.62%     
==========================================
  Files         124      139      +15     
  Lines       14575    15840    +1265     
==========================================
+ Hits        12969    13839     +870     
- Misses       1606     2001     +395     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

RobotSail and others added 14 commits July 16, 2026 22:15
Covers types.py (model validation, strict/forbid), skill.py (all edit
ops, protected regions, patch application), gate.py (score selection,
accept/reject logic), clip.py (passthrough and truncation fallback),
adapter.py (abstract enforcement), and trainer.py (construction,
_compute_score). No external services required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover all previously-untested modules with mocked LLM/subprocess calls:
- reflect.py: fmt_trajectory, fmt_minibatch, _call_llm, _extract_json,
  _parse_raw_patch, run_error/success_analyst, run_minibatch_reflect
- aggregate.py: merge_patches, _hierarchical_merge, _parse_patch
- clip.py: LLM-driven ranking paths (success, bad JSON, truncation)
- trainer.py: full train loop with mock adapter (accept/reject/checkpoint)
- swebench.py: build_train/eval_env, _parse_jobs_dir, _collect_results,
  _extract_trace_ids, rollout error paths
- __main__.py: arg parsing, _load_adapter, unknown benchmark error
- adapter stubs: all 4 raise NotImplementedError
- adapter.py: reflect delegation, setup noop

Also fix Python 3.9 compat: add `from __future__ import annotations` to
factory/worktree.py and tests/conftest.py for `str | None` union syntax.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In overfit mode (--overfit), the trainer evaluates candidate skills on
the same task distribution used for training instead of a separate eval
split, enabling deliberate overfitting to a fixed task set. Rejected
edits are fed back into reflect prompts via step_buffer_context so the
LLM avoids re-proposing them. --results-from loads pre-existing rollout
results for the first step baseline, avoiding redundant Harbor runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SwebenchAdapter: add instances field, use --include-task-name instead
  of --limit when instances are pinned
- run-harbor.sh: accept and forward --include-task-name flags to Harbor
- Gate: add accept_ties param (>= for current_score in overfit mode)
- Trainer: pass accept_ties=overfit, summarize rejected edits (cap 2000
  chars) instead of dumping full patch text
- CLI: add --instances flag for comma-separated instance IDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…collision bugs

Two bugs prevented SkillOpt from iterating on the same pinned tasks:
1. Harbor dedup: back-to-back runs could produce empty results because
   _find_latest_result_file() might pick a stale file from a prior run.
2. Result file collision: newest *-swebench-full.json wasn't necessarily
   from the current run.

Fix: _clean_result_files() deletes all *-swebench-full.json before each
Harbor invocation, and rollout() now logs an error with diagnostics
(instances, returncode, stderr tail) when results are empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only task_prompt_* slot values are mutable. The reflect phase now shows
prompt slots (not full SKILL.md) to the LLM analyst and parses SlotEdit
proposals, converting them to Edit(op=replace) for the unchanged
aggregate/clip/skill pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…AML surface is active

The YAML slot values don't appear verbatim in the rendered SKILL.md because
the renderer transforms them. Instead of apply_patch (text search/replace),
load the Python workflow definition, override prompt_template slots with the
new values, and call the deterministic renderer to regenerate the SKILL.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rejects edits where the line-level diff magnitude exceeds the learning
rate, preventing oversized prompt mutations from bypassing the gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nforcement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The adapter was dropping prompt_slots and prompt_slots_text kwargs,
causing the analyst prompt to show {{PROMPT_SLOTS}} literally instead
of the actual slot values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The optimizer was rewriting entire prompts (19-26 line changes) every step,
exceeding the learning rate of 5 and causing 100% rejection rate. Now the
analyst prompts include the LEARNING_RATE constraint explicitly so the LLM
knows to make small incremental changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… pipeline

The aggregate/clip LLM calls can subtly modify edit target text (whitespace,
truncation), causing exact-match validation to reject valid prompt edits.
Now uses prefix matching on the first 200 chars with stripped whitespace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Steps with magnitude=0 were running full Harbor eval rollouts on unchanged
skills, then "accepting" phantom improvements from random batch variance.
Now rejects immediately when no actual prompt change occurred.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ranking LLM was reconstructing Edit objects from scratch instead of
passing through the originals, producing empty/garbled edits (magnitude=0).
Now the ranking prompt asks for selected_indices (0-based) and clip picks
edits from the original patch by index. Also skips eval when no actual
prompt changes occurred (magnitude=0).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@osilkin98 osilkin98 changed the title fix: SwebenchAdapter uses Harbor --all --limit --preserve for benchmarks feat: SkillOpt — benchmark-driven prompt optimization for workflow SKILL.md files Jul 20, 2026
RobotSail and others added 8 commits July 21, 2026 14:13
Add yaml_to_workflow() and workflow_to_yaml() to yaml_surface.py for
closing the SkillOpt loop: edited YAML annotations can now be converted
back into Pydantic Workflow objects for the deterministic executor.

Key design decisions:
- yaml_to_workflow accepts optional workflow param for testing with
  unregistered workflows
- _strip_exporter_suffix removes Read/Write lines appended by
  skill_export to prevent duplication on re-export
- TYPE_CHECKING import for Workflow avoids circular imports

Includes 7 comprehensive round-trip tests covering: no-edit identity,
prompt edits, multi-line edge cases, real swebench workflow, gate nodes,
selective multi-node edits, and structural field preservation.
Replace the stub with a working adapter mirroring SwebenchAdapter but
with featurebench-specific differences: exact filter style (no glob
prefix), *-featurebench-full.json result files, 'featurebench' benchmark
name in rollout, 'feature_implementation' task type, and the
workflow-featurebench SKILL.md path.

Update the test to verify actual behavior instead of expecting
NotImplementedError.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- benchmarks/searchqa/: dataset loader (HF + built-in sample), SQuAD-style
  EM/F1 evaluator, task template with <answer> tags, post-eval script
- SearchQAFactoryCeo agent in factory_harbor_agent.py
- searchqa case in config.sh benchmark configuration
- factory/skillopt/adapters/searchqa.py: SearchQAAdapter for SkillOpt
  training (hard=EM, soft=F1)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Minimal 3-node pipeline (study → builder → auto_merge) with a seed
prompt matching SkillOpt's empty QA skill. Uses sonnet model with 300s
timeout for fast answer extraction from search passages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… Counter-based F1

- dataset.py: load from kyunghyuncho/search_qa (HuggingFace), export
  train/val/test splits as JSONL with [DOC] separators, keep SAMPLE_DATA
  fallback
- workflow.py: simplify to single builder AgentNode (no study/auto_merge),
  timeout 120s, model sonnet
- evaluator.py: Counter-based token F1, add sub_em metric, extract_answer
  falls back to last non-empty line
- test_workflow.py: updated assertions for 1 node, 0 edges
- Add searchqa to QA_EXEMPT_WORKFLOWS in test suites
- Add README.md for contributed workflow
… loader

The old source (kyunghyuncho/search_qa with raw_jeopardy config) fails due
to a legacy loading script. Switch to lucadiliello/searchqa which has the
data pre-formatted with [DOC] separators. Map fields directly (key->id,
question, context, answers). Remove hardcoded sample data fallback and test
split (dataset only has train + validation).

Exported: 117,384 train + 16,980 val = 134,364 total items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Harbor dependency

SearchQA is text-in/text-out QA — routing through Harbor containers was
massive overkill. The adapter now loads JSONL datasets directly, calls
`claude -p` in parallel via ThreadPoolExecutor, and scores with the
inline evaluator (EM/F1). Also registers searchqa in __main__.py and
adds --dataset-dir CLI arg.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t LLM calls

The rendered SKILL.md wraps the prompt inside factory agent --task "..."
blocks. For direct claude -p calls, we need just the QA skill text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RobotSail and others added 8 commits July 22, 2026 05:23
- Add --model sonnet flag to _call_claude() so the student model is
  always sonnet regardless of ANTHROPIC_MODEL env var
- Strip task instructions from workflow prompt_template to match
  SkillOpt's exact blank seed format
- Move task instructions (answer format, <answer> tags) into the
  adapter's rollout prompt where they belong

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed writes set from builder node so skill_export doesn't append
"Write output to:" metadata to the prompt_template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SkillOpt's edit_budget limits the NUMBER of edit operations (append/replace/
delete/insert_after) per step, not the total lines changed. Our implementation
was measuring line-level diff which is the wrong abstraction — a single
"append a 5-line rule" is 1 operation but 5+ lines.

The clip step already enforces max_edits at the operation level. Removed the
redundant line-level magnitude check from the trainer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SkillOpt evaluates on the entire validation split at every gate decision.
We were using batch_size (40) which made scores noisy (each question = 2.5%).
Now passes env_num=0 to build_eval_env which returns the full val set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…adroom

Sonnet baseline is 87.5% EM — too high for the optimizer to improve.
Haiku baseline is ~75% EM, giving 25 points of headroom.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…izer

- Replace generic code-agent analyst prompts with QA-specific versions
  that include failure type categories (rule_missing, rule_wrong,
  rule_ignored, answer_format, other) and QA analysis process
- Enrich SearchQA adapter rollout results with question, gold_answers,
  prediction, and trace_dump in extras dict
- Set fail_reason to 'EM=0: predicted X but expected Y' instead of empty
- Update fmt_minibatch_trajectories to render question, predicted answer,
  and gold answers from extras for the analyst to reason about

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 implementation of longitudinal skill refinement that runs at epoch
boundaries, comparing rollout performance of the same samples under
consecutive skill versions to identify regressions and persistent failures.

- Create factory/skillopt/slow_update.py with field manipulation (inject,
  extract, replace), comparison pair building, and LLM-driven optimizer call
- Create factory/skillopt/prompts/slow_update.md adapted for QA optimization
- Add SlowUpdateResult model to types.py
- Integrate _run_slow_update_epoch into trainer.py training loop
- Add --slow-update CLI flag to __main__.py
- Add 12 tests covering field manipulation, comparison pairs, and formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@osilkin98 osilkin98 changed the title feat: SkillOpt — benchmark-driven prompt optimization for workflow SKILL.md files feat: SkillOpt — benchmark-driven prompt optimization with YAML surface + SearchQA Jul 24, 2026
RobotSail and others added 8 commits July 24, 2026 19:59
Generate Harbor task directories (task.toml, instruction.md, gold.json,
Dockerfile, test.sh) from SearchQA JSONL splits so the benchmark can run
through `factory workflow run searchqa` inside Harbor containers.

- Add benchmarks/searchqa/generate_harbor_tasks.py script
- Generate 400 train + 200 val Harbor tasks under benchmarks/searchqa-harbor/
- Update config.sh to use BENCH_LOCAL_PATH for searchqa
- Update SearchQA workflow prompt with task scaffolding (read instruction,
  write answer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generated 400 train + 200 val task directories with task.toml, instruction.md,
Dockerfile, and test.sh verifier. Fixed network_mode and gold.json path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Harbor expects /workspace/reward.txt with a float score. The test.sh
was only printing to stdout without writing the reward file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Harbor expects reward at /logs/verifier/reward.json, not /workspace/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…irect claude calls

- Replace direct `claude -p` invocations with `run-harbor.sh searchqa --all`
- Model implementation on SwebenchAdapter: _clean_result_files, _find_latest_result_file,
  _collect_results, _parse_jobs_dir helpers
- Pass skill content via SEARCHQA_SKILL_B64 env var (base64-encoded) to avoid
  shell escaping issues with multiline content
- Modify SearchQA workflow to decode SEARCHQA_SKILL_B64 and use as prompt_template,
  falling back to the hardcoded default
- Add SEARCHQA_SKILL_B64 pass-through in run-harbor.sh via --ae flag
- Set FACTORY_GIT_REF to current git commit so container installs from our branch
- Remove _call_claude, _extract_skill_prompt, _extract_answer, _load_jsonl

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The SKILL.md frontmatter (---\nname:...) was being passed as a CLI arg
to claude, which interpreted --- as a flag. Now extracts just the prompt
text before base64-encoding for the container env var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…/answer)

The env var override was replacing the ENTIRE prompt including the
infrastructure instructions (read task, write answer.txt). Now the
env var only overrides the skill rules section; the fixed instructions
are always appended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- rollout() detects split from env_manager tuple (build_eval_env returns ("val", limit))
- SEARCHQA_SPLIT env var controls which Harbor task directory is used
- config.sh reads SEARCHQA_SPLIT (defaults to train)
- Results enriched with verifier predicted/gold answers for reflect phase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@osilkin98 osilkin98 changed the title feat: SkillOpt — benchmark-driven prompt optimization with YAML surface + SearchQA feat: SkillOpt — benchmark-driven prompt optimization via Harbor + YAML surface Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants