Skip to content

fix: resolve mypy error and add DataNode source_path test coverage - #15

Draft
colehurwitz wants to merge 68 commits into
colehurwitz:mainfrom
akashgit:factory/run-3110c50a
Draft

colehurwitz wants to merge 68 commits into
colehurwitz:mainfrom
akashgit:factory/run-3110c50a

Conversation

@colehurwitz

Copy link
Copy Markdown
Owner

Closes akashgit#1483

Changes

  • mypy fix: Added assert self.workflow is not None before WorkflowExecutor construction in _step_with_data_node() (factory/inner_loop.py:391). This is safe because the method is only called after _workflow_has_data_node() which already checks self.workflow is not None.
  • Test coverage for source_path code paths (factory/workflow/executor.py lines 672-693):
    • directory source_format: temp dir with subdirs → each becomes a DataItem
    • jsonl source_format: temp .jsonl file → each JSON line becomes a DataItem
    • csv source_format: temp .csv file → each row becomes a DataItem
    • Non-existent source_path: gracefully yields empty items list
  • Test for _step_with_data_node() (factory/inner_loop.py): verifies the delegation path produces the correct CycleRecord

🤖 Generated with Claude Code

mihirathale98 and others added 30 commits August 25, 2026 14:14
Reconcile with main's removal of dead workflow modes (#1346/#1376).
Main deleted the mode definitions, tests, and routing entries; this
change keeps the complementary piece: graceful migration of legacy
mode names to design via the DEAD_MODES map, auto-routing of all
project states to design, and the deprecation/migration test coverage.

Squashed from 25 commits on factory/run-bf11e015.
)

* feat: package ecosystem prototype — composable workflow subgraphs

Introduces the Package primitive and four composition operators
(Sequential, Parallel, Conditional, Loop) for building workflows
from reusable, typed subgraph units.

Key abstractions:
- Package: wraps a Workflow subgraph with typed Ports (data plane),
  StateContract (control plane), OptKnobs (optimization surface),
  and MemoryDeclarations (pluggable persistence)
- Sequential: chains packages, wiring exit to entry
- Parallel: forks into N packages via ForkNode/JoinNode
- Conditional: routes to branches via GateNode verdicts
- Loop: re-enters body on RELOOP verdict
- compile(): lowers compositions to flat mutable Workflow IR

Includes design doc (docs/design/package-ecosystem.md) covering
vision, system architecture (8 components), composition model,
three-representation model (author/optimize/distribute), and
migration path.

21 tests demonstrate wrapping existing subgraph patterns (study,
deep-QA) as Packages and composing them into larger workflows.

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

* feat: add interactive demo for package ecosystem prototype

7-section walkthrough: standard library, composition, design mode
before/after, optimization surface, parallel/conditional operators,
nested composition with loops, and the compile/serialize round-trip.

Run: uv run python examples/package_ecosystem_demo.py

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

* feat: end-to-end package ecosystem demo

Demonstrates the full composability story:
1. Two teams build packages independently (security audit, perf optimizer)
2. A third team composes them three ways (shift-left, parallel, loop)
3. All three compile to valid executable graphs
4. Optimizer searches over compositions and knob configurations
5. Winner is serialized and round-tripped as a distributable package

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

* feat: real optimization demo using factory's outer loop

Runs factory's actual mutation operators (mutate_params, mutate_prompt)
and structural analysis (compute_features, structural_hash) over
composed Package workflows. 8 generations, 32 mutations, tournament
selection, deduplication. No faked scores.

Also fixes the composition demo to include strategy package and
produce valid graphs.

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

* feat: real optimization demo using factory's outer loop

5 candidates (3 topologies + 2 mutations) ran through factory's real
WorkflowExecutor with live Claude agents against a task queue project.

Results show topology and knob choices producing measurably different
outcomes (0.875-0.955 score range). Frozen nodes protect package
internals while mutations tune agent parameters.

Baseline: 5 tests, 123 lines, score 0.417
Winner:  18 tests, 285 lines, score 0.955

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

* feat: real optimization demo using factory's outer loop

Iterative optimization over 4 generations with real Claude agents.
Score trajectory: 0.991 → 0.992 → 0.998 → 0.998 → 1.000

Fix: validate_and_repair now follows ForkNode.targets and
JoinNode.sources when computing reachability, preventing fork/join
branch nodes from being pruned as "unreachable."

9 evaluations, 50 minutes. Each generation mutates the best from the
previous generation (model, prompt, timeout), runs through the real
WorkflowExecutor, scores on real pytest results, and selects.

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

* chore: remove superseded demo scripts

Keep only package_e2e_optimize.py (the real iterative optimization).

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

* fix: lint errors in e2e demo

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

* fix: compute_features includes OptKnob values for MAP-Elites diversity (#1388)

Package.compile() now propagates knob defaults to Workflow.knob_values.
compute_features() hashes knob values into additional feature dimensions,
giving the MAP-Elites archive diversity pressure across the optimization
surface — not just graph topology.

Without this, all knob-only mutations (prompt styles, thresholds, boolean
flags) map to the same archive cell, collapsing the grid to a single-best
selector and eliminating quality-diversity search.

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

* feat: chess evolution demo — Opus-driven pipeline optimization with live UI

End-to-end demo of the Package ecosystem optimizing an LLM chess pipeline.
Haiku plays chess through a composable pipeline (Sequential, Parallel,
Conditional, Loop), while Opus drives experiment selection and invents
new prompt variants at runtime.

Key capabilities demonstrated:
- All 4 Package composition primitives in one pipeline
- MAP-Elites archive with knob-aware features for diversity
- Opus reflects on full history, then proposes experiments
- Auto-expanding OptKnobs: Opus invents new prompt variants
  that get registered and available to future generations
- Composite scoring (avg_eval - 20*blunders + 8*moves)
- Live web UI with game replay, eval charts, best-game tracking
- Recording/replay system for demos

Results: Opus-invented prompt "capture_refutation_gate" became the
top-scoring tactical style across 700+ experiments, outperforming
all hand-authored variants. ASCII board representation improved
median score by +28 points over FEN-only.

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

* feat: add expandable OptKnob for runtime surface expansion

OptKnob gains two fields:
- expandable: bool — when True, the outer loop may propose values
  beyond the initial bounds (e.g. new prompt variants, extended ranges)
- expansion_hint: str — tells the optimizer how to generate new values

Validated in the chess evolution demo: Opus invented 8 new prompt
variants at runtime, with "capture_refutation_gate" becoming the
top-scoring tactical style across 700+ experiments — outperforming
all hand-authored variants.

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

* feat: KNOB_MUTATE operator for OptKnob-aware evolution

Adds a new mutation operator that mutates knob_values on compiled
Workflows. When a Package declares OptKnobs with bounds, KNOB_MUTATE
picks a random knob and changes it to a different value from its bounds.

Changes:
- MutationType.KNOB_MUTATE added to the enum
- mutate_knob() picks from knob_bounds carried through compile()
- Workflow.knob_bounds propagated by Package.compile() alongside knob_values
- Default weight 0.25 (highest single operator) — knob tuning is the
  most common optimization when the graph topology is already good
- WeightedRandomStrategy handles KNOB in guided operator selection

This closes the gap where the outer loop could only mutate graph
topology (nodes, edges) but not the optimization surface declared
by OptKnobs. Partially addresses #1389.

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

* feat: expandable knobs in KNOB_MUTATE via expander callback

When a knob is marked expandable and all its bounds are exhausted,
KNOB_MUTATE calls an expander callback to generate a new value:

  expander(knob_name, expansion_hint, current_value, bounds) -> new_value

The expander can be backed by any LLM — Opus authoring a new prompt
variant, or a simpler model extrapolating a threshold range. New values
are automatically added to knob_bounds for future mutations.

Workflow now carries knob_expandable (name -> expansion_hint) through
Package.compile(), and apply_random_mutation accepts a knob_expander
kwarg to thread the callback through.

This completes the OptKnob lifecycle: declare -> compile -> mutate ->
expand. The chess demo's Opus prompt invention is now expressible
entirely through factory's mutation operators.

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

* feat: default_knob_expander — factory invents new knob values via CLI

KNOB_MUTATE now ships with a default expander that calls `claude` CLI
(Opus) to invent new values when expandable knobs exhaust their bounds.
No custom code needed — the caller gets prompt invention for free:

  apply_random_mutation(wf, strategy, gen)
  # KNOB_MUTATE auto-expands when needed

The expander is a simple subprocess call — synchronous, no async
dependency, works in any context. Returns a short name for prompt
knobs or a number for threshold knobs.

This eliminates the last piece of custom optimization code from
the chess demo — the main loop is now pure factory calls
(OuterLoopReflector, apply_random_mutation, MAPElitesArchive).

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

* fix: _deep_copy_workflow preserves knob_values through graph mutations

Graph mutations (NODE_INSERT, NODE_REMOVE, EDGE_REDIRECT, etc.) were
dropping knob_values, knob_bounds, and knob_expandable because
_deep_copy_workflow didn't copy them. This meant KNOB_MUTATE always
returned None after any graph mutation, silently disabling knob
optimization.

Adds tests for:
- KNOB_MUTATE within bounds
- KNOB_MUTATE with expander callback for expandable knobs
- Knob preservation through insert_node and remove_node

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

* fix: lint errors in chess demo and mutation tests

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

* fix: mypy error in KNOB_MUTATE expander passthrough

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

* fix: address 6 bugs from Factory QA review

B3 (Critical): to_dict()/from_dict() now serialize knob_values,
    knob_bounds, knob_expandable — no more silent data loss on
    checkpoint/restore.

B4 (High): Parallel() now detects node ID collisions via
    _merge_graphs(), matching Sequential's behavior.

B5 (High): Loop() now adds a PROCEED exit edge from gate to a
    new FnNode, giving loops a termination path.

B6 (High): compute_features() uses hashlib.sha256 instead of
    hash() for deterministic knob hashing across processes.

B7 (Medium): _try_mutation mutable_nodes guard now exempts
    KNOB_MUTATE alongside NODE_INSERT.

B8 (Medium): Conditional() raises ValueError for unknown
    branch labels instead of creating unconditional edges.

Adds 11 new tests covering all fixes.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the outer loop to optimize OptKnob values alongside graph
topology, with reflection-guided exploitation.

KNOB_MUTATE operator:
- Mutates knob_values within declared bounds (25% default weight)
- When bounds exhausted + knob is expandable, default_knob_expander
  (Opus via CLI) invents new values at runtime
- Guided by reflector: 70% picks the winning knob+value from
  contrastive analysis, 30% explores randomly

Knob-contrastive reflection (OuterLoopReflector):
- New knob_values_by_id param on reflect()
- Per-knob gradient: avg score per value across all individuals
- Top-K vs bottom-K knob contrast: which values consistently win
- Produces "KNOB_MUTATE: name=value outperforms ..." suggestions

Infrastructure:
- _deep_copy_workflow preserves knob fields through graph mutations
- _try_mutation exempts KNOB_MUTATE from mutable_nodes guard
- reflection_report threaded through apply_random_mutation to
  mutate_knob for guided selection

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sequential, Parallel, and Loop all collected knobs and memory from
their child packages. Conditional was missing these, silently
dropping OptKnobs and MemoryDeclarations from any package inside
a Conditional branch.

Adds 2 tests: knob/memory propagation, compile round-trip.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

* feat: add design-v2 workflow — inference-time scaling for design mode

Adds the design-v2 workflow as a contributed workflow with dynamic
research/strategy/QA directors that scale agent count at inference time
based on project complexity, replacing the static fork/join patterns.

- Research Director: decides N research directions dynamically (3-7)
- Strategy Director: spawns M strategy perspectives (2-5)
- QA Director: creates K tailored adversarial test approaches (2-5)
- User Intent Ledger: tracks idea + feedback throughout the session
- Design Doc: rewrites strategy into human-readable design document
- Synthesize QA: merges adversarial reports with confidence scoring

29 nodes, 32 edges. 63 tests covering graph structure, node properties,
edge wiring, post checks, and removed-node assertions.

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

* refactor: extract design-v2 prompts to separate module, fix unused import

- Extract all 7 prompt template constants from workflow.py into prompts.py
- Remove unused `glob` import from synthesize_qa inline Python (Path.glob used instead)
- Add comment on ADVERSARIAL_PROMPT noting it's used by QA Director when spawning testers

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

* fix: recognize design-v2 (and contributed workflows) in CLI mode routing

The CLI mode validation only checked CEO_MODES and project-local
workflows, ignoring builtin registry entries like design-v2. Two fixes:

1. Add "design-v2" to CEO_MODES in _helpers.py
2. Change fallback validation from project-only to all workflow registry
   entries, so any registered workflow (builtin, contributed, project)
   is automatically valid
3. Add "design-v2" alongside "design" in all mode routing checks so it
   supports --focus, --auto-approve, --from-plan, --just-plan

Closes #1392

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

* fix: remove design-v2 from CEO_MODES — discovered via WorkflowRegistry

design-v2 is a builtin workflow discovered by WorkflowRegistry.discover()
and passes the fallback check in _ceo_helpers.py without needing an
explicit CEO_MODES entry. The hardcoded entry was redundant.

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

* fix: wire auto_approve through to CEO task string for design/design-v2 modes

When --auto-approve is passed, the CEO now receives instructions to act
as the user at approval gates — reviewing plans against user-intent.md
and making PROCEED/feedback decisions instead of waiting for human input.

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

* fix: address 5 code review issues on design-v2 workflow (#1392)

- Add design-v2 to mode checks in run.py (auto_approve, focus, skip_improve)
- Fix init_user_intent FOCUS env var: read from FACTORY_IDEA env, backlog.md
  fallback, skip if user-intent.md already exists
- Fix single-quote injection in init_user_intent and synthesize_qa by passing
  project_path via sys.argv instead of string interpolation
- Update error messages in _ceo_helpers.py to mention design-v2
- Remove dead ADVERSARIAL_PROMPT constant, fold output format into QA_DIRECTOR_PROMPT

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

* fix: rewrite init_user_intent FnNode to avoid SyntaxError in python3 -c

The command used compound if/else blocks after semicolons in a python3 -c
one-liner, which Python does not allow. Rewritten using ternary expressions
and or-chains: early exit via (sys.exit(0) if cond else None), idea fallback
via env or backlog or default.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

* Revert "feat: design-v2 workflow — inference-time scaling for design mode (#1392)"

This reverts commit 2275f57.

* feat: design-v2 workflow — inference-time scaling for design mode

Replaces #1392 (reverted + re-landed with all fixes).

Design-v2 applies inference-time scaling (parallel generation → synthesis
→ gate) to every stage of the design pipeline:

1. Research Director — CEO designs N research directions dynamically
   (3-7 based on complexity), spawns N researchers with tailored prompts
2. Strategy Director — CEO designs M strategy perspectives dynamically
   (2-5), spawns M strategists with tailored prompts
3. Design Doc — rewrites strategy as human-readable design document
4. QA Director — CEO designs K adversarial testing approaches (2-5),
   spawns K testers + 1 code reviewer in parallel
5. User Intent Ledger — append-only file tracking all user input,
   read by QA as ground truth for acceptance criteria
6. Structured QA Gate — checks user-intent.md as primary criteria

Consistent Director pattern across all 3 stages. 29 nodes, 32 edges.
63 tests. CLI routing supports --auto-approve (CEO acts as user at
approval gates) and --focus.

Fixes from external code review:
- run.py mode checks updated for design-v2
- init_user_intent uses sys.argv (no single-quote injection)
- init_user_intent reads from backlog fallback (no missing env vars)
- Error messages mention design-v2
- Dead ADVERSARIAL_PROMPT removed
- init_user_intent SyntaxError fixed (no compound if in python3 -c)
- Code review runs in parallel as K+1th agent

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- README: add design-v2 section under Design Mode with usage examples
  and comparison table (design vs design-v2)
- README: add design-v2 to Built-in Workflows table
- CLAUDE.md: add design-v2 CLI examples to Running the factory section

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#1400)

Design-v2 applies inference-time scaling (parallel generation → synthesis
→ gate) to every stage of the design pipeline. Three Director agents
dynamically design N/M/K approaches with project-specific prompts.

29 nodes, 32 edges. 63 tests. All review fixes included.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#1401)

The prompts.py landed on main was missing the last two fixes:
- Mandatory code review as part of QA Director's Phase 2
- All K+1 agents (K adversarial + 1 code reviewer) run in parallel

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three enforcement points prevent the strategy from drifting away from
what the user asked for:

1. Strategy Director — extracts intent_items from user-intent.md, verifies
   every ask is covered by at least one perspective before spawning
   strategists. Each strategist prompt includes the relevant user asks.

2. Synthesize Strategy — 3-step process: extract intent list, synthesize
   plan, then audit coverage. Writes an Intent Coverage table mapping
   every user ask to where it appears in the plan. Fails if any ask is
   missing.

3. Gate Strategy — mandatory intent fidelity check before approval.
   Verifies the Intent Coverage table, rejects plans that drop or
   reinterpret user asks. RELOOP goes back to strategy_director.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add intent-fidelity checks to design-v2 strategy pipeline

Three enforcement points prevent the strategy from drifting away from
what the user asked for:

1. Strategy Director — extracts intent_items from user-intent.md, verifies
   every ask is covered by at least one perspective before spawning
   strategists. Each strategist prompt includes the relevant user asks.

2. Synthesize Strategy — 3-step process: extract intent list, synthesize
   plan, then audit coverage. Writes an Intent Coverage table mapping
   every user ask to where it appears in the plan. Fails if any ask is
   missing.

3. Gate Strategy — mandatory intent fidelity check before approval.
   Verifies the Intent Coverage table, rejects plans that drop or
   reinterpret user asks. RELOOP goes back to strategy_director.

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

* feat: add Overwatch agent to design-v2 workflow

Add a final verification step after QA passes that checks completeness
and honesty of the entire pipeline output before the PR reaches the user.

- overwatch (AgentNode, CEO, timeout=1800): reads user intent, strategy,
  builder output, and all QA reports; performs intent checklist, evidence
  audit, and mandatory spot checks; writes overwatch-latest.md
- gate_overwatch (GateNode, agent, CEO): proceeds to doc freshness if
  Overwatch passes, reloops to builder with specific findings if not
- Edge chain: gate_qa [PROCEED] → overwatch → gate_overwatch →
  gate_doc_freshness (replaces direct gate_qa → gate_doc_freshness)
- Graph: 29 → 31 nodes, 32 → 35 edges, validates clean

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…config (#1391)

Closes #1391

- factory/state.py: Auto-bootstrap .factory/config.json from tracked
  factory.md when config is missing (no discovery, reparse only)
- factory/worktree.py: Add _finalize_greenfield() to commit untracked
  factory.md on greenfield branches before worktree cleanup; retain
  worktree with recovery instructions on finalization failure
- Tests for all new paths: auto-bootstrap, malformed factory.md,
  greenfield detection, finalization commit, worktree retention
* fix: 7 code review findings in design-v2 workflow

- Fix IndexError crash in init_user_intent when backlog.md is whitespace-only
- Add design-v2 specific task instructions instead of inheriting P0-P3 from design mode
- Include design-v2 in banner_mode check so it shows correctly in CLI output
- Increase dedup key truncation in synthesize_qa from 80 to 200 chars
- Fix two E501 violations in _ceo_helpers.py (108 chars → multi-line)
- Add comment noting bootstrap nodes mirror definitions.design_workflow()
- Extract 45-line inline synthesize_qa script to qa_synthesis.py module

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

* fix: 8 code review findings from design-v2 round 2

- Thread auto_approve through run.py's _run_single_cycle to _build_ceo_task
- Fix new-idea design-v2 getting wrong P0-P3 instructions (check mode, not shown_mode)
- Add comment noting naive string dedup limitation in qa_synthesis
- Fix error messages referencing "--mode design" to say "--mode design/design-v2"
- Extract DESIGN_MODES constant to replace 14 scattered tuple checks
- Add TODO for bootstrap copy-paste between design and design-v2
- Show available modes in unknown mode error instead of filesystem path
- Guard qa_synthesis.main() against missing argv with usage message

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

* feat: mandatory run-the-code testers in QA Director

Two hardcoded adversarial testers that ALWAYS run, non-negotiable:
1. smoke-run — actually execute the built application (CLI, server, lib)
   with example inputs. Show commands + output. No reading code, no
   checking tests — run it.
2. user-scenario — read user-intent.md, then use the app exactly as the
   user described. The user's words become the test script.

These run alongside the K designed testers + code reviewer, all in
parallel. Total agents = K + 2 (hardcoded) + 1 (code review).

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

* fix: 3 code quality findings in design-v2 qa_synthesis and workflow

- qa_synthesis: filter out positive/neutral bullet lines (PASS, verified,
  etc.) — only lines with negative signals (fail, error, bug, missing,
  etc.) are now treated as findings
- qa_synthesis: add comment documenting dedup limitation (exact text match
  vs semantic — accepted for v1, false separation safer than false merge)
- workflow: extract init_user_intent inline script to intent_init.py module,
  matching the qa_synthesis.py extraction pattern

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

* fix: synthesize_strategy reads field + qa_synthesis empty report warning

- Add strategy-plan.json to synthesize_strategy reads (was missing,
  affects SKILL.md generation and executor ordering)
- qa_synthesis warns explicitly when no adversarial reports exist
  instead of producing a false-clean report
- Switch from negative-signal allowlist to positive-signal denylist
  so security findings ("authentication bypassed") aren't filtered out

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PROMPT_MUTATE now supports a PromptRewriter callback that REPLACES
agent prompts instead of appending snippets. default_prompt_rewriter
uses Opus via CLI — receives current prompt + reflection hint, writes a
complete replacement that can remove bad advice and fix contradictions.
Falls back to append when rewriter is unavailable.

KNOB_MUTATE now accepts a reflection_report and uses guided mutation:
70% exploitation (pick the knob+value the reflector identified as best)
and 30% exploration (random). _parse_knob_suggestion extracts structured
suggestions from ReflectionReport.mutation_suggestions.

Also changes the ReflectionReport import from TYPE_CHECKING to direct,
since it's now used at runtime in mutate_knob.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: include prompt content in MAP-Elites feature extraction (#1388)

compute_features() now hashes each AgentNode's prompt_template into a
feature dimension. Without this, PROMPT_MUTATE variants with the same
knob config all land in the same MAP-Elites cell and only the
highest-scoring one survives — effectively discarding prompt diversity.

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

* fix: cover all mutation types in MAP-Elites feature extraction (#1388)

compute_features() now includes feature dimensions for every mutation
type so that different mutations land in different MAP-Elites cells:

- NODE_INSERT/REMOVE → agent_count, depth (already covered)
- PARALLELIZE/SERIALIZE → fork_degree (already covered)
- EDGE_REDIRECT → edge_hash (sorted edge signature, 8 buckets)
- PARAM_MUTATE → param_hash (model + timeout per agent, 8 buckets)
- KNOB_MUTATE → knob value hashes (10 buckets each)
- PROMPT_MUTATE → prompt content hashes (8 buckets per agent)

Without this, PROMPT_MUTATE and PARAM_MUTATE variants with the same
topology and knob config all collapse into one cell — only the
highest-scoring one survives, discarding all diversity.

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

* fix: fixed-length feature tuples with aggregate hashes

Addresses CEO review feedback:

1. Feature tuples are now always 8 elements regardless of agent count:
   (depth, fork_degree, agent_count, gate_count, edge_hash, param_hash,
   prompt_hash, knob_hash). This fixes the variable-length tuple issue
   that caused silent truncation in zip-based Pareto comparisons.

2. Prompt and param hashing now use sorted node IDs for deterministic
   ordering across different workflow construction paths.

3. Fixed test_population.py assertion (len == 8, not 4).

4. Added tests: prompt mutations produce different features, no-agent
   workflows still produce 8-element tuples.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump CLI timeout from 60s to 120s (Opus needs time for quality rewrites)
- Add structured logging for all outcomes: success, empty, timeout, error
- Log prompt_mutate_validation_failed when rewritten prompt fails model_copy
- Log knob_expander outcomes (empty, timeout, error, success)

Every failure path in the mutation pipeline is now observable via
structlog, making it possible to diagnose why mutations fail silently.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#1394)

sample_parent() gains rank_weighted option: individuals are drawn
with probability proportional to their rank (best=N, worst=1) instead
of uniformly. This biases toward stronger parents while preserving
diversity.

Changes:
- MAPElitesArchive.sample_parent(rank_weighted=True) uses
  random.choices with rank-proportional weights
- SwarmConfig.rank_weighted_selection: bool = False (backward compat)
- SwarmEngine threads the config through to sample_parent
- 2 new tests: rank bias verification, uniform baseline

Fixes #1393

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1412)

sample_parent() now automatically enables rank-weighted tournament
selection when the archive reaches auto_rank_cell_threshold (default 8)
occupied cells, indicating enough diversity that biasing toward stronger
parents is beneficial.

This mirrors the on_plateau() pattern of adapting strategy when the
search state calls for it. On by default; opt out with
auto_rank_weighted=False.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1408)

## Summary
- New `--mode create-v2` factory workflow mode that replaces generic QA pipeline with workflow-specific testing and Overwatch verification gate
- Inherits from `create_workflow()` via inherit-and-mutate pattern (29 nodes, 33 edges)
- Adds Research Director, Strategy Director, QA Director (with mandatory workflow-validate and cli-integration testers), and Overwatch (4-step verification)
- User intent ledger threaded through 6 stages for requirement tracking
- QA synthesis glob fix: `adversarial-*` → `adversarial*` to match actual filenames
- SKILL.md line limit raised 600→1200 to accommodate v2 director patterns

## Test plan
- 150 dedicated tests (5 smoke + 145 integration) across 12 test classes
- All 16 SKILL.md validations pass
- Full suite green (no regressions)
- Ruff clean, mypy clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes to mutate_knob():

1. Guided mutation no-op detection: when the reflector suggests a value
   that's already the current value (e.g. theory -> theory), skip it
   and fall through to random selection instead of wasting an eval slot.

2. Exclude synthetic _prompt_* knobs from random selection. These are
   created by PROMPT_MUTATE to persist prompts through compile()
   round-trips and should not be randomly selected by KNOB_MUTATE.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1411)

* fix: PROMPT_MUTATE persists in knob_values for compile() round-trips

mutate_prompt() now stores the rewritten prompt in knob_values under
a synthetic `_prompt_<node_id>` key. Package.compile() reads these
back and applies them to node prompt_templates.

Without this, any consumer that rebuilds a Package from config and
calls compile() loses prompt mutations — the rewritten prompt lives
only on the Workflow IR that apply_random_mutation returned, and the
fresh compile() overwrites it with the original.

Fixes #1410.

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

* fix: preserve _prompt_* entries when compile() has declared OptKnobs

compile() was replacing knob_values/knob_expandable entirely from
declared OptKnobs, destroying _prompt_* entries before they could be
read back. Now saves and restores _prompt_* entries across the
overwrite.

Found by CEO adversarial QA (Test 3): any workflow with both
PROMPT_MUTATE and declared OptKnobs silently lost all prompt mutations.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skips hooks, LSP, and plugin loading for faster subprocess startup.
Every agent invocation saves ~1-2s of startup overhead.

Fixes #1420

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1418) (#1423)

_extract_knob_patterns now recognizes _prompt_* and prompt_* keys:
- Truncates prompt values to 100 chars for comparison (full text
  would make every value unique, defeating the contrastive analysis)
- Reports PROMPT_MUTATE suggestions instead of KNOB_MUTATE for
  prompt-type keys
- Displays truncated values (60 chars) in suggestions

Fixes #1418

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-zero exit codes from invoke_agent now log a warning and return
the stdout (which may contain partial output) instead of raising
RuntimeError that halts the entire workflow. This allows downstream
nodes to proceed even when one agent fails transiently.

Fixes #1415

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
) (#1426)

Adds async_prompt_rewriter alongside the existing sync
default_prompt_rewriter. Uses asyncio.create_subprocess_exec
instead of subprocess.run, so async consumers don't block the
event loop for 30-120s per prompt rewrite.

The sync version remains as the default for backward compatibility.
Async consumers can pass async_prompt_rewriter as the rewriter
parameter to mutate_prompt.

Fixes #1417

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cts (#1425)

* fix: use exec mode for python3 -c gate commands to avoid quote conflicts (#1416)

Gate evaluator_commands with python3 -c had nested double quotes
(e.g. print("PROCEED")) that broke shell parsing. Now detects
python3 -c commands and runs them via create_subprocess_exec with
the code as a separate argument, bypassing shell quoting entirely.

Also removed shlex.quote on {project_path} in gate commands since
exec mode doesn't need it and shell mode handles it naturally.

Fixes #1416

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

* fix: address CEO review — restore shlex.quote, use _run_shell for gates

1. Gate evaluator calls _run_shell (not _run_shell_or_exec directly)
   to preserve test mockability
2. Restored shlex.quote on project_path for gate commands — needed
   for shell-path commands with spaces

Both smoke tests now pass.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
create-v2 was missing from the focus allowlist in _validate_ceo_flags(),
causing `factory ceo --mode create-v2 --focus "..."` to error out despite
the mode being properly registered.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Custom/plugin-defined modes (e.g. find-upstream-patch, buildroot) would
complete their workflow in headless mode but then chain into discover/improve
cycles via _chain_modes(), running for 23+ minutes until timeout. The
chaining logic failed for unregistered modes because
WorkflowRegistry.get_workflow() returned None, bypassing the terminal check.

Remove _chain_modes() entirely — every mode now completes and exits. If
further work is needed, the user or CI pipeline invokes the next mode
explicitly.

Fixes: https://github.com/akashgit/refactory-midstream/issues/2415

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
colehurwitz and others added 23 commits September 6, 2026 01:33
* feat: add TaskRef model and --task-module CLI flag for outer loop task discovery

Enable projects with custom Task subclasses to pass their task class to
`factory outer-loop calibrate/evaluate` via `--task-module module:ClassName`.

- Add TaskRef(BaseModel) in factory/task.py mirroring EvaluatorRef pattern
- Add task_module field to SwarmConfig with 3-tier get_task() precedence
- Add --task-module flag to calibrate and evaluate CLI subparsers
- Add 7 tests covering resolution, errors, precedence, serialization
- Document Task Discovery in docs/outer-loop.md

Closes #1462

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

* fix: skip _inner_loop_factory when task_module is set in evaluator

When a Task is available via config.get_task(), the compose() path
uses the workflow object directly and never needs CLI mode resolution.
Move the task check before the _inner_loop_factory call so ephemeral
mode names (e.g. 'evolve-gen0-xxx') don't trigger noisy 'unknown mode'
errors. The task path now uses 'task-eval' as a simple label instead.

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

* fix: restore subset_selector wiring and deleted tests in evaluator

Restores the subset_selector wiring in _evaluate_via_inner_loop that
was removed when the task_module skip logic was added. Also restores the
two deleted tests (test_task_path_wires_subset_selector and
test_task_path_no_subset_selector_when_empty_instances), updated to
reflect the new mode_name='task-eval' behavior and assert that
_inner_loop_factory is NOT called when a task is set.

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

* fix: add --dangerously-skip-permissions to Task.run() CEO subprocess

Task.run() spawns a 'factory ceo' subprocess but was missing
--dangerously-skip-permissions, causing 'Not logged in' failures in
eval worktrees where the directory isn't trusted by Claude Code.
This matches invoke_agent() which already defaults this flag to True.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…I subprocess (#1464)

Task.run() shells out to `factory ceo`, not to `claude`. The
--dangerously-skip-permissions flag belongs to the claude CLI, not the
factory CLI, causing `factory: error: unrecognized arguments`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: close outer loop reflection→mutation wiring gaps

Wire up the 7 confirmed gaps that made the CLI-driven outer loop's
guided mutation pipeline structurally dead:

1. _cmd_evolve now loads persisted ReflectionReport from disk and passes
   it to apply_random_mutation, activating guided mutations on CLI path
2. _cmd_reflect builds knob_values_by_id from registry workflows and
   passes it to reflector.reflect(), activating knob-contrastive analysis
3. engine.py builds knob_values_by_id from population individuals and
   passes it to reflector.reflect() on the in-process path
4. _extract_knob_patterns populates prompt_improvements for prompt knobs,
   so _extract_prompt_hint returns targeted guidance instead of generic
5. _PROMPT_VARIANTS uses domain-neutral reasoning strategies instead of
   coding-specific strings
6. Benchmark configs clear dead seed_workflow name='improve' to name=''
7. Delete orphaned factory/outer_loop/prompts/reflect.md

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

* fix: update seed_workflow assertion to match featurebench.toml change

The Bug 5 fix changed benchmarks/configs/featurebench.toml seed_workflow
from 'improve' to '' (empty string), but the backward-compat test still
asserted 'improve'. Align the assertion with the config.

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

* fix: remove unused import and variable flagged by ruff

- tests/test_outer_loop/test_cli.py: remove unused MagicMock import (F401)
- tests/test_outer_loop/test_mutations.py: drop unused `result` assignment (F841)

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

* test: add coverage for knob_values_by_id wiring and malformed reflection fallback

Cover three previously-untested code paths in the outer loop:
- _cmd_reflect building knob_values_by_id from registry workflows (lines 433-437)
- _cmd_evolve gracefully handling malformed reflection JSON (lines 484-485)
- engine.evolve_generation building knob_values_by_id from population (lines 271-277)

These tests raise patch coverage from ~74% to above the 79% threshold.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…/Workflow separation (#1472)

* feat: LLM-based reflector diagnosis, typed mutation suggestions, and OptKnob propagation

Part A: Add _llm_reflect() to OuterLoopReflector — single claude -p call per
generation contrasts top-K vs bottom-K individuals, populates the previously
dead report.prompt_improvements field. Enabled via llm_reflect=True kwarg
(defaults False so existing tests don't hit real subprocess).

Part B: Add MutationSuggestion dataclass and typed_suggestions field to
ReflectionReport. All suggestion-building sites (_generate_mutation_suggestions,
_generate_structural_recommendations, _extract_knob_patterns) now produce typed
objects alongside backward-compatible string fields. select_guided_operator and
mutate_knob in mutations.py prefer typed suggestions with direct enum matching,
falling back to string substring path when typed_suggestions is empty.

Part C: Wire seed_workflow and Task.workflow() knob propagation in
_cmd_calibrate so knob_values/knob_bounds/knob_expandable from source workflows
survive into seed Individuals.

Includes 31 tests covering all three parts plus E2E integration tests verifying
reflector→mutation typed suggestion consumption and serialization round-trips.

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

* fix: guard _llm_reflect against non-dict JSON and reject empty knob values

Bug 1: _llm_reflect crashes with AttributeError when json.loads returns
a non-dict type (list, int, str, null, bool). Added isinstance(data, dict)
guard with early return.

Bug 2: _parse_knob_suggestion accepts empty-string values that silently
corrupt knob settings. Strengthened guard to also reject whitespace-only
strings.

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

* test: boost patch coverage for outer loop reflector, mutations, and CLI

Add tests covering new code paths introduced in the LLM reflector,
typed mutation suggestions, and OptKnob propagation changes:

- reflector.py: avg_steps NODE_INSERT/REMOVE suggestions, ForkNode
  parallel recommendations, _collect_individual_details edge cases,
  _llm_reflect payload truncation and error handling, _save_report
  typed_suggestion serialization
- mutations.py: select_guided_operator string fallback for NODE_INSERT
  and NODE_REMOVE, mutate_knob guided no-op when value matches current,
  float coercion in guided knob path
- cli/outer_loop.py: fix broken seed_workflow/task_module calibrate
  tests (patching load_config instead of args namespace), add reflect
  not-enough-candidates error path, evolve no-modes error, evolve
  non-dict/non-list typed_suggestions handling, evolve registry.load
  returning None

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

* refactor: split domain execution from workflow execution in Task

Remove Task.run() entirely — the Task interface is now just four hooks:
instances(), setup(), prompt(), verify(). Execution is driven by
InnerLoop._step_with_task() which runs setup → WorkflowExecutor → verify
per instance, with task.prompt() seeded as initial_context.

- Add initial_context parameter to WorkflowExecutor for domain-specific
  per-instance prompts (seeds node_context on start_node)
- Update ChessEvolveTask: remove run() override and _apply_mutation()
- Update SWEBenchTask: docstring-only (already used default path)
- Rewrite tests to mock WorkflowExecutor instead of task.run()

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

* feat: add --seed-workflow CLI flag to outer-loop calibrate

Adds explicit --seed-workflow flag that accepts a 'module.path:callable'
ref returning a Package or Workflow with OptKnobs. This is the topology
concern counterpart to --task-module (domain concern), completing the
Task/Workflow separation for the outer loop.

Precedence: --seed-workflow > config.seed_workflow > task_module fallback.
Replaces the implicit Part C knob-loading logic with cleaner elif chain.

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

* fix: make --seed-workflow required for calibrate, remove fallback paths

The outer loop evolves workflows — it needs an explicit seed workflow.
The generic benchmark workflow was always a placeholder. Remove the
implicit registry and task_module fallback paths in favor of the
explicit --seed-workflow flag.

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

* fix: remove unused imports flagged by ruff

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

* chore: remove accidentally committed .claude and cache files

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

* fix: use None sentinel for required_capabilities to distinguish unset from empty

TaskConstraints.required_capabilities defaulted to [] (falsy), so
TaskCapabilities.from_task() always fell through to exit_code inference,
adding CAN_MODIFY_CODE/CAN_RUN_TESTS/HAS_BUILDER even for domain tasks
like chess-evolve that don't need software-dev capabilities.

Changed to three-way semantics:
- None (default) = not set, fall through to inference (backward compat)
- [] = explicitly no capabilities needed (domain tasks)
- [...] = explicit capability list (unchanged)

Updated from_toml() to yield None for absent key, compose.py to check
`is not None` instead of truthiness, and chess-evolve.toml to declare
required_capabilities = [].

Closes #1472

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

* fix: LLM reflection now produces typed mutation suggestions

The _llm_reflect prompt only asked for prompt_improvements and
failure_patterns (string lists). All typed_suggestions came from
heuristic methods which are generic. Updated the LLM prompt to also
request structured mutation_suggestions with operator/target/rationale/value,
parse them into MutationSuggestion objects, and filter invalid operators.
Added workflow node IDs to the prompt context so the LLM can target
specific nodes.

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

* fix: remove unused tomllib import in test_compose

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

* fix: drop --max-turns 1 from LLM reflector, add retry

--max-turns 1 in print mode conflicts with Claude Code's internal
turn counting — sometimes an internal tool call consumes the single
allowed turn, returning "Error: Reached max turns" instead of JSON.
Removing the flag lets print mode handle turns naturally.

Also adds 3-attempt retry with exponential backoff for transient
failures (rate limits, network errors).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LL83gxqJ4qjR5oungfcVuf

* feat: add experiment context to reflector with top-down budget

_collect_individual_details now appends rec.experiments (hypothesis,
verdict, score delta) after existing fields, gated by a char_budget
parameter. _llm_reflect divides _LLM_PAYLOAD_BUDGET across individuals
(85% usable, 15% reserved for headers) and passes per-individual shares.
Experiments fill remaining space greedily — earlier experiments get
priority, hypothesis text is truncated to fit, and the list stops when
budget is exhausted.

Closes #1472

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

* fix: guard initial_context assignment to AgentNode start nodes only

The initial_context code logged 'initial_context_ignored' when the start
node was not an AgentNode, but still wrote the context to node_context
unconditionally. This meant GateNode and LLMNode paths silently consumed
context that the log claimed was ignored.

Now initial_context is only written to node_context when the start node
is an AgentNode. For other node types, the warning is logged and the
assignment is skipped.

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

* fix: repair 7 broken mutation operators on small-topology workflows

Six targeted fixes to factory/outer_loop/mutations.py:

1. Split mutable_nodes into structurally_mutable (excludes start node)
   and content_mutable (includes start node) so PROMPT_MUTATE and
   PARAM_MUTATE work when the only AgentNode is the start node.

2. NODE_INSERT now creates contextual complementary nodes with inherited
   file wiring (upstream writes → new reads, downstream reads → new
   writes) and a complementary role via _COMPLEMENTARY_ROLES mapping,
   instead of blank random-role agents.

3. NODE_REMOVE guards against removing the last AgentNode in a workflow.

4. EDGE_REDIRECT pre-filters targets using nx.ancestors() on the
   unconditional-edge subgraph to avoid creating ungated cycles,
   reducing wasted retries through validate_and_repair().

5. insert_node ID generation uses loop-until-unique pattern to prevent
   silent dict overwrite on collision.

6. mutate_params and mutate_prompt use constructor-based creation
   instead of model_copy(update=...) so Pydantic validation actually
   runs on updated fields.

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

* fix: make test_remove_last_agent_returns_none deterministic

The test used a 3-node workflow (start→agent→end) where both
agent and end were structurally mutable. random.choice could pick
the FnNode ~50% of the time, bypassing the last-AgentNode guard
and causing ~30% flake rate.

Fix: use a 2-node workflow (start→agent) so agent is the only
structurally mutable node, guaranteeing the guard is always tested.

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

* fix: propagate eval scores to population and add tournament selection in outer-loop CLI

Two bugs made the CLI outer-loop path fitness-blind:

1. `_cmd_evaluate` wrote scores to `results/gen{N}.json` but never updated
   `population/population.json`, so every individual kept `score=0.0`.
2. `_cmd_evolve` mutated `modes[:population_size]` — every candidate
   reproduced exactly once regardless of fitness, so there was zero
   selection pressure.

Changes (all confined to `factory/cli/outer_loop.py`):

- `_propagate_scores_to_population()` loads the population after evaluation,
  matches modes to individuals via the `evolve-gen{N}-{id[:8]}` naming
  convention, and writes back score + cost_usd. No-ops when the population
  file is absent.
- `_load_mode_scores()` reads `results/gen{N}.json` into a
  `{mode_name: score}` map, tolerating a missing or malformed file.
- `_select_parent()` performs k=2 tournament selection over mode names
  ranked by score; unevaluated generations degrade to uniform random.
- `_cmd_evolve` now restricts parents to the current generation's candidate
  modes (excluding `-eval-` mirrors) and draws one parent per offspring
  slot, printing the parent mode and score for observability.

Tests in `tests/test_outer_loop/test_cli.py` cover score propagation
(including missing-population and unmatched-mode paths), score loading,
tournament distribution, and a 200-trial statistical check that offspring
are biased toward higher-scoring parents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: add offspring individuals to population.json during CLI evolve

_cmd_evolve created mode files via registry.register() but never added
corresponding Individual objects to population/population.json. This
meant gen 1+ individuals had no population entry, so
_propagate_scores_to_population() could never update their scores after
evaluation — the population was frozen at gen 0.

Changes (all in factory/cli/outer_loop.py):

- _make_offspring_individual() builds an Individual from a freshly
  registered offspring mode, with proper id (via _mode_suffix),
  features (via compute_features), parent_id, and mutation_record.
- _cmd_evolve now loads the Population at the start, adds each
  offspring Individual after registry.register(), and saves back
  after the loop.

Tests in tests/test_outer_loop/test_cli.py cover offspring persistence,
id-matching for score propagation, population growth across generations,
and the no-population-file fallback.

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

* fix: remove model from param_mutate's mutable parameters

Model is infrastructure configuration, not an evolvable parameter.
param_mutate was randomly switching models between sonnet/opus/haiku,
defeating the purpose of baking a specific model into seed workflows.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… (#1477)

10 tests that chain real main(["outer-loop", ...]) calls against a shared
tmp_path project, asserting both exit codes and on-disk artifacts between
each step using real loaders (load_config, load_checkpoint, Population.load).

Tests cover:
- calibrate→evaluate, evaluate→reflect, reflect→evolve, full pipeline+status
- evaluate --project-dir mirroring to separate target directory
- promote reads mode files written by calibrate
- reflect deserializes cycle_summary.json from evaluate
- calibrate propagates knob_values/bounds/expandable into seed population
- typed_suggestions round-trip: reflect writes, evolve deserializes
- _llm_reflect mocked to prevent claude subprocess in tests
- _resolve_seed_workflow mocked for required --seed-workflow flag

Closes #1468


Claude-Session: https://claude.ai/code/session_01PcxJrv9nztJyDTkdg3v9Eg

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rkflows

Add DataItem and DataNode as workflow graph primitives, enabling
data-driven fan-out where a subgraph is executed per data item with
Semaphore-throttled concurrency and per-item fault isolation.

Touches 8 source files across 3 architectural layers:
- Models: DataItem + DataNode with exactly-one-source validation
- Executor: _execute_data with task_ref/inline/source_path resolution
- Validation: structural + reachability checks for both functions
- Skill export: DataNode rendering + subgraph skip-set + trailing else
- Inner loop: cached DataNode detection, executor delegation
- Compose: CAN_ITERATE capability inference
- MAP-Elites: appended has_data_node feature axis (9-tuple)
- Diversity: generalized to len(key) instead of hardcoded range(4)

Closes #1482

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add assert for self.workflow before WorkflowExecutor construction in
_step_with_data_node() to satisfy mypy's type narrowing. Add tests for
directory, jsonl, csv source_format paths and non-existent source_path
in the executor, plus a test for the inner_loop delegation path.

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

Adds TestDataNodeIntegration class with 5 tests covering:
- DataNode workflow routing through _step_with_data_node()
- DataNode delegates to executor once (not per-instance)
- Non-DataNode workflows still use manual per-instance loop
- compute_features detects DataNode at feature index 8
- Full pipeline: DataNode → InnerLoop.step() → CycleRecord + features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DataNodes are infrastructure that drives per-item subgraph execution —
the optimizer should never accidentally mutate or remove them. Add
_auto_frozen_nodes() helper and apply it at both mutation call sites
in SwarmEngine (seed and evolve_generation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Ugj23EAAf1HoHcknrBwhL
…don't timeout

The per-item WorkflowExecutor in _execute_data() was created with empty
completed_files, causing _wait_for_reads to poll for 60s then halt if the
subgraph's start node had reads dependencies on upstream artifacts. Copying
the parent's completed_files into each item_executor lets the subgraph
inherit the parent's artifact state.

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

DataNode sub-executors timed out (60s) waiting for files that task.setup()
had already created on disk. _wait_for_reads only checked the
completed_files set, which tracks node outputs — not filesystem state.

Before spawning per-item sub-executors, scan all subgraph node reads and
add any that already exist on disk to completed_files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DataNode with task_ref resolved items and ran subgraphs but never called
task.verify(), silently scoring every completed subgraph as 1.0 regardless
of the task's actual evaluation. This made task-defined pass/fail invisible.

Changes:
- executor.py: Pair TaskInstances with DataItems so filters apply in lockstep.
  Move setup()/prompt() from eager pre-loop into run_item() for per-item
  fault isolation. Call task.verify() after each subgraph completes and use
  its score/passed/details instead of binary subgraph success.
- inner_loop.py: _step_with_data_node() now aggregates per-item verify
  scores (mean) from DataNode output instead of using binary exec success.
- inline_items and source_path paths remain unchanged (no Task, no verify).

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

_step_with_task() hand-built a CycleRecord without calling CycleAnalyzer,
so total_cost_usd was always 0.0. Now captures event_offset before the
instance loop and uses CycleAnalyzer afterward to aggregate costs from
events.jsonl — matching the pattern _step_subprocess() already uses.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	factory/inner_loop.py
- Fix 1: parallelism default 3→1, Field(ge=1) constraint, worktree isolation for parallelism>1
- Fix 2: write current_item.json for subgraph item visibility (cleaned up in finally)
- Fix 3: reject explicit DataNode→subgraph edges in validation
- Fix 4: raise on missing source_path, warn on empty data, isolate JSONL errors
- Fix 5: seeded shuffle via random.Random(seed), shuffle_seed field on DataNode
- Fix 6: direct DataNode score lookup by node ID + populate instance_results
- Fix 7: document spec deviations in PR body (applied via gh pr edit)
- Fix 8: compose() relaxes capability validation for DataNode workflows

executor.py exceeds 500-line gate (was already 1405, now 1507) — _execute_data
method is tightly coupled to executor internals, splitting would hurt readability.

Addresses review feedback from @lambdabaa on PR #1483.
Fixes compose() DataNode gap from issue #1488 Gap 0.
A gate could route only proceed/reloop/halt, so a graph whose gate selects
among alternative forward continuations (draft/improve/debug, mutate/merge)
had to spell one branch 'reloop' — which every consumer reads as a rewind.

An edge condition is now a free label matched case-insensitively; a
gate-named label becomes the edge's lowercase condition. The three verdict
labels are unchanged, so existing graphs and executors behave identically.
GateNode.max_iterations lets a gate carry its own reloop cap through the
graph, so a budget-gated loop is not bounded by the runtime default.
…ion (#1487)

reads and writes are sets, whose iteration order is not stable across
processes, so a given graph could serialize to different bytes on different
runs. Sorting them makes graph.json byte-reproducible — required for a wire
format that consumers diff, hash, and compare.

OptKnob gains a human-readable description so a downstream package can carry
prose about a knob without inventing a side channel.
VerdictType(str, Enum) instances ARE strings, so .value is redundant
and mypy flags it as attr-defined on the str union branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts str() back to .value (str() produces 'VerdictType.PROCEED'
instead of 'proceed'). Adds type:ignore to silence mypy since
VerdictType(str, Enum) makes mypy see the str branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lambdabaa and others added 6 commits September 10, 2026 14:45
* feat: first-class plugin workflow registration (closes #1490)

Plugins can now register workflows via PluginRegistry.add_workflow_search_path()
and have WorkflowRegistry.discover() consume them natively, replacing the
manual _search_paths bridging downstream consumers currently do.

Discovery now layers sources by priority: project > plugin > user > builtin.
Name collisions between sources are resolved by priority with a logged
warning instead of last-write-wins (which previously let plugin paths
silently override project-local workflows).

The skill cache checksum now hashes discovered workflow source files in
addition to the model dump, so editing a plugin workflow file invalidates
the exported SKILL.md cache even when the change is not semantic (e.g.
comments). _compute_checksum takes an optional source_files mapping;
existing callers are unaffected.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat: warn on plugin mode/workflow drift at discovery time

Review feedback on #1491: a plugin mode with no discovered workflow of the
same name usually means a typo or renamed file, and without a warning it
only surfaced as a silent fallback to the default improve loop. discover()
now logs a warning for declared-but-missing modes and an info event for
discovered-but-undeclared plugin workflows (legal: subgraph libraries,
composed packages). With no plugin modes declared there is nothing to drift
against, so the check is skipped.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
FIX 1 (BLOCKING): Format/path-kind mismatch raises ValueError
  - source_format='directory' on a file → ValueError
  - source_format='jsonl'/'csv' on a directory → ValueError

FIX 2 (BLOCKING): diversity_metric uses named structural axes
  - STRUCTURAL_AXES = (0, 1, 2, 3, 8) module-level constant
  - Excludes hash-bucket axes 4-7 (edge_sig, param, prompt, knob)
  - Correctly includes has_data_node (index 8) instead of edge_sig (index 4)

FIX 3: Deterministic shuffle seed via hashlib.sha256
  - Replaces hash() which is PYTHONHASHSEED-randomized

FIX 4: initial_context=item.prompt or None
  - Suppresses noisy initial_context_ignored warnings for empty prompts

FIX 5: Test coverage for _workflow_has_data_node, _step_with_data_node,
  and format/path-kind validation

Also includes uncommitted Edge.condition_label property from prior review round.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
model defaults to '' and provider to 'auto', so a graph that does not pin them
leaves the choice to the runtime. 'auto' resolves through the same non-vertex
alias table as 'anthropic', so existing consumers behave identically.
The numerator counted all 9-dimensional grid cells (including hash-bucket
distinctions) while the denominator used only the 5 structural axes.
Two individuals with identical structure but different edge-hash buckets
would produce a metric >1.0. Fix: count unique structural-axis cells
in the numerator to match the structural denominator.

Co-Authored-By: Claude Opus 4.6 (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.

9 participants