Skip to content

feat: add smoke test harness for core factory modes - #4

Draft
osilkin98 wants to merge 369 commits into
crqu:mainfrom
osilkin98:factory/run-971b4953
Draft

feat: add smoke test harness for core factory modes#4
osilkin98 wants to merge 369 commits into
crqu:mainfrom
osilkin98:factory/run-971b4953

Conversation

@osilkin98

Copy link
Copy Markdown

Closes akashgit#1343

Changes

  • New file tests/test_smoke_cli.py — 21 smoke tests covering all core factory modes:

    • factory detect: all 5 ProjectState values tested with exact assertions
    • factory discover: eval profile generation against tests/fixtures/hello-cli, validates EvalProfile model
    • factory study: observations file written and non-empty
    • design_workflow(): Tier 4 integration — patches invoke_agent at the subprocess boundary, runs the real WorkflowExecutor with FnNode/GateNode gates, asserts research-*.md and current.md artifact files appear
    • create_workflow(): same Tier 4 pattern
    • factory agent <role>: 8 kept roles (researcher, strategist, builder, health_checker, code_reviewer, adversarial_tester, archivist, ceo) — verifies prompt resolution and .factory/reviews/<role>-latest.md written
    • factory refactory: workspace setup creates .refactory/, .claude/settings.local.json, CLAUDE.md; session IDs persist and reset correctly
  • pyproject.toml: Registered smoke and e2e pytest markers

  • Performance: pytest -m smoke completes in ~4 seconds (target was <30s)

This is Phase 1 of the repo cleanup plan — the safety net that must exist before any code deletion in subsequent phases.

osilkin98 and others added 30 commits July 5, 2026 19:56
…ce persistence (akashgit#963, akashgit#964, akashgit#965) (akashgit#966)

* fix: add fallback handler for contributed workflow modes in CEO task builder

_build_ceo_task() had explicit elif blocks for build, discover, meta,
research, and create modes but no fallback for contributed/custom modes
like swebench or legacybench. Without instructions, the CEO would waste
time figuring out what to do instead of reading the mode's SKILL.md.

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

* fix: remove auto_merge FnNode from swebench workflow (akashgit#963)

Harbor's recovery chain already handles merging changes to main,
making the auto_merge node redundant. Pipeline is now 3 nodes
(study → builder → gate_verify) with RELOOP on test failure.

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

* feat: add terminal flag to Workflow model to prevent post-completion chaining (akashgit#964)

Workflows marked terminal=True cause _chain_modes() to return immediately
after the mode completes, preventing unwanted auto-chaining into discover/
improve modes. Sets terminal=True on the swebench workflow.

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

* fix: persist trace_id.txt to Harbor /logs/ so benchmark scripts can find it (akashgit#965)

Two bugs prevented trace_id extraction after benchmark runs:
1. trace_id.txt was written inside the container's project dir but never
   copied to /logs/ (the only path Harbor exports to JOBS_DIR)
2. Benchmark cleanup functions deleted JOBS_DIR before searching it for
   trace_id.txt, so even with the file present it would never be found

Fix: copy .factory/trace_id.txt to /logs/.factory/ in both Harbor agent
classes after factory ceo exits, and reorder cleanup() in all 5 benchmark
scripts to extract trace_id before rm -rf.

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

---------

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…hgit#968)

Without auto_merge, the factory makes the correct fix but it stays on
the worktree branch — which the factory deletes on exit. Harbor's
recovery chain can't find the changes, so the benchmark fails despite
a correct solution.

Uses `git branch -f $BASE HEAD` instead of `git checkout && git merge`
to update main's ref without checking it out — works inside worktrees
where checkout would fail.

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-gate

# Conflicts:
#	factory/workflow/definitions.py
#	tests/test_workflow_definitions.py
#	tests/test_workflow_qa.py
The old monolithic qa node (writing qa-latest.md) was replaced by the
deep-QA pipeline. Update gate_doc_freshness to read adversarial-qa.md
from the adversarial_tester node, which is the last step in the
deep-QA chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd doc-update workflows

- Extract the duplicated gate_prompt string from 4 workflow definitions
  into a module-level DOC_FRESHNESS_GATE_PROMPT constant
- Add doc_generate_workflow(): scan_project -> gate -> generate_docs ->
  gate -> validate_docs -> gate (standalone, no trigger)
- Add doc_update_workflow(): diff_scope -> patch_docs -> gate ->
  revalidate -> gate (standalone, no trigger)
- Register both as 'doc-generate' and 'doc-update' in register_all()
- Add comprehensive tests for both workflows and constant usage

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

- Use `factory workflow run swebench .` instead of `factory ceo --mode swebench`
  (CEO paraphrases shell commands; executor runs them verbatim)
- Fix auto_merge: `git update-ref` + `git-common-dir` for correct branch detection
  + copy changed files to parent working tree
- Fix trace_id: copy to /logs/agent/ (Harbor-exported path), search by -name

Verified: 5/5 local Harbor runs pass with trace_id captured.

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ess-gate

feat: add gate_doc_freshness GateNode to 5 factory workflows
Change tension from 0.2 to 0 in all Chart.js dataset configs so charts
render linear segments instead of bezier curves.

Closes akashgit#971

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inject trace summary, analysis, and Langfuse URL into results.jsonl
during CI, and render them in the benchmarks dashboard with expandable
detail rows and clickable trace links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- HTML-escape trace_summary in renderPerBenchmarkTable() before injecting
  as innerHTML, consistent with trace_analysis escaping
- Use with-statement for file reads in benchmark.yml to ensure proper
  file handle cleanup

Closes akashgit#973

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use straight line segments in benchmark charts
Change tension from 0.2 to 0 in all Chart.js dataset configs so charts
render linear segments instead of bezier curves.

Closes akashgit#971

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

FactoryCeo was hardcoded to run the swebench workflow for all benchmarks.
Extract _get_factory_command() as an override point, restore generic
factory ceo for featurebench/terminalbench, and add SwebenchFactoryCeo
and LegacybenchFactoryCeo subclasses with their respective workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: display trace analysis on benchmarks dashboard
Seed .factory/ with minimal config.json and eval_profile.json so state
detection yields has_factory → improve mode, then use --focus with the
instruction text instead of --mode build --prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The seeded eval_profile.json used "reviewed" but the EvalProfile model
and state detection check "human_reviewed". This caused state detection
to return EVALS_PENDING_REVIEW instead of HAS_FACTORY, breaking --focus
for featurebench/terminalbench runs.

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

fix: split FactoryCeo into benchmark-specific agent classes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…k scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Harbor containers installed factory from main, so new workflows
(terminalbench, featurebench) weren't available until merged.
Now passes FACTORY_GIT_REF=${{ github.sha }} through the benchmark
workflow → run scripts → Docker container, and the install step
uses it to pin the factory version to the commit being tested.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
${REF} was inside single quotes so the shell didn't expand it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_parse_fn_verdict checked for "fail" anywhere in the output text,
which matched "no failure signals detected" and halted the workflow
even on a pass verdict. Now checks the first line's prefix instead:
"pass:" → proceed, "fail:"/"revert:" → halt, "reloop:" → reloop.

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

feat: add terminalbench and featurebench deterministic workflows
…kflow dependency

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

fix: rewrite legacybench as lean pipeline — remove broken improve_workflow dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-split

feat: add programbench discovery-first workflow
…git#982)

Without this, Claude Code uses its default max_turns which can be
as low as 3 in fresh CI installs, causing agents to be killed before
finishing their work.

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
osilkin98 and others added 30 commits August 13, 2026 14:45
* feat: integrate DevOps Gym as contributed benchmark (akashgit#1152)

Add DevOps Gym build/configuration benchmark following the proven
contributed benchmark pattern (legacybench, swebench, etc.).

- 4-node pipeline: study -> solver -> gate_verify -> auto_merge
- Study node detects build systems (Maven, Gradle, Go, Make, Docker)
- Gate verify attempts build with detected build tool
- Harbor agent class DevOpsGymFactoryCeo runs workflow deterministically
- Registered in definitions.py, config.sh, run.sh, benchmark.yml
- 26 structural tests covering graph, trigger, registration, and meta

* fix: update workflow count assertion for devopsgym

The devopsgym contributed workflow adds a 34th workflow to the registry.
Update test_register_all_count assertion from 33 to 34.

---------

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
feat: add CLI plugin architecture via entry points
…ssertion

Two bugs prevented plugin modes from working:

1. _validate_ceo_flags checked mode against the static CEO_MODES list
   instead of get_all_ceo_modes(), rejecting plugin-registered modes
   (e.g. attack, buildroot) as "unknown mode".

2. cmd_ceo asserted raw_path is not None before running pre-hooks.
   Plugin modes that create their work directory via add_ceo_pre_hook
   crashed on the assert because no positional path was provided —
   the pre-hook that would supply it never got to run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mypy caught that raw_path can be None when pre-hooks are registered.
Add a truthiness check before passing it to Path() in the --refine
validation branch.

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

fix: plugin modes rejected and pre-hooks crash on missing path
…ktrees (akashgit#1235)

* fix: replace directory symlink with selective symlinks in CEO run worktrees

Closes akashgit#1234

The single .factory/ directory symlink caused context contamination
(stale strategy/review files from prior cycles) and race conditions
(concurrent CEO runs writing to the same mutable state).

Replace with a real .factory/ directory that selectively symlinks
shared append-only state (config, results.tsv, experiments, etc.)
and creates fresh per-cycle directories (strategy, reviews, state).
Backlog.md is copied in at creation and synced back at teardown.

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

* fix: update worktree tests to assert selective symlink layout

Tests were still checking for the old directory-symlink behavior where
.factory was a single symlink. Updated to assert the new layout: .factory
is a real directory with selective symlinks inside (config.json) and
per-cycle real directories (strategy/).

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

* fix: add adversarial_state.json and performance_report.json to shared symlinks

Both files are cross-cycle project-wide state that was shared via the old
directory symlink but missing from _SHARED_SYMLINK_ENTRIES. Without them,
adversarial convergence tracking resets each cycle and ACE loses qualitative
signals from performance reports in worktrees.

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

---------

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

The slow update was injecting guidance into self.current_skill
(rendered SKILL.md) but never into the YAML annotations prompt slots.
Since the adapter serializes YAML for Harbor, the guidance never
reached the agent inside the container.

Now injects into the primary prompt slot (largest prompt slot) and
writes YAML annotations, so the guidance flows through:
prompt_slot → YAML → FACTORY_WORKFLOW_YAML_B64 → container → agent.

Also uses _serialize_yaml() for the prev/curr rollouts so the
comparison runs use the correct mechanism.

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add study mode with graph-powered code exploration

Add a standalone Study Mode workflow and integrate graph exploration
into the design workflow's study subgraph.

Study subgraph (shared by design and study workflows):
  graph_update → study → graph_explorer → concat_study

- graph_update: runs `factory graph update` to build/refresh graph.json
- study: runs `factory study` producing observations.md
- graph_explorer: researcher agent that reads observations, queries
  the code graph via `factory graph query/explain/path`, and writes
  graph-context.md with structural findings
- concat_study: merges observations.md + graph-context.md into
  study-combined.md for downstream consumers

Design workflow integration:
- Existing projects route through the study subgraph before research
- Researchers and strategist read study-combined.md for project context
- join_research is now a pure sync barrier (no concatenation)
- gate_research and strategist read individual research files directly

CLI additions:
- `factory graph query/explain/path` — agent-accessible graph tools
- `factory ceo --mode study` / `factory run --mode study`
- Study mode registered across all integration points

Also updates researcher agent prompt to read study-combined.md instead
of re-running factory study redundantly when study has already run
upstream in the workflow.

* fix: update plan workflow and registry count tests

Plan workflow inherits the study subgraph from design_workflow, so
node count goes from 15 to 18 and edge list includes the subgraph
edges. Registry count goes from 34 to 35 with the new study workflow.
Plugin modes registered via add_modes() failed Pydantic validation
when headless mode created a CycleState, since the Literal type only
accepted hardcoded built-in mode names.

Closes akashgit#1262

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin modes registered via add_modes() failed Pydantic validation
when headless mode created a CycleState, since the Literal type only
accepted hardcoded built-in mode names.

Closes akashgit#1262

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

The hardcoded choices list in argparse rejected plugin-registered roles
before the agent runner could execute. Move validation to cmd_agent()
where it checks both BUILTIN_AGENT_ROLES and plugin-registered roles
via get_registry().agent_roles. Add add_agent_roles() to PluginRegistry
following the same collision-guard pattern as add_modes().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After the existing symlink/copy logic and per-cycle fresh dirs, iterate
over remaining subdirectories in source .factory/ and copy any not
already handled. Skips entries already present (symlinked, copied, or
created fresh) to avoid duplication.

Closes akashgit#1263

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

Closes akashgit#1264

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

fix: plugin integration — 4 bugs blocking plugin modes and agents
…egist plan reading

Fixes akashgit#1204, akashgit#1205, akashgit#1206.

Converts the sequential deep-QA pipeline (health_checker → code_reviewer →
gate_review → adversarial_tester) to parallel execution via fork/join:

  fork_qa → [health_checker, code_reviewer, adversarial_tester] → join_qa

Key changes:

1. Worktree anchoring (akashgit#1204): All 3 QA agent prompts now include a
   "Working Directory Constraint" preventing agents from navigating to
   parent directories or other worktrees.

2. Strategist plan reading (akashgit#1205): adversarial_tester.md now has an
   explicit Step 0 that reads .factory/strategy/current.md to derive
   testing scope from hypothesis deliverables.

3. Parallel execution (akashgit#1206): _deep_qa_subgraph() now returns fork/join
   nodes instead of sequential edges. All workflows (build, improve,
   research, refine, create, deep-qa, parallel-improve) updated.

4. Graph validator: added implicit edges for ForkNode targets and
   JoinNode sources in reachability analysis.

5. parallel-improve: fixed SubgraphForkNode renaming to also rename
   ForkNode.targets and JoinNode.sources when namespacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_gate_qa_topology: check for fork_qa instead of sequential
  health_checker/code_reviewer/gate_review/adversarial_tester nodes
- test_e2e_gate_before_improve: check builder < fork_qa in topo order
  instead of builder < health_checker (fork targets have no ordering
  guarantee relative to other nodes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes 3 issues from factory CEO review:

1. _topological_sort (skill_export.py) now adds implicit edges for
   ForkNode targets and JoinNode sources, so fork children sort after
   the fork node. This fixes loop context topology rendering and skill
   export ordering.

2. Removed stale sequential prerequisites from all 3 QA prompts:
   - adversarial_tester.md: removed "health check must have passed"
     and "code review must have found no critical issues"
   - code_reviewer.md: removed "health check must have passed"
   - health_checker.md: removed "proceed to code review" gate language
   All now state they run in parallel with the other agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: parallelize deep-QA pipeline + worktree anchoring + strategist plan reading
… review file

When multiple AgentNodes share the same role, the generic
reviews/{role}-latest.md file from the first node caused all subsequent
same-role nodes to be auto-skipped. Fix by promoting the declared writes
check above the generic review file check, and returning None after the
writes check to prevent fallthrough.

Closes akashgit#1277

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

fix: widen CycleState.mode from Literal to str for plugin modes
fix: tool engine auto-skips agent nodes when multiple nodes share the same role
…#1279) (akashgit#1283)

Replace check-then-act (_session_exists → branch) with EAFP pattern:
try new-session first, fall back to new-window on non-zero returncode.
When parallel agents race on session creation, only one new-session
wins; the others now fall back to new-window instead of failing.

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

* feat: add glaude runner (Claude Code via GLM-5.2 LiteLLM proxy)

Closes akashgit#1229

- Create factory/runners/glaude.py mirroring ClaudeRunner with binary
  'glaude', empty required_env_vars (auth baked into wrapper), and full
  capability flags (telemetry, background, session naming, model override)
- Register GlaudeRunner in factory/runners/__init__.py
- Add FACTORY_GLAUDE_DRY_RUN support for testing without tokens
- Add 41 tests covering runner selection, dry-run, metadata, command
  building, headless invocation, interactive mode, and temp file cleanup
- Document Glaude specifics in CLAUDE.md runner section

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

* Revert "feat: add glaude runner (Claude Code via GLM-5.2 LiteLLM proxy)"

This reverts commit 16fd72a.

* feat: add env overlay support to config.toml credential profiles

Profiles now override existing env vars (not setdefault), support
unsetting vars via [credentials.*.unset].vars list, and reject
protected system vars (PATH, HOME, etc.). Enables any endpoint
variant (e.g. glaude/LiteLLM proxy) via config alone — no new
runner class needed.

Closes akashgit#1229

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

* refactor: replace glaude-specific references with generic custom endpoint examples

Rename all glaude/GLM-5.2 references to generic placeholder names
(litellm-proxy, custom) so the env overlay feature is presented as a
general-purpose mechanism for configuring custom model endpoints via
profiles, not tied to any specific deployment.

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

* docs: update load_config docstring to reflect override semantics

The docstring still described the old setdefault behavior. Updated to
document that profiles override existing env vars, support unsetting
vars, and protect PATH/HOME/etc.

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

* harden config.toml credential profiles: expand protected vars, validate unset types, add override warnings

- Expand _PROTECTED_VARS to block code execution vectors (LD_PRELOAD,
  DYLD_INSERT_LIBRARIES), language path injection (PYTHONPATH, GOPATH,
  CLASSPATH, NODE_PATH), shell parsing (IFS), and factory internals
  (FACTORY_TRACE_ID, FACTORY_PARENT_SPAN_ID)
- Validate unset.vars is a list, raising ValueError on string/other types
- Log structured warning when profile overrides existing env var (no values logged)
- Mask sensitive values in nested sub-table rendering in show_config
- Check config file permissions and warn if group/other readable
- Add 8 new tests covering all hardening behaviors
- Document expanded protections in CLAUDE.md

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Phase 1 — wire InnerLoop into SwarmEngine with partial credit scoring

Cherry-pick v1 outer_loop foundation from feat/outer-loop-phase1-foundation,
restructure for InnerLoop-based evaluation. Key changes:

- Add Workflow.to_dict()/from_dict() serialization methods
- Create FeatureBenchEvaluator with partial credit scoring (pytest-json-report)
- Create FeatureBenchInnerLoop wrapping InnerLoop.step() for CycleRecord exhaust
- Add CycleRecordCache for content-addressable eval caching
- Modify SwarmEvaluator to support both legacy EvaluatorFn and InnerLoop
- Update DirectFeatureBenchEvaluator for partial credit (float) scoring
- Add parsimony pressure: fitness = score - 0.01 * num_nodes

182 tests passing (excluding CLI tests pending Phase 4).

* feat: Phase 2 — ephemeral mode registry for candidates-as-modes

Create EphemeralModeRegistry with register/cleanup/promote lifecycle:
- Content-addressable storage with hash verification
- evolve-gen{N}-{id[:8]} naming — never collides with main registry
- Context manager protocol for guaranteed cleanup
- Wire into SwarmEngine.seed() and evolve_generation()
- 11 tests passing

* feat: Phase 3 — contrastive reflection agent with exhaust analysis

Create OuterLoopReflector with two-stage contrastive reflection:
- Compare top-K vs bottom-K CycleRecords to find structural differences
- Extract failure/success patterns from agent steps and experiment results
- Generate informed mutation suggestions based on role and topology diffs
- Save reflection reports to .factory/outer_loop/reflections/
- Wire into SwarmEngine between evaluate and evolve phases
- Add MAX_NODES=30 bloat prevention in mutation operators
- Create reflector.md and evolver.md agent prompts
- Create reflect.md prompt template
- 7 reflector tests + 200 total passing

* feat: Phase 4 — outer-loop as registered mode with CLI subcommands

- Create workflow graph: seed → evaluate → reflect → evolve → gate_converge
- Add CLI subcommands: calibrate, evaluate, reflect, evolve, status, promote
- Register outer-loop in CEO_MODES and main CLI dispatch
- Add outer-loop to Self-Evolution command group
- 17 CLI tests + 217 total passing

* feat: Phase 5 — convergence detection with cost logging and diversity monitoring

- Add configurable convergence criteria to SwarmConfig: plateau_window,
  plateau_threshold, diversity_floor, early_stop_unchanged
- Implement 3 convergence detectors: fitness plateau (< 1% improvement
  over N generations), diversity collapse (< 20% of initial), early stop
  (top-3 unchanged for N generations)
- Add event logging to .factory/outer_loop/events.jsonl (diversity, fitness)
- Add cost logging to .factory/outer_loop/costs.jsonl (per individual)
- BudgetTracker is logging/tracking only — never limits execution
- 217 tests passing

* fix: lint — remove unused imports and fix f-string

* fix: wire reflection→mutations, implement CLI stubs, add PROMPT_MUTATE, cleanup modes

6 fixes from deep QA pipeline review:

1. Wire reflection to mutations (CRITICAL): pass ReflectionReport from
   engine.py to apply_random_mutation; 70% guided / 30% random selection
   via WeightedRandomStrategy.select_guided_operator()

2. Implement 3 stub CLI subcommands: evaluate, reflect, evolve now call
   real SwarmEngine methods (load config, evaluate population, run
   reflection, produce offspring)

3. Add PROMPT_MUTATE operator: 7th mutation type that modifies AgentNode
   prompt_template with strategy variants or reflection-derived hints

4. Wire cleanup_generation(): called after offspring evaluation in
   evolve_generation() to remove non-surviving ephemeral mode files

5. Fix lint + type errors: Sequence instead of list for covariant type
   hints in reflector.py; type-safe dict access in direct_evaluator.py;
   7 unused imports removed via ruff --fix

6. Add contributed workflow compliance: README.md and test_workflow.py
   for factory/workflow/contributed/outer_loop/

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

* fix: wire population seeding into outer-loop calibrate command

The calibrate command only initialized the filesystem and wrote config
but never created the initial population of workflow variants. This
caused 'factory outer-loop evaluate' to fail with 'no ephemeral modes
found' because no modes were registered.

Now calibrate loads the base FeatureBench workflow, creates a
SwarmEngine with EphemeralModeRegistry, calls engine.seed() to
populate slots (unmodified seed + mutations + designer variants),
saves the population to checkpoint, and prints the created modes.

Closes akashgit#1282

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

* fix: register designer variants as ephemeral modes and wire inner_loop_factory in CLI

Bug 1: _add_designer_variants() added individuals to the population but
never called mode_registry.register(), so only the base seed appeared as
an ephemeral mode. Now registers each designer individual identically to
the seed and mutation slots.

Bug 2: All CLI commands (calibrate, evaluate, reflect) created
SwarmEvaluator without inner_loop_factory, leaving it None. This caused
evaluation to skip InnerLoop.step() entirely and return dummy score=0.0.
Added _make_inner_loop_factory() helper that bridges the registry into
SwarmEvaluator so FeatureBenchInnerLoop is actually invoked.

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

* fix: write workflow wrappers so ephemeral modes are discoverable by sub-CEO

EphemeralModeRegistry.register() now writes a thin .py wrapper to
.factory/workflows/{mode_name}.py alongside the JSON in outer_loop/modes/.
This makes ephemeral modes discoverable by WorkflowRegistry.discover()
when a sub-CEO process runs 'factory ceo --mode <name>'.

cleanup_generation() and cleanup_all() remove both the JSON and wrapper.

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

* fix: export meta and workflow from outer_loop contributed package

The __init__.py was empty, causing ImportError in test_workflow.py
which imports from factory.workflow.contributed.outer_loop directly.
Follows the same pattern as other contributed workflows (featurebench).

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

* fix: add explicit graph detection and project path to Researcher graph commands

The Researcher agent's graph_explorer prompt had two bugs:
1. Ambiguous detection: "if graph.json exists" without specifying WHERE
   or HOW to check, causing the agent to report "no graph.json" even
   when graph.json existed at the project root
2. Missing path argument: `factory graph query "<q>" --depth 2` omitted
   the required `<path>` positional arg (CLI expects `factory graph query
   <path> <question> --depth <n>`)

Fix: Add explicit `factory graph status .` detection step and include `.`
(CWD = project root, set by the agent runner) as the path argument in all
graph CLI commands. Also fix the tracked `skills/study/SKILL.md` skill.

Closes akashgit#1256

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

* fix: resolve 4 outer loop E2E validation issues

Issue 1 — Set designer_count=0 for featurebench and use a 1-node
builder-only seed workflow instead of the full 4-node pipeline.
Lowered NoveltyFilter min_edit_distance for small seeds so mutations
produce novel variants.

Issue 2 — Added offset-based artifact isolation to CycleAnalyzer and
InnerLoop so each sub-CEO's events/results are scoped by snapshot
offsets, preventing cross-contamination of experiment data.

Issue 3 — Cost attribution automatically scoped by the offset approach
from Issue 2.

Issue 4 — Added --project-dir override to calibrate and evaluate CLI
subcommands for benchmark instance isolation.

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

* fix: make Researcher graph explorer use explicit paths and smoke check

The graph_explorer prompt told the Researcher to run `factory graph status .`
but Sonnet often skipped that step and checked for graph.json in the wrong
location (e.g. .factory/ instead of the project root). Three changes:

1. Add a `test -f graph.json` smoke check with explicit GRAPH_EXISTS/NO_GRAPH
   output so the agent has a concrete, unambiguous detection step
2. Clarify that graph.json lives at the PROJECT ROOT, NOT inside .factory/
3. Replace `.` with `"$(pwd)"` in all graph CLI commands for explicit path
   resolution, matching the pattern already used in skills/study/SKILL.md

Closes akashgit#1256

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

* fix: run sub-CEO in headless mode for outer loop evaluate

InnerLoop.step() subprocess was missing --headless, causing the sub-CEO
to take the interactive path (runner.interactive_run) which doesn't
reliably execute workflows as a nested subprocess. Adding --headless
routes through run_ceo_with_completion_guard → invoke_agent for proper
one-shot agent execution and event emission.

CycleAnalyzer offset mechanism verified correct — the 0 steps/cost was
a consequence of sub-CEOs not running, not an offset bug.

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

* fix: use ls+pwd smoke check for graph.json detection in Researcher prompt

Replace `test -f graph.json && echo GRAPH_EXISTS` with explicit `pwd` + `ls -la`
in a fenced code block. This gives the agent concrete, visible output to reason
about rather than a boolean flag — easier for Sonnet to follow reliably.

Closes akashgit#1256

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

* fix: InnerLoop writes cycle_summary.json for reliable outer loop scoring

The CycleAnalyzer approach of parsing shared .factory/ artifacts is
broken for the outer loop because the sub-CEO writes to the same
.factory/ and doesn't produce experiment records. Instead, InnerLoop
now writes a structured cycle_summary.json after each step() with
observable outcomes (agents spawned/succeeded/failed, builder commits,
subprocess exit code) and a 0.0-1.0 score. The evaluator reads this
file as the primary score source, falling back to CycleRecord.

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

* fix: inject resolved project path into Researcher graph explorer prompt

The Researcher agent fell back to grep despite graph.json being present
because `$(pwd)` in the prompt was ambiguous — the agent had to run a
shell command to discover the path, and Sonnet frequently reported "no
graph.json" without actually running the check.

Three changes fix this:
1. `_GRAPH_EXPLORER_PROMPT` now uses `{project_path}` template variable
   instead of `$(pwd)`, so the actual path is baked into the prompt text
2. `executor._run_agent()` substitutes `{project_path}` with the
   resolved project path before passing to the agent
3. `skill_export._agent_node_to_md()` maps `{project_path}` to
   `$PROJECT_PATH` for the interactive SKILL.md path

Closes akashgit#1256

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

* fix: mirror ephemeral mode wrappers to target project dir for sub-CEO resolution

When `factory outer-loop evaluate --project-dir` targets a different project
(e.g. a FeatureBench instance), the sub-CEO couldn't resolve ephemeral modes
because wrappers were only written to the outer loop project's .factory/.
EphemeralModeRegistry now accepts a target_dir parameter and mirrors mode JSON
and workflow wrapper files to the target directory so the sub-CEO finds them.

Closes akashgit#1256

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

* fix: add target_project to SwarmConfig so sub-CEOs evaluate on FeatureBench instance

- Add target_project field to SwarmConfig (defaults to empty string)
- _cmd_calibrate persists --project-dir into config.target_project
- _cmd_evaluate falls back to config.target_project when --project-dir not passed
- Add tests for CLI parsing, model round-trip, and evaluate fallback behavior

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

* fix: remove nested triple backticks from graph_explorer prompt

The _GRAPH_EXPLORER_PROMPT contained fenced code blocks (```) for the
smoke check example. When skill_export embedded this prompt inside a
bash code fence in SKILL.md, the nested ``` prematurely closed the
outer code block. The CEO then saw a truncated command and passed an
incomplete task to the Researcher, which never received the graph
detection instructions and always fell back to grep.

Replace the fenced code block with inline code formatting so the
prompt can safely nest inside SKILL.md bash blocks.

Closes akashgit#1256

* fix: use pytest pass rate for scoring and worktree isolation per candidate

Issue 1: InnerLoop._write_cycle_summary now runs the configured
test_command after the sub-CEO completes and uses the pytest pass
rate (passed/total) as the primary score. The old heuristic is
kept as metadata (heuristic_score) but no longer drives scoring.

Issue 2: SwarmEvaluator._evaluate_via_inner_loop creates an isolated
git worktree per candidate so each evaluation starts from a clean
instance state. Worktrees are cleaned up after scoring. evaluate_batch
supports parallel evaluation via ThreadPoolExecutor.

Changes:
- SwarmConfig: add test_command field
- InnerLoop: add test_command param, _run_test_command method
- FeatureBenchInnerLoop: forward test_command to InnerLoop
- SwarmEvaluator: worktree create/cleanup, parallel evaluate_batch
- CLI: --test-command arg on calibrate subcommand

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

* fix: outer loop CLI bugs — reflect target, generation filter, eval dedup

Bug 1: _cmd_reflect now uses config.target_project instead of
project_path when evaluating workflows, matching _cmd_evaluate behavior.

Bug 2: _cmd_evaluate filters modes to the specified generation prefix
(evolve-gen{N}-*) and excludes eval copies (evolve-gen{N}-eval-*).

Bug 3: _make_inner_loop_factory checks if an eval mode already exists
before registering, preventing duplicates across CLI re-invocations.

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

* fix: register outer-loop workflow in builtin registry

The outer-loop contributed workflow was not imported in
definitions.py's _get_builtin_registry(), so the WorkflowRegistry
never discovered it. This caused SKILL.md not found errors when
creating worktrees (the skill cache only includes registered
workflows). Adding the import makes outer-loop a first-class
builtin, fixing both the naming mismatch and the skill cache issue.

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

* feat: add 20 edge-case tests and document outer loop subsystem in CLAUDE.md

H2: Add 8 test coverage gaps (20 tests in test_coverage_gaps.py) covering
graph fallback propagation, worktree cleanup with locked files, eval dedup
logic, disk-full worktree creation, budget exhaustion partial results,
all-identical convergence detection, reflector empty history, and mode
registry hash collisions. Total outer loop tests: 262 (up from 242).

H3: Update CLAUDE.md with Layer 2b (outer loop architecture, key modules,
MAP-Elites pipeline, single-builder E2E finding), outer loop models
documentation, CLI subcommands (calibrate/evaluate/reflect/evolve/status/
promote), and .factory/outer_loop/ directory layout.

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

* fix: heuristic double-count, outer_loop dir naming, CLAUDE.md paths

- I1: Replace duplicate returncode==0 signal with experiments>0 in
  heuristic scoring (inner_loop.py) — each signal now contributes
  exactly 0.2 with no double-counting
- I2: Standardize .factory/outer-loop/ → .factory/outer_loop/ in
  filesystem.py to match engine.py/cli — fixes status command missing
  trajectory.jsonl
- I2b: Fix CLAUDE.md checkpoint.json → state.json to match actual
  filename
- I4: Add TestHeuristicScoreWeights (7 tests) verifying each signal
  weight and no double-counting

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

* feat: add cache persistence, stale mode pruning, and disk space pre-check for outer loop

Implements H1 (operational hardening) from the strategy:

1. CycleRecordCache now persists to .factory/outer_loop/eval_cache.jsonl
   (append-only JSONL with dedup). SwarmEvaluator loads on init and
   exposes checkpoint_cache() for generation-level saves.

2. EphemeralModeRegistry.prune_stale_modes(older_than_hours=24) removes
   old mode JSONs and workflow wrappers to prevent unbounded accumulation.

3. _check_disk_space() in the CLI requires population_size*0.2+10 GB free
   before calibrate/evolve, failing fast with a clear message.

14 new tests cover all three features.

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

* fix: outer loop convergence gate, structural hash dedup, best_score tracking

- Add convergence gate to outer-loop SKILL.md: gate_converge now has explicit
  PROCEED → promote and RELOOP → evaluate paths so the CEO loops generations
  instead of falling into default improve mode
- Fix skill_export _gate_to_checkpoint to show RELOOP (not HALT) when RELOOP
  edges exist without HALT edges
- Remove workflow.name from structural_hash so structurally identical workflows
  with different names share the same fitness cache entry
- Update _cmd_evaluate to track best_score across generations in state.json

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

* fix: stop eval-copy mode accumulation and redundant re-evaluation in outer loop

_make_inner_loop_factory now looks up existing modes by structural hash
instead of creating new eval-copy modes on every evaluation call. _cmd_evaluate
persists cycle_summary.json for each mode, and _cmd_reflect reads those cached
results instead of re-evaluating all candidates sequentially.

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

* chore: update uv.lock after outer loop bug fixes

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

* fix: mirror ephemeral modes to target project in calibrate and evolve commands

Both _cmd_calibrate and _cmd_evolve created EphemeralModeRegistry
without target_dir, so gen0 and gen2+ modes were never mirrored to
the FeatureBench instance. Sub-CEOs on the target couldn't resolve
these modes, scoring 0.0. Now all CLI commands that register modes
pass config.target_project to the registry.

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

* fix: use CWD-relative path in graph_explorer smoke check

The Researcher agent's graph_explorer prompt used {project_path}/graph.json
for the smoke check, which becomes $PROJECT_PATH/graph.json in the SKILL.md.
Since $PROJECT_PATH is not set as an env variable in the Researcher's shell,
the check always fails and the agent falls back to grep-based exploration
even when graph.json exists at the project root.

Changed the smoke check to use `test -f graph.json` (relative path) since
the agent's CWD is already set to the project root via --project. Factory
graph CLI commands retain {project_path} since they're resolved by the CEO.

Closes akashgit#1256

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

* fix: propagate InnerLoop test results through evaluator to cycle_summary

The evaluator read only the score from InnerLoop's cycle_summary but
discarded scoring_method and test_details. The CLI then wrote its own
summary without test information, so cycle_summary.json always showed
heuristic scoring even when pytest pass rate was computed.

Now _read_cycle_summary returns the full dict and the evaluator includes
scoring_method + test_details in EvalResult.details, which the CLI
persists to the main project's cycle_summary.json.

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

* docs: add outer loop guide and link from README

New docs/outer-loop.md covers the full outer loop architecture:
two-CEO model, pipeline (calibrate→evaluate→reflect→evolve→gate),
scoring (pytest pass rate + parsimony), worktree isolation, ephemeral
mode lifecycle, CLI reference, module map, and E2E findings.

Linked from the README/index.md under "Other Workflows".

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mpt (akashgit#1294)

The full assembled CEO prompt (33KB base + 13-31KB SKILL.md) exceeded Claude
Code's ~40KB CLAUDE.md character limit in every workflow mode. Split the
prompt delivery: CLAUDE.md now receives a slim ~2KB resume-resilient identity
(Sacred Rules, agent dispatch syntax, review verdicts, mode pointer), while
the full prompt continues via --append-system-prompt-file unchanged.

Changes:
- factory/models.py: Add prompt_core field to AgentRunRequest
- factory/agents/runner.py: Add resolve_prompt_core() returning slim identity
- factory/runners/claude.py: Write prompt_core to CLAUDE.md (fallback to full
  prompt for backward compat), backup/restore existing CLAUDE.md
- factory/cli/_ceo_helpers.py: Pass prompt_core to interactive AgentRunRequest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ite loop (akashgit#1298)

* fix: update test helper to write declared artifacts, preventing infinite loop

The _simulate_reloop_cycle() helper in TestLoopContextE2EComparison was
writing generic {role}-latest.md files for all AgentNodes. After
f33b35b reordered _detect_artifact to check node.writes before the
generic reviews/ path, nodes with explicit writes (e.g. health_checker
writing .factory/reviews/health-check.md) never had their declared
artifacts created, so tool_next's artifact detection loop spun forever.

Fix: when a node declares writes, write those exact paths instead of
the generic fallback. Nodes without writes still get the old behavior.

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

* fix: update stale hardcoded count assertions to match current state

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

* fix: accept all valid convergence reasons in test_run_terminates_on_budget

The engine's convergence behavior is nondeterministic — it can terminate
with any reason from _get_convergence_reason(), not just the three
previously asserted. This caused flaky test failures when the engine
happened to hit diversity_collapse or another valid reason.

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

---------

Co-authored-by: Oleg Silkin <97077423+RobotSail@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: write slim CEO identity to .claude/CLAUDE.md instead of full prompt
… docs/index.md (akashgit#1303)

The old README.md (374 lines) had richer content than docs/index.md after
PR akashgit#1100 turned README.md into a symlink. This restores the missing sections
(Design Mode spec file tips, LangFuse tracing, Plugin install, Plugin Agents,
Verified Skill Generation) while preserving all new content added since
(mermaid diagrams, Other Workflows, Outer Loop, multi-issue focus examples).

- Convert doc links from root-relative (docs/foo.md) to docs-relative (foo.md)
- Use absolute GitHub URL for LICENSE badge and infra/langfuse cross-ref
- Use absolute URL for logo image
- Update 'uv run factory' commands to 'factory'

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the repo cleanup plan — build a safety net before any
deletion.  Creates tests/test_smoke_cli.py (21 tests, <5 s) covering:

- factory detect: all 5 ProjectState values
- factory discover: eval profile generation against hello-cli fixture
- factory study: observations.md written and non-empty
- design_workflow: Tier 4 integration with mocked invoke_agent,
  real WorkflowExecutor, FnNode gates, and artifact file assertions
- create_workflow: same pattern
- factory agent <role>: 8 kept roles — prompt resolution + review file
- factory refactory: workspace setup + session ID persistence

Registers 'smoke' and 'e2e' pytest markers in pyproject.toml.

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.

Build smoke/integration test harness for core modes