Skip to content

Greenfield ML auto-research flow + engine support - #2

Merged
cgpadwick merged 13 commits into
masterfrom
ml-greenfield-workflow
Jun 9, 2026
Merged

Greenfield ML auto-research flow + engine support#2
cgpadwick merged 13 commits into
masterfrom
ml-greenfield-workflow

Conversation

@cgpadwick

Copy link
Copy Markdown
Owner

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)

  1. engine: reset nested-loop counters on subflow entry — a retry_loop nested in a counting_loop now gets a fresh attempt budget each outer iteration (Subflow.prep clears its own counters). Required for the implement→verify retry inside the hill-climb.
  2. engine: configurable workspace root, venv auto-activation, append_file — tools/commands operate in a --workspace dir (decoupled from where the flow lives, {{ workspace }}); every command auto-activates the workspace venv once it exists (no source in prompts); new append_file tool.
  3. docs: greenfield ML design sketch — maps MLE-Beast's pipeline onto cwe primitives; locked-in decisions (user-provided metric direction, LLM-written train harness, base-stack setup, greenfield-only).
  4. engine: expose {{ flow_dir }}; trim run-summary file list — flows can ship helper scripts a command invokes; run summary shows real outputs, not .git/.venv internals.
  5. flows: greenfield ML auto-research flow (MNIST hill-climb)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-flash via 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) and keep_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

  • Setup uses the CUDA-only cgpadwick/ml-frameworks stacks on GPU boxes; a lean CPU torch fallback keeps it portable.
  • git_init/baseline_commit are LLM agents (per request, readable skills) — trade a little determinism for prose; keep_or_revert stayed deterministic Python.

🤖 Generated with Claude Code

cgpadwick and others added 5 commits June 7, 2026 17:55
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 cgpadwick left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + reset pairs) 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. The test_nested_retry_loop_resets_each_outer_iteration test (6 = 2×3, not 4) pins exactly the bug. Good.
  • venv_env existence-gate is a genuinely elegant bootstrap solution: the command that creates the venv runs with system Python, everything after auto-activates it — no source in 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: 98best_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.py has 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 across git clean) would be cheap insurance — especially the research-log save/restore, which is subtle. (flows/greenfield_ml/keep_or_revert.py)
  • improved = cand > best treats 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_env is Unix-only (bin, not Scripts). 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()) while flow_dir is .resolve()d into the seed — a relative --workspace leaks a relative {{ workspace }} into prompts/commands. setup_env.py re-resolves it, so it works today, but resolving once in build_flow would 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_commit moving 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

cgpadwick and others added 3 commits June 7, 2026 19:16
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>
Base automatically changed from initial-development to master June 9, 2026 02:12
cgpadwick and others added 3 commits June 8, 2026 19:16
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>
Comment thread cwe/cli.py Outdated
flag = {"--provider": "type", "--model": "model", "--base-url": "base_url"}
optflag = {"--workspace": "workspace", "--venv": "venv"}
i = 0
while i < len(args):

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really weird, why aren't we using argparse here??!?!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cwe/cli.py Outdated
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):

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like old c code lol

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, fair :) that index-walking while i < len(args) loop is exactly the C-ism — argparse handles all of it now (3ecfeb8).

Comment thread cwe/cli.py Outdated


# noise dirs the run-summary file-diff should ignore (tooling/env/data, not user output)
_SNAPSHOT_SKIP = {".git", ".venv", "venv", "__pycache__", ".pytest_cache",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt this the same as in the .gitignore? or does this become the gitingore file?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

cgpadwick and others added 2 commits June 8, 2026 19:40
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>
@cgpadwick

Copy link
Copy Markdown
Owner Author

Thanks — worked through the review. Status:

Inline comments (cli.py) — fixed in 3ecfeb8:

  • Replaced the hand-rolled index parser with argparse (real run subcommand, typed options, -v/-q, free --help).
  • Renamed _SNAPSHOT_SKIP_SUMMARY_SKIP_DIRS with a comment clarifying it's a display-only filter for the run-summary's "files written" line (not a .gitignore, never written to disk).
  • Added tests/test_cli.py.

#1 / #2 (score read from prose, gameable + scale-blind) — fixed in b227ee2:

  • 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. A missing/malformed/out-of-range value yields no capture, leaving the score untouched instead of spuriously meeting the target (Test accuracy: 98 no longer "passes"). Added tests/test_read_score.py.

Already addressed earlier on the branch:

  • Retry LLM calls with exponential backoff #3 supply-chain — setup_env.py has --ref pinning + a "fetching/executing remote code" warning (47a0415).
  • Workspace .resolve() consistency and the venv_env POSIX-only note (47a0415).
  • keep_or_revert.py unit test (47a0415); nested-loop reset + stale-_feedback clearing (760c9b9).

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).

@cgpadwick
cgpadwick merged commit 98c8a80 into master Jun 9, 2026
6 checks passed
@cgpadwick
cgpadwick deleted the ml-greenfield-workflow branch June 9, 2026 02:50
cgpadwick added a commit that referenced this pull request Jun 9, 2026
…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>
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.

1 participant