Greenfield ML auto-research flow + engine support - #2
Conversation
A loop primitive is a Subflow; when an outer counting_loop re-enters an inner retry_loop, its stale attempt counter in shared["_iter"] made it exit early. Subflow.prep now clears its own counter namespaces on every entry, so nested loops get a fresh budget each outer iteration. No-op for non-nested loops. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Workspace root: tools/commands operate in a workspace dir (--workspace / yaml
workspace: / default flow dir), decoupled from where the flow+skills live;
exposed to skills as {{ workspace }}. Backward compatible.
- Venv auto-activation: every command injects PATH/VIRTUAL_ENV for the workspace
venv once it exists on disk (--venv / yaml venv: / default .venv), so a venv
created by `setup` is used by all later commands without sourcing.
- append_file tool: first-class append (research_log.md / training.log are
append-only), rounding out read/write/append/edit/delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Maps MLE-Beast's hill-climb pipeline onto cwe primitives (setup, download_data, implement→verify retry, train, evaluate, propose, keep_or_revert), with locked-in decisions: user-provided metric direction, LLM-written train harness, base-stack setup, greenfield only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Seed flow_dir (absolute path of the flow's directory) into the shared store so flows can ship helper scripts a command invokes (e.g. setup_env.py, keep_or_revert.py) — resolves the earlier "skill-bundled scripts can't run when workspace != flow dir" limitation. - Run-summary file diff now skips tooling/env/data dirs (.git, .venv, data, __pycache__, .pytest_cache, ...) so the "files:" list shows real outputs, not hundreds of .git/.venv internals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A self-contained MLE-Beast-style pipeline on cwe primitives:
setup(ml-frameworks) -> git_init -> download_data -> [implement -> verify]x3
-> train -> evaluate -> baseline_commit
-> hillclimb counting_loop (until target or plateau):
propose -> [implement -> verify]x3 -> train -> evaluate -> keep_or_revert
- setup_env.py: GPU -> clone cgpadwick/ml-frameworks and poetry-install the CUDA
stack into the workspace venv; no GPU -> lean CPU torch fallback. Idempotent.
- keep_or_revert.py: deterministic git keep/revert + best_score/failures capture.
- git_init / baseline_commit: prose skills; implement/verify/train/evaluate/propose
agents read logs and report (train = log analyzer, evaluate = eval-log reader).
Verified end-to-end on a 4070 (torch 2.3.1+cu121): baseline 0.9713 -> kept 0.9813,
beating the 0.97 target via a real hill-climb iteration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cgpadwick
left a comment
There was a problem hiding this comment.
Code Review — Greenfield ML auto-research flow + engine support
Stacks on #1 (base initial-development). Two cleanly separable halves: small, well-tested engine additions (configurable workspace, venv auto-activation, append_file, nested-loop counter reset, {{ flow_dir }}), and a greenfield ML flow (8 skills + setup_env.py + keep_or_revert.py). The engine half is excellent; the flow half has one integrity issue worth addressing before relying on its results.
Engine changes — strong, and they close prior gaps
- Nested-loop reset (
Subflow.prep+resetpairs) is the right fix and is well-targeted: it also clears_feedback[action_id]on retry-loop entry, which resolves the stale-feedback nit I raised on #1. Thetest_nested_retry_loop_resets_each_outer_iterationtest (6 = 2×3, not 4) pins exactly the bug. Good. venv_envexistence-gate is a genuinely elegant bootstrap solution: the command that creates the venv runs with system Python, everything after auto-activates it — nosourcein prompts, fully deterministic. Tests cover present/absent/PATH-ordering.- Workspace decoupling is clean; resolution order (arg → YAML → flow dir) is documented and back-compat preserved.
Issues & risks
1. (Most important) The score is self-reported by the LLM, not read from the source-of-truth file — it's gameable and diverges from your own design. evaluate/baseline_eval capture best_score/candidate_score via set: regex over the agent's free-text reply ("Test accuracy:\\s*..."). The evaluate skill tells the agent to run evaluate.py, but nothing forces it to — the agent can emit Test accuracy: 0.99 without running anything, or edit_file evaluate.py to print a fixed number (it has edit_file in its tool list and is instructed to "fix it and rerun" until a score appears). Since loop termination (exit_when: best_score >= target) keys off this number, the model is structurally incentivized to produce a passing line. Your own design doc (§1, §6) says "Score = source of truth file: eval_results.json" — but the implementation reads prose instead. Recommend: capture the metric from a deterministic command step, e.g. python -c "import json;print('SCORE='+str(json.load(open('eval_results.json'))['value']))", and set: off that command's stdout. That restores the file-as-truth contract and removes the gaming surface. (flows/greenfield_ml/flow.yaml evaluate/hc_eval, evaluate/skill.md)
2. Score-scale mismatch silently passes the target. [0-9]*\\.?[0-9]+ happily captures Test accuracy: 98 → best_score=98, and 98 >= 0.97 exits the hill-climb on iteration 0 as "target met." Reading from eval_results.json (a known float in [0,1]) per #1 also fixes this; otherwise consider validating the captured value's range. (flows/greenfield_ml/flow.yaml)
3. Supply-chain / arbitrary execution in setup_env.py. It clones cgpadwick/ml-frameworks (--depth 1, then git pull --ff-only on re-run) and runs poetry install, which executes third-party build code into the workspace venv. It's your own repo so this is acceptable, but worth a one-line comment/README note that setup runs remote code, and pinning the clone to a tag/SHA rather than tracking the default branch would make runs reproducible. (flows/greenfield_ml/setup_env.py:install_gpu_stack)
Minor / nits
keep_or_revert.pyhas no test, yet it's the deterministic core of the hill-climb (improve→commit→reset-failures vs revert→increment). A tiny unit test over the four branches (improved/not, lower_is_better true/false, failure-counter math, research-log preservation acrossgit clean) would be cheap insurance — especially the research-log save/restore, which is subtle. (flows/greenfield_ml/keep_or_revert.py)improved = cand > besttreats ties as regressions (revert + failure++). Fine, but worth a comment since a re-run yielding the identical score will count as a plateau step.venv_envis Unix-only (bin, notScripts). Fine for Linux/WSL; note it if Windows is ever a target. (cwe/tools.py:venv_env)- Workspace path not resolved (
Path(workspace or ...).expanduser()) whileflow_diris.resolve()d into the seed — a relative--workspaceleaks a relative{{ workspace }}into prompts/commands.setup_env.pyre-resolves it, so it works today, but resolving once inbuild_flowwould be more consistent. (cwe/hydrate.py) - Nested-loop
_iter[name]reset means the run-summary under-reports total iterations for nested loops (only the last entry's count survives). Cosmetic. (cwe/primitives.py+cli._print_summary) git_init/baseline_commitmoving from deterministic git to LLM agents is acknowledged in the PR; reasonable given the prose-readability goal, just trades a little determinism on a step that can't really go wrong.
Test coverage
Engine additions are well covered (workspace x3, venv x3, nested reset, append_file) — 30 passing offline. Gaps: the two shipped Python helpers (keep_or_revert.py, setup_env.py) have no unit tests despite being deterministic and testable (see nit above), and the ML flow is live-only (acknowledged, reasonable).
Verdict: Engine half is clean, tested, and closes real gaps — merge-ready. For the flow, I'd fix #1 (read the metric from eval_results.json via a command, not the agent's reply) before treating any "beat the target" result as trustworthy; the rest are minor. Nice work overall.
🤖 Generated with Claude Code
Addresses PR #2 review #1 (score self-reported by the LLM). train and evaluate are now deterministic `command` steps (`python train.py --epochs {{ train_epochs }}` / `python evaluate.py`) with a FIXED epoch budget held constant across every hill-climb experiment, so the metric is captured from a real run's stdout rather than an agent's free text. The implement/verify/propose skills are hardened to forbid running training or drifting the epoch/dataset budget; the train and evaluate LLM skills are removed (the LLM now only writes code and proposes ideas).
- setup_env.py: add --ref to pin ml-frameworks to a trusted tag/branch instead of tracking its default branch, and warn on stderr that setup fetches and executes remote code (review #3). - keep_or_revert.py: document that ties (cand == best) count as a plateau step (revert), not an improvement. - tests: add test_keep_or_revert.py covering keep/revert/tie, lower-is- better, the failure counter, and research-log preservation across the git clean (the helper previously had no coverage). - hydrate.py: resolve the workspace path so {{ workspace }} is always absolute/canonical (matching flow_dir), instead of leaking a relative --workspace into prompts/commands; update workspace tests accordingly. - tools.py: note venv_env is POSIX-layout only (<venv>/bin). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Proposal critic: wrap propose in a retry_loop (propose -> proposal_critic) so each proposal is vetted (duplicate? specific? grounded? bold enough on a plateau?) before the expensive implement+train. Critic feedback is re-injected into the next proposal via the existing retry feedback path. - Remove the consecutive-failures early stop: a revert is normal in a hill-climb, so the loop now runs until target met or max_iterations (the compute budget). A plateau should make the proposer escalate (critic enforces this), not quit. - report.py: deterministic HTML research report at the end — summary cards, hill-climb plot (best line + kept/reverted points), experiments table with proposal text, and best-architecture introspection. - keep_or_revert.py records each experiment to a gitignored experiments.jsonl (survives git clean; stdout unchanged so the unit test stays green); propose writes its proposal to proposals/ for the report. Verified live on Fashion-MNIST: baseline 0.9193 -> 0.9368 across 6 vetted experiments (cosine annealing, label smoothing, residual connections kept; regressions and a crashed train reverted), report.html generated with plot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The architecture introspection grabbed the first nn.Module in model.py, which for a residual net is BasicBlock (a helper block needing constructor args) -> "could not instantiate" in the report. Now it instantiates every no-arg nn.Module subclass and picks the one with the most parameters (the full model), preferring a class train.py actually instantiates. Falls back cleanly to listing class names when none are no-arg constructable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ments) A new report_narrative agent reads the logs (research_log.md, experiments.jsonl) and the final model.py, then writes a prose summary to report_narrative.md: how the final architecture works, the experiments that were KEPT (with scores and why each helped), notable reverts, and key takeaways. report.py renders that markdown into a "Summary" callout at the top of report.html (minimal md->HTML, no new deps). The deterministic plot/table/architecture sections are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deepseek-v4-flash sometimes ended its critique without an ACTION line, which parsed as "default" (the proposal then proceeded by fallback) so the critic rarely hard-rejected. The prompt now mandates that every reply end with exactly `ACTION: pass` or `ACTION: fail` on its own final line, defaulting to pass when unsure. Verified live: vague/duplicate/novel proposals all return a clean verdict (and a known-reverted duplicate is now failed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| flag = {"--provider": "type", "--model": "model", "--base-url": "base_url"} | ||
| optflag = {"--workspace": "workspace", "--venv": "venv"} | ||
| i = 0 | ||
| while i < len(args): |
There was a problem hiding this comment.
this is really weird, why aren't we using argparse here??!?!
There was a problem hiding this comment.
Agreed — rewritten with argparse in 3ecfeb8: a real run subcommand, typed options, mutually-exclusive -v/-q, and a free --help. The hand-rolled parser is gone.
| if a in flag and i + 1 < len(args): | ||
| overrides[flag[a]] = args[i + 1] | ||
| i += 2 | ||
| elif a in optflag and i + 1 < len(args): |
There was a problem hiding this comment.
this looks like old c code lol
There was a problem hiding this comment.
Ha, fair :) that index-walking while i < len(args) loop is exactly the C-ism — argparse handles all of it now (3ecfeb8).
|
|
||
|
|
||
| # noise dirs the run-summary file-diff should ignore (tooling/env/data, not user output) | ||
| _SNAPSHOT_SKIP = {".git", ".venv", "venv", "__pycache__", ".pytest_cache", |
There was a problem hiding this comment.
isnt this the same as in the .gitignore? or does this become the gitingore file?
There was a problem hiding this comment.
Good question — it is neither. It is a display-only filter for the run summary's "files written:" line, so it does not list hundreds of .git/.venv/data internals. It is never written to disk and is not a .gitignore; the overlap with common ignore patterns is just because those dirs are noise either way. Renamed to _SUMMARY_SKIP_DIRS with a clarifying comment in 3ecfeb8.
Replace the hand-rolled index-based flag parser with argparse — a real subcommand, typed options, mutually-exclusive -v/-q, and a free `--help`. --set values are still JSON-coerced. Rename _SNAPSHOT_SKIP -> _SUMMARY_SKIP_DIRS with a comment clarifying it is a display-only filter for the run summary's "files written" line (NOT a .gitignore, never written to disk). Adds tests/test_cli.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ose (PR #2 #1/#2) The hill-climb captured best_score/candidate_score from the free-text "Test accuracy:" line, which is gameable and scale-blind ("Test accuracy: 98" parsed as 98.0 and instantly met a 0.97 target). evaluate/hc_eval now run evaluate.py then read_score.py, which reads eval_results.json['value'] (the documented source of truth), validates it's a finite fraction in [0,1], and prints SCORE=<value> only then — so a missing/malformed/out-of-range value yields no capture and leaves the score untouched rather than spuriously passing. implement contract notes eval_results.json['value'] is the authoritative score. Adds tests/test_read_score.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — worked through the review. Status: Inline comments (cli.py) — fixed in
#1 / #2 (score read from prose, gameable + scale-blind) — fixed in
Already addressed earlier on the branch:
Suite is at 42 passing offline. Remaining items are the acknowledged cosmetic nits (nested-loop run-summary under-count; git_init/baseline_commit as agents). |
…ltered Address PR #4 review: #1 (correctness): CommandPolicy.check() returned on the first deny match, so an allow could waive a co-occurring un-allowed deny — `allow: [rm -rf ./build]` plus `rm -rf ./build && rm -rf /` was permitted and the chained rm ran. An allow is now a *whole-command* carve-out (re.fullmatch on the stripped command): it overrides the denylist only when it matches the ENTIRE command, so a carved-out prefix can never wave through a chained extra. Added tests for the chained-rm/mkfs cases. #2 (docs): note that the policy guards the LLM's run_command tool, not deterministic command: (CommandNode) steps, which are author-written and run unfiltered — in config.py docstring and the README. Also: make the config.py module docstring a raw string to drop the invalid-escape DeprecationWarning (the regex \s/\b/\. in the example YAML). Suite: 115 passed, 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stacks on #1. Adds the engine features needed for real ML auto-research flows, then a self-contained MLE-Beast-style greenfield pipeline that runs end-to-end and beats its target via hill-climbing.
Review by commit (5)
retry_loopnested in acounting_loopnow gets a fresh attempt budget each outer iteration (Subflow.prepclears its own counters). Required for the implement→verify retry inside the hill-climb.--workspacedir (decoupled from where the flow lives,{{ workspace }}); every command auto-activates the workspace venv once it exists (nosourcein prompts); newappend_filetool.{{ flow_dir }}; trim run-summary file list — flows can ship helper scripts a command invokes; run summary shows real outputs, not.git/.venvinternals.flow.yaml+ 8 skills +setup_env.py(ml-frameworks GPU stack / CPU fallback) +keep_or_revert.py(deterministic git keep/revert).Verified end-to-end
Ran live on an RTX 4070 with
deepseek/deepseek-v4-flashvia OpenRouter (torch 2.3.1+cu121):init → baseline 0.9713 → keep 0.9813, beating the 0.97 target via a real hill-climb iteration;git_init/baseline_commit(prose skills) andkeep_or_revert.py(Python) all behaved.Tests
Engine suite: 30 passing (unit + integration, offline). CI runs them on push/PR across Python 3.10–3.12. The ML flow is live-only (needs a provider + GPU), not part of the offline suite.
Notes
cgpadwick/ml-frameworksstacks on GPU boxes; a lean CPU torch fallback keeps it portable.git_init/baseline_commitare LLM agents (per request, readable skills) — trade a little determinism for prose;keep_or_revertstayed deterministic Python.🤖 Generated with Claude Code