agentleFS
akashgitakashgit/remote-factory/CLAUDE.md

remote-factory

CLAUDE.md74 starsChanged 38 days agoMITView on GitHub

Tests use pytest-asyncio with asyncio_mode = "auto" — async test functions run without @pytest.mark.asyncio. Shared fixtures (tmp_project, sample_config, python_project) live in tests/conftest.py. An autouse _isolate_registry fixture redirects the global registry to a temp directory during tests. Version is derived from git tags via hatch-vcs at build time — no static version = in pyproject.toml. The factory is a four-layer system: Pure tools that don't make decisions. Entry point is factory/cli.py → factory.cli:main (registered as factory script in pyproject.toml). Each subcommand…

  • Reads credentials

1 other project uses a version of this.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 1

    1. boston-harness-group/remote-factory★ 74100% same

    Edited · 0

    None found.

    Wrote their own version · 0

    None found.

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository8 other files it gives its agents.

The file

We split this CLAUDE.md into the rules it gives an agent

6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All Pydantic models use ConfigDict(strict=True, extra="forbid")

    line 36
  2. Very specific:

    After pulling new tags, re-run uv sync for editable installs to pick up the new version

    line 47
  3. Very specific:

    Credential profiles: Use --profile <name> with factory ceo, factory run, or factory agent to load a [credentials.<name>] section.

    line 185
  4. Very specific:

    Unset: Add a [credentials.<name>.unset] sub-table with vars = ["VAR1", "VAR2"] to remove env vars before injection.

    line 189
  5. Very specific:

    Override warnings: When a profile overrides an existing env var with a different value, a log.warning("profile_override", key=k, …

    line 192
  6. Very specific:

    Events: All agent invocations and cycle transitions are logged to .factory/events.jsonl as append-only structured events.

    line 369

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build & Run

uv sync                          # Install all deps (including dev group)
factory --help                   # Verify CLI entry point

Test

pytest -v                        # Full suite
pytest tests/test_models.py -v   # Single file
pytest -k "test_detect" -v       # By name pattern
pytest --cov                     # With coverage

Tests use pytest-asyncio with asyncio_mode = "auto" — async test functions run without @pytest.mark.asyncio. Shared fixtures (tmp_project, sample_config, python_project) live in tests/conftest.py. An autouse _isolate_registry fixture redirects the global registry to a temp directory during tests.

Lint & Type Check

ruff check .                     # Lint
ruff check --fix .               # Lint with autofix
mypy factory/                    # Type check

Style

  • Python 3.11+ — use X | Y unions, not Union[X, Y]
  • Snake_case everywhere
  • 100 char line length (enforced by ruff)
  • All Pydantic models use ConfigDict(strict=True, extra="forbid")
  • Async/await by default — library functions in store.py and eval/runner.py are async, the CLI wraps them with asyncio.run()
  • Structured logging via structlog — use log = structlog.get_logger() at module level

Versioning

Version is derived from git tags via hatch-vcs at build time — no static version = in pyproject.toml.

  • Tag pattern: v* (e.g., v0.3.1); nightly-* tags are ignored via --match 'v*'
  • Dev installs show X.Y.Z.devN+gSHA between releases
  • factory/_version.py is generated by the hatch-vcs build hook and gitignored
  • After pulling new tags, re-run uv sync for editable installs to pick up the new version
  • fallback_version = "0.0.0" is used in environments without git history (Docker builds, tarballs)
  • Runtime version: importlib.metadata.version("remote-factory")
  • CLI: factory --version

Architecture (v2 — CEO Agent + Workflow Graph Engine)

The factory is a four-layer system:

Layer 1: Python CLI (factory/)

Pure tools that don't make decisions. Entry point is factory/cli.py → factory.cli:main (registered as factory script in pyproject.toml). Each subcommand is a cmd_* function dispatched via a handler dict. Key modules include factory/clean_pr.py (Clean PR Mode — strips non-essential artifacts from PRs before pushing to external repos).

Layer 2: Workflow Graph Engine (factory/workflow/)

All 10 factory modes (build, design, improve, research, meta, discover, review, refine, founder, plan) are defined as directed graphs of typed nodes in factory/workflow/definitions.py. Each graph is a Workflow Pydantic model with AgentNode, FnNode, GateNode, ForkNode, JoinNode, and Study primitives connected by Edge objects. See factory/workflow/README.md for full documentation.

The same graph definition produces two execution formats:

  • Headless: WorkflowExecutor (factory/workflow/executor.py) walks the DAG deterministically — factory workflow run <name> --project /path
  • Interactive: skill_export.py converts graphs to Claude Code SKILL.md files under skills/workflow-*/ — the CEO agent reads these at runtime as mode-specific playbooks

Package ecosystem (factory/workflow/package.py): A Package wraps a workflow subgraph behind a typed interface (Port, StateContract, OptKnob, MemoryDeclaration) and composes with other Packages via Sequential, Parallel, Conditional, and Loop operators. Package.compile() lowers compositions to flat Workflow IR with knob_values, knob_bounds, and knob_expandable fields for optimizer-tunable parameters. See docs/design/package-ecosystem.md for the design doc.

Layer 2b: Outer Loop — Evolutionary Workflow Search (factory/outer_loop/)

The outer loop evolves workflow topologies via MAP-Elites quality-diversity search. Given a base workflow (e.g. the single-builder FeatureBench seed), it produces a population of structurally diverse candidates, evaluates each via an inner loop (one full CEO cycle per candidate), and uses contrastive reflection to guide mutations toward higher fitness.

Pipeline: calibrate → evolve → reflect → evaluate (repeats until budget exhaustion, plateau, or target score).

Key modules:

  • engine.py — SwarmEngine orchestrates the evolutionary loop: seeding, tournament selection, mutation, evaluation, convergence detection (plateau, diversity collapse, early stop)
  • evaluator.py — SwarmEvaluator with FitnessCache (structural-hash dedup) and CycleRecordCache (content-hash dedup). Supports both EvaluatorFn protocol and FeatureBenchInnerLoop evaluation with git worktree isolation
  • mutations.py — 8 structured mutation operators (NODE_INSERT, NODE_REMOVE, EDGE_REDIRECT, PARALLELIZE, SERIALIZE, PARAM_MUTATE, PROMPT_MUTATE, KNOB_MUTATE) with WeightedRandomStrategy, reflection-guided selection, and default_knob_expander (Opus via CLI) for runtime expansion of OptKnob bounds
  • population.py — Population (collection management) and MAPElitesArchive (4D grid: depth × fork_degree × agent_count × gate_count)
  • similarity.py — structural_hash, graph_edit_distance, compute_features (includes knob values as feature dimensions for MAP-Elites diversity), NoveltyFilter
  • reflector.py — OuterLoopReflector performs two-stage contrastive reflection (top-K vs bottom-K) to identify failure/success patterns and generate mutation suggestions
  • mode_registry.py — EphemeralModeRegistry registers candidate workflows as temporary modes (evolve-gen{N}-{id[:8]}) with content-hash integrity checking, target-dir mirroring, and promotion to permanent modes
  • designer.py — DesignerAgent generates from-scratch workflow designs (minimal, thorough, custom variants)
  • models.py — Pydantic models: SwarmConfig, Individual, EvalResult, GenerationSummary, OuterLoopResult, HyperparameterRecord, MutationRecord, OuterLoopState, AuditResult
  • overfit.py — OverfitDetector compares training vs holdout scores to flag overfitting
  • subset.py — SubsetSelector protocol and FixedSubsetSelector for training instance selection
  • filesystem.py — Outer loop directory initialization, config/checkpoint persistence
  • featurebench_inner_loop.py — Bridges outer loop evaluation to a full CEO cycle on a FeatureBench instance

E2E finding: On simple FeatureBench tasks, a single-builder topology (1 AgentNode, no fork/join) wins on parsimony + cost. The outer loop's value emerges on harder multi-agent problems where topology diversity matters.

Layer 3: CEO Agent (factory/agents/prompts/ceo.md + skills/workflow-*/SKILL.md)

The CEO prompt is split into two parts:

  • ceo.md (501 lines) — core identity, cross-cutting rules (Sacred Rules, FEEC, keep/revert framework, review gates, error recovery, self-learning). No mode-specific procedures.
  • skills/workflow-*/SKILL.md (8 files) — each mode's full step-by-step playbook, auto-generated from the workflow graph definitions via factory workflow export-skills.

Spawned via factory ceo /path or factory run /path. The CEO receives ceo.md as its system prompt, detects project state, then reads the appropriate SKILL.md into its context and follows it as the mode-specific playbook.

Layer 4: Specialist Agents (factory/agents/)

Eight specialist Claude Code subprocesses spawned by the CEO via factory agent <role>. Agent prompts are resolved via factory/agents/runner.py with a two-tier lookup: project-specific override (.factory/agents/<role>.md) then factory default (factory/agents/prompts/<role>.md). Evolved playbooks from ~/.factory/playbooks/<role>.md (user-local, ACE-generated) are auto-injected, falling back to factory defaults in factory/agents/playbooks/<role>.md.

Roles: Researcher (observe), Strategist (hypothesize and refine ideas), Builder (implement), QA (health check + code review + adversarial QA), Archivist (record), Refiner (scope refinements), Failure Analyst (research mode), CEO (orchestrate).

Key data flow

  1. State detection (factory/state.py): Checks git, .factory/config.json, and eval_profile.json to determine one of 5 ProjectState enum values
  2. Discovery (factory/discovery/): introspect.py → profile.py → generate.py — detects project language/framework, builds an EvalProfile of dimensions, generates eval/score.py
  3. Eval (factory/eval/): runner.py executes the eval command as a subprocess, expects JSON stdout {"results": [...]}. Growth dimensions (growth.py) are computed locally and merged at 50/50 with project hygiene dimensions. scorer.py computes the weighted composite
  4. Strategy (factory/strategy.py): FEEC priority heuristic (Fix > Exploit > Explore > Combine) classifies hypotheses by keyword matching, with stuck detection after 3+ consecutive same-category reverts
  5. Store (factory/store.py): ExperimentStore manages the .factory/ directory — config, TSV history, per-experiment dirs with hypothesis/eval/diff/verdict artifacts. Auto-registers projects in the global registry on begin() and updates stats on finalize()
  6. Registry (factory/registry.py): Global project registry at ~/.factory/registry.json — self-registration pattern, project discovery for ACE/insights without --projects-dir
  7. Report (factory/report.py): Performance report generation — consolidates experiment records, CEO verdicts, and observations into .factory/performance_report.json for ACE consumption
  8. Checkpoint (factory/checkpoint.py): Saves and loads CEO state for crash-resilient resume
  9. Analysis (factory/analysis.py): Experiment comparison (diff) and FEEC analysis (explain)
  10. Adversarial (factory/adversarial.py): GAN-style adversarial eval loop state machine — phase transitions with hysteresis, per-role streak counters, convergence detection. State persisted at .factory/adversarial_state.json
  11. Contained (factory/contained/ + factory/podman.py + factory/cli/contained.py): factory contained [runtime flags] -- <any factory command> runs the factory in a podman container (--target local) or a cluster pod (--target k8s). See "Contained runtimes" below.

Target project's .factory/ layout

.factory/
├── config.json               # Parsed from factory.md (FactoryConfig model)
├── eval_profile.json         # Discovered eval dimensions (EvalProfile model)
├── results.tsv               # Append-only experiment history
├── performance_report.json   # Consolidated project data for ACE (auto-generated)
├── experiments/
│   └── 001/                  # Per-experiment: hypothesis.md, eval_before.json, eval_after.json, changes.diff, verdict.json
├── strategy/                 # observations.md, current.md, backlog.md, insights.md, research.md
├── reviews/                  # Agent output capture + CEO review verdicts
│   ├── <role>-latest.md      # Auto-saved stdout from each agent invocation
│   └── ceo-verdict-<role>.md # CEO's review verdict (PROCEED/REDIRECT/ABORT)
├── adversarial_state.json    # Adversarial loop state (phase, streaks, history)
├── outer_loop/               # Evolutionary workflow search state
│   ├── config.json           # SwarmConfig for the current run
│   ├── state.json            # OuterLoopState for crash recovery
│   ├── population/           # Serialized Population (population.json)
│   ├── archive/              # Serialized MAPElitesArchive (grid.json)
│   ├── modes/                # Ephemeral mode JSONs (evolve-gen{N}-{id}.json)
│   ├── results/              # Per-generation eval results (gen{N}.json)
│   ├── reflections/          # Contrastive reflection reports (gen{N}.json, gen{N}.md)
│   ├── events.jsonl          # Per-generation best/mean/diversity metrics
│   ├── costs.jsonl           # Per-individual cost tracking
│   └── trajectory.jsonl      # Score trajectory over generations
├── workflows/                # Ephemeral .py wrappers for WorkflowRegistry discovery
├── archive/                  # Long-term knowledge store (Archivist notes)
│   ├── experiments/          # Per-experiment learnings and decision rationale
│   ├── patterns/             # Recurring patterns and anti-patterns
│   └── decisions/            # Major architectural and strategy decisions
└── agents/                   # Per-project agent prompt overrides

Models

All domain models live in factory/models.py as strict Pydantic v2 models. Key types: ProjectState (enum), FactoryConfig, EvalProfile / EvalDimension, CompositeScore / EvalResult, ExperimentRecord, CrossProjectInsights, AgentVerdict, Observation, PerformanceReport, ProjectEntry / ProjectRegistry, AdversarialConfig / AdversarialComponent / AdversarialState / AdversarialPhaseRecord. The Notifier protocol defines the async notification interface. FactoryConfig includes clean_pr (bool), clean_pr_include (list[str]), and clean_pr_exclude (list[str]) for Clean PR Mode — stripping non-essential artifacts from PRs before pushing to external repos. FactoryConfig.adversarial (AdversarialConfig | None) holds the GAN-style adversarial eval loop configuration parsed from factory.md.

Outer loop models live in factory/outer_loop/models.py: SwarmConfig (evolutionary search configuration — benchmark, budget, population_size, mutation_rate, frozen_node_ids, training/holdout instances, convergence thresholds), Individual (candidate with workflow_data, score, features, lineage), EvalResult (benchmark_score + hygiene_score + cost + complexity), GenerationSummary (per-generation stats), OuterLoopResult (final run result with trajectory, pareto front, hyperparameter history), HyperparameterRecord (per-generation mutation_rate, operator_weights, diversity), MutationRecord (operator + target_node + before/after), MutationType (enum: 8 mutation operators including KNOB_MUTATE), OuterLoopState (checkpoint for crash recovery), AuditResult (overfit detection).

Environment

Requires Claude Code installed and authenticated. The factory spawns claude subprocesses — it does not call the API directly. Any Claude Code authentication method works (API key, Vertex AI, etc.).

Configuration (~/.factory/config.toml)

All FACTORY_* environment variables can also be set in ~/.factory/config.toml. Env vars remain supported — config.toml is additive. Five-tier precedence: CLI flag > env var > profile credential > config.toml default > hardcoded default.

[defaults]
runner = "claude"
model = ""
projects_dir = "~/factory-projects"

[credentials.vertex]
FACTORY_RUNNER = "claude"
ANTHROPIC_API_KEY = "sk-ant-..."

Commands:

  • factory config show [--reveal] — show resolved config with secrets masked
  • factory config edit — open ~/.factory/config.toml in $EDITOR
  • factory config migrate — create starter config from current env vars (requires tomli_w)

Credential profiles: Use --profile <name> with factory ceo, factory run, or factory agent to load a [credentials.<name>] section. Profile keys override existing env vars (explicit --profile opt-in means the profile is authoritative). CLI flags still win via 5-tier precedence.

Env overlay features:

  • Override: Profile keys are set via os.environ[k] = v, not setdefault — the profile wins over shell env vars
  • Unset: Add a [credentials.<name>.unset] sub-table with vars = ["VAR1", "VAR2"] to remove env vars before injection. Unsets are processed before sets.
  • Protected vars: The following env vars cannot be set or unset via profiles — a ValueError is raised if attempted: PATH, HOME, USER, SHELL, TMPDIR, TERM, PWD (shell fundamentals); LD_PRELOAD, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES (code execution vectors); PYTHONPATH, GOPATH, CLASSPATH, NODE_PATH (language path injection); IFS (shell parsing); FACTORY_TRACE_ID, FACTORY_PARENT_SPAN_ID (factory observability internals).
  • Unset vars validation: The [credentials.<name>.unset].vars field must be a list — a ValueError is raised if it is a string or other non-list type.
  • Override warnings: When a profile overrides an existing env var with a different value, a log.warning("profile_override", key=k, profile=profile) is emitted (values are not logged to avoid leaking secrets).

Custom endpoint example (e.g. a LiteLLM proxy):

You can use profiles to point the factory at a custom model endpoint:

[credentials.litellm-proxy]
FACTORY_RUNNER = "claude"
FACTORY_MODEL = "your-model-name"
ANTHROPIC_BASE_URL = "https://your-litellm-proxy.example.com"
ANTHROPIC_API_KEY = "your-api-key-here"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1"

[credentials.litellm-proxy.unset]
vars = ["CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_BEDROCK", "ANTHROPIC_VERTEX_PROJECT_ID"]

Usage: factory ceo /path --profile litellm-proxy

Implementation: factory/user_config.py — load_config(), resolve(), show_config(), migrate_env_to_config().

Runners

The factory uses Claude Code (claude CLI) as its agent backend. The runner abstraction (factory/runners/) supports this via ClaudeRunner in factory/runners/claude.py. The runner protocol (factory/runners/protocol.py) defines the interface.

Custom binary: Set FACTORY_CLAUDE_BIN to use a wrapper script (e.g. glaude) instead of the claude binary. All runner invocations (headless, interactive, background, tmux) and CLI commands (factory ceo, factory resume) respect this override. Works with credential profiles:

[credentials.glaude]
FACTORY_CLAUDE_BIN = "glaude"

Usage: factory ceo /path --profile glaude

Important: Target projects should add .factory/ to their .gitignore. The factory writes experiment data and usage logs to this directory. These are project-local artifacts that should not be committed to version control.

Running the factory

# Build — from idea, spec file, or GitHub URL
factory ceo "Build a weather CLI"               # Raw idea → ~/factory-projects/weather-cli/
factory ceo "Build a weather CLI" --dir my-app  # Explicit dir name override
factory ceo ~/ideas/spec.md                     # Spec file → new project
factory ceo https://github.com/user/repo        # Clone and improve
factory ceo "distributed eval runner" --mode design  # Brainstorm → build
factory ceo ~/ideas/detailed-spec.md --mode design   # Long idea from file (no length limit)
factory ceo /path/to/project --mode design           # Discuss what to work on → improve
factory ceo /path/to/project --mode design --focus "auth"  # Discuss a specific topic
factory ceo "weather CLI" --mode design --auto-approve  # Design without user approval gate
factory ceo /path/to/project --mode design --from-plan .factory/strategy/current.md  # Build from local plan
factory ceo /path/to/project --mode design --from-plan 42  # Build from plan issue #42
factory ceo /path/to/project --mode design --from-plan 'auth dashboard'  # Fuzzy search plans
factory ceo "SWE-bench solver" --mode research            # Research ideation → build
factory ceo /path/to/factory --mode create --focus "mode description"  # Create a new factory mode
factory ceo /path/to/factory --mode create --focus "improve: add plateau detection"  # Update existing mode
factory ceo /path/to/factory --mode create --focus 'approval workflow' --plugin                           # Plugin package → ./approval-workflow-plugin/
factory ceo /path/to/factory --mode create --focus 'approval workflow' --plugin --folder ~/plugins/approval  # Explicit output dir
factory ceo /path/to/project --mode design --just-plan                    # Research + strategy, no implementation
factory ceo "distributed eval runner" --mode design --just-plan            # Plan a new idea
factory ceo /path/to/project --mode design --just-plan --focus "auth"      # Focused planning

# Design-v2 — inference-time scaling (dynamic directors, user intent ledger)
factory ceo "markdown link checker" --mode design-v2                      # Dynamic research + strategy
factory ceo /path/to/project --mode design-v2 --focus "auth"              # Existing project
factory ceo /path/to/project --mode design-v2 --auto-approve              # CEO acts as user at gates

# Improve — point at existing codebase
factory ceo /path/to/project                    # Single improvement cycle
factory run /path/to/project --loop --interval 1800  # Continuous heartbeat
factory tmux /path/to/project --loop            # In detached tmux session

# Focus — build exactly one thing
factory ceo /path/to/project --focus "dashboard UI"  # One item, one hypothesis, done
factory ceo /path/to/project --focus 42              # Target GitHub issue #42
factory ceo /path/to/project --focus "owner/repo#42" # Target issue by shorthand
factory ceo /path/to/project --focus '42 and 43'     # Multiple issues
factory ceo /path/to/project --focus 'issue 42, issue 43'  # With 'issue' keyword

# Founder — rapid prototyping (NOT for production)
factory ceo /path/to/project --mode founder                       # One fast hypothesis
factory ceo /path/to/project --mode founder --focus "auth flow"   # Targeted prototype
factory run /path/to/project --mode founder --loop --interval 300 # Rapid iteration

# Study — graph-powered codebase analysis
factory ceo /path/to/project --mode study                    # Graph-powered codebase study
factory ceo /path/to/project --mode study --focus "auth flow" # Focused study with graph context

# Meta — improve the factory's own agents
factory ceo /path/to/project --mode meta        # Improve + ACE playbook evolution

# Agents & analysis
factory agent researcher --task "..." --project /path  # Invoke a specialist directly
factory study /path                             # Analyze code + write observations
factory diff /path --exp1 N --exp2 M            # Compare two experiments
factory explain /path --exp N                   # Explain experiment with FEEC analysis

# Backlog
factory backlog-list /path                      # List pending backlog items
factory backlog-add /path "item text"           # Add a new item to the backlog
factory backlog-remove /path "item text"        # Remove a completed backlog item

# Adversarial eval loops
factory adversarial-state /path/to/project           # Inspect adversarial loop state
factory adversarial-state /path/to/project --reset   # Reset to defaults

# Outer loop — evolutionary workflow search
factory outer-loop calibrate /path --benchmark featurebench --budget 50 --population-size 4
factory outer-loop calibrate /path --training-instances t1 t2
factory outer-loop calibrate /path --project-dir /path/to/target  # Evaluate on a different project
factory outer-loop evaluate /path --generation 0                  # Evaluate current generation
factory outer-loop evaluate /path --generation 0 --project-dir /path/to/target
factory outer-loop reflect /path --generation 0                   # Contrastive reflection
factory outer-loop evolve /path --generation 0                    # Produce next generation
factory outer-loop status /path                                   # Show progress and metrics
factory outer-loop status /path --check-converge                  # Exit 0 if converged, 1 if not
factory outer-loop promote /path --mode-name evolve-gen5-abc12345 --permanent-name best-evolved

# Operations
factory dashboard --projects-dir ~/factory-projects    # Live web dashboard on :8420
factory export /path/to/project                 # Dump full project snapshot as JSON
factory checkpoint /path/to/project             # Save CEO state for crash recovery
factory resume /path/to/project                 # Resume an interrupted CEO session
factory precheck /path --score-before 0.7 --score-after 0.85  # Hard precheck gate
factory review --verdict KEEP --pr 42           # Post structured review on GitHub PR

factory run / factory ceo spawn the CEO agent as a subprocess using the Claude Code runner. The CEO owns the full workflow: state detection, agent spawning, experiment lifecycle, and mandatory archival. The --loop flag adds a heartbeat wrapper with configurable interval and max cycles. --mode meta runs the full Improve loop on the factory itself, then ACE playbook evolution for all agent roles. --focus activates targeted mode: builds exactly one item and exits. Accepts backlog names (--focus "eval reliability"), issue numbers (--focus 42), issue URLs, or owner/repo#N shorthand. Multiple issues can be specified in a single --focus string using commas, spaces, or "and" (e.g., --focus "111 and 112", --focus "issue 42, issue 43", --focus "#111 #112"). Each issue is fetched independently and added as a separate backlog item. Issue refs are auto-detected and fetched via gh/glab CLI. Works in improve, research, and create modes; mutually exclusive with --loop. In create mode, --focus provides the mode description; use --focus "mode_name: change description" to update an existing registered mode instead of creating a new one. --mode design enters ideation mode. For new ideas (e.g. factory ceo "distributed eval runner" --mode design), the CEO researches the space via the Researcher, then iteratively refines the idea with the Strategist through user feedback, producing a phased build plan before building. For existing projects (e.g. factory ceo /path/to/project --mode design), the CEO studies the project (backlog, eval scores, open issues, history), presents findings, and discusses what to work on, then continues to implementation automatically after approval. --mode interactive is accepted as a backward-compatible alias for --mode design. --focus is allowed on existing projects to seed the discussion topic. Incompatible with --headless unless --auto-approve is used. --auto-approve lifts the headless restriction for design mode, forcing headless execution and auto-approving user gates (e.g. strategy review) — useful for CI/CD and automated pipelines. --from-plan <source> loads an existing plan into design mode, skipping the research phase. Accepts a local file path, GitHub issue URL, issue number, or fuzzy search string (searches GitHub issues with the plan label). Requires --mode design; mutually exclusive with --focus and --prompt. When fetching from a GitHub issue, includes both the issue body and all comments. --mode research enters research ideation for new projects (e.g. factory ceo "SWE-bench solver" --mode research) — the Strategist collects research config (target metric, mutable/fixed surfaces, constraints) before building. For existing projects with research_target configured, runs the research improvement loop directly. Incompatible with --headless (for new projects) and --prompt. --refine "<request>" enters refinement mode — routes a single change request through the Refiner → Builder → full review pipeline. Mutually exclusive with --mode, --prompt, and --focus. Requires an existing project directory. In foreground mode, the CEO also enters the refinement loop automatically after completing a build/improve cycle, staying active for follow-up requests without --refine. --mode founder enters rapid prototyping mode — a stripped-down pipeline (Study → Strategist → Builder → health gate → record) with 2 agent calls and 1 test run. Skips research, code review, adversarial QA, and eval scoring. Designed for fast hypothesis iteration: test an idea, see if it works, pivot. Terminal mode — does not chain to other modes. Not for production use; run --mode improve afterward to harden what works. Compatible with --focus and --loop. --just-plan (requires --mode design) enters planning-only mode — research + strategy + optional GitHub publishing with no implementation. Three parallel researchers investigate domain, practices, and constraints. The Strategist synthesizes a phased plan. Single user gate: keep the plan? Approval auto-publishes to GitHub as an issue with the plan label and seeds the backlog with plan phases. Terminal mode — does not chain to other modes. Compatible with --focus. Mutually exclusive with --from-plan and --prompt.

Contained runtimes

factory contained runs any factory command somewhere other than the developer's shell. Everything after -- is handed inward verbatim except for path rewriting — the runtime is a place to run the factory, not a mode of it, so the host never parses the payload's semantics and cannot break when the CLI grows.

factory contained -- ceo ~/code/rta                      # local container, watch it
factory contained --division -- ceo ~/code/rta           # ...and let the agent build images
factory contained --target k8s --namespace ns -- run ~/code/rta --loop
factory contained --target k8s --division -- ceo ~/code/rta
factory contained --target openshell -- ceo ~/code/untrusted-project --headless  # sandbox (experimental)
factory contained ls | attach <name> | rm <name> | sync <name> | setup | verify | bundle
FACTORY_CONTAINED_DRY_RUN=1 factory contained -- study ~/code/rta   # compose, provision nothing

The three targets share a command surface and an image, not a threat model. Local and k8s do not confine agent-authored code and do not replace review — local is the weaker of the two (no egress control, credentials live inside the container); k8s keeps a restricted SCC and namespace-scoped RBAC. --target openshell (experimental) is the exception: it exists to confine untrusted agent code — kernel Landlock/seccomp, deny-by-default egress through an OpenShell supervisor, credentials held by the gateway and attached as a claude-code provider (never in the sandbox env; no fallback, by design). The key cannot be extracted, but any descendant of claude — every command the agent's Bash tool runs — can use the placeholder against the provider's endpoint (OpenShell binary rules match parent processes), so the provider profile is copy-edited to allow api.anthropic.com only (never stock, whose statsig/sentry endpoints would be exfiltration channels) and the plan env sets CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1. Its default policy (workspace rw, read-only /opt/factory, uv→PyPI; inference egress comes solely from the provider attachment, whose endpoints the policy must not restate) is code in factory/contained/openshell.py; --policy is a full replacement, never a merge. The openshell Python SDK is an optional extra (uv sync --extra contained-openshell); the run path is factory/cli/contained_openshell.py, mirroring contained_k8s (self-contained workspace copy, tarball upload via CLI, provenance probes via SDK exec) — except the run is a detached nohup process writing .factory/run.log/run.pid/run.exit, not a tmux session: a sandbox cannot allocate a PTY (OpenShell #749), so those files are the run's interface and attach is a read-only tail -f. Because there is no terminal at all, interactive payloads (ceo/run/resume/tmux without --headless) are refused at parse time. --gateway NAME must reach every gateway-bound operation — SDK and CLI (_cli() composes it into each argv) — or an upload lands in a same-named sandbox on the active gateway.

Six things are load-bearing and fail quietly if broken:

  • Provenance. A run always starts from the files on this machine, uncommitted changes included — never HEAD, never a fresh clone. The workspace is a git worktree with the working tree rsynced over the top, because a HEAD checkout silently drops the gitignored .factory/ the whole experiment history lives in. Five assertions then run between provisioning and the first agent call (factory/contained/provenance.py); a failure aborts naming the file and the likely cause, and leaves the runtime up for inspection.
  • Identity. A bind mount carries ownership through unchanged, so a container whose UID does not own the tree gets a silently read-only workspace. The rule differs between rootless, rootful and macOS, so factory/contained/identity.py probes rather than deciding: a throwaway container reports the mount's owner as the kernel inside sees it, and the run matches. The runtime image is built for arbitrary UIDs (group 0, chmod g=u), which is also what OpenShift's restricted SCC needs.
  • PID 1. The factory spawns agent subprocesses and is not a well-behaved init, so the container runs --init around sleep infinity and the run itself lives in tmux. The runtime persists after the run — a failed run is exactly when its state is worth reading.
  • Credentials cross the boundary, by design. There is no gateway. The policy is FACTORY_ by default, plus exactly what --forward names, plus the backend variables the resolved shape requires (factory/contained/credentials.py) — nothing implicit. verify reports credential shape, never material, and secret-looking values are redacted anywhere a command is printed. On k8s the credentials come from a namespace Secret the user creates; the factory references it by name and never handles the material.
  • Both divisions reach outward, and that is the point. Builds cannot happen inside either boundary, so --division is opt-in and separately named. Locally it starts an unauthenticated podman-mcp-server on 0.0.0.0:8430 — every interface, because the tool has no bind flag and the container reaches the host through a gateway address rather than loopback — detached into its own process group, because the run outlives the launch, and stopped by factory contained rm. On the cluster it goes through OpenShift Build objects behind a sidecar container that is the only holder of oc and the ServiceAccount token; that separation is a boundary only while the Role excludes pods/exec, which verify asserts via a SubjectAccessReview API object — oc auth can-i --as collapses pods/exec onto pods and answers "yes" where RBAC says no. The sidecar runs a different image (FACTORY_CONTAINED_SIDECAR_IMAGE, an oc image) from the agent's; one image for both silently collapses the boundary.
  • Interactive prompts stall an unattended run. A fresh ~/.claude makes Claude Code ask about folder trust, project MCP servers, and Bypass Permissions mode — all interactive-only, so headless agents never hit them and the interactive CEO does, and the run then sits at a menu nobody is watching. factory/contained/claude_state.py pre-records those answers, which the invocation already implies.
  • All podman knowledge lives in factory/podman.py and all cluster knowledge in factory/contained/k8s.py. Both compose commands and do not execute them, which is what makes FACTORY_CONTAINED_DRY_RUN=1 print the same argv the real path runs rather than a separate rendering that drifts.

The runtime image (containers/factory/Containerfile) is UBI9 + the factory wheel + the agent CLIs + tmux, published multi-arch by CI (.github/workflows/runtime-image.yml) — amd64 for cluster nodes, arm64 for a Mac laptop. It publishes on pushes to main (:latest), on published releases (:<tag>, plus :latest unless the release is a prerelease — nightlies are, so they never move :latest), and on dispatch. The release trigger is load-bearing: factory contained setup pulls and does not build, so a release whose image was never built breaks every new user's first command. Release and dispatch tag names reach the shell through env: and are validated against the legal image-tag character set before use.

setup is a numbered wizard rather than a column of output (factory/contained/style.py): step rules, [ ok ]/[FAIL] marks, and — the part that caused real confusion — every resolved value printed quoted and coloured, because "in namespace default" gives the reader no way to tell the name from the sentence. Colour obeys NO_COLOR > FORCE_COLOR > TTY detection, so the same strings stay plain in pipes, logs and CI. The cluster half asks which namespace to prepare when --namespace was not given (the current context supplies the default, not the answer) and names the cluster alongside it — a namespace alone identifies nothing, since default exists on every cluster. Only names are read from the kubeconfig, never the users section.

Which cluster is chosen, not assumed. setup lists the kubeconfig's contexts and lets one be picked (--context NAME skips the question). The choice is applied as --context on every cluster command via k8s.cli() — a process-global _ACTIVE_CONTEXT set once at entry, because threading it through forty call sites means forty chances to forget, and cli() is a single auditable application point. It never rewrites the kubeconfig; switching the default is offered separately at the end, with the oc config use-context command printed either way.

The chosen namespace is checked for existence (_namespace_status) even when passed via --namespace, and creation is offered — oc new-project, not create namespace, because a regular user is usually denied the second. On OpenShift a Forbidden on get namespace says nothing about existence, so it falls back to get project and reports unreadable rather than absent.

The cluster review is object-by-object, not a wall of YAML (factory/contained/k8s_review.py). The bundle exists as a list (bundle_objects()) before it exists as a blob; render_bundle joins that list, and verify's per-object checks are derived from it, so the three can never describe different object sets. Each object is compared against the namespace with oc diff (server-side, so cluster-defaulted fields do not read as user changes), producing current / absent / differs / unknown. Only the ones needing a decision are walked, each showing its purpose plus its diff (for differs) or its manifest (for absent). current is never prompted about — a prompt whose only sane answer is yes teaches people to stop reading prompts — and unknown is never silently skipped.

Each object is applied at the moment it is accepted, never batched. Batching made q report "nothing was applied" to a user who had already said yes, which is false; WalkResult records what actually happened and the abort message says how much survives. A failed apply names itself and does not stop the walk. There is no second blanket confirm after the walk.

Prompt options are spelled out ([y]es [n]o [a]ll remaining [q]uit), not [y/n/a/q]. style.read_key and style.read_line put the terminal in cbreak mode, which is the only way Escape can cancel — a line-buffered input() only ever sees the ^[ characters it inserts. read_line is a small line editor (echo, Backspace, arrow-key drain) because cbreak turns off the line discipline that normally provides them. Both return None when raw reading is impossible (pipe, non-POSIX) and callers fall back to input() plus style.is_escape(); input() raises OSError under pytest capture, not EOFError, so both are caught. Ctrl-C is caught in cmd_contained and exits 130 with a message — backing out of a wizard is ordinary, not a crash.

verify_k8s streams. It takes an on_check callback and reports each result the moment it is known; prereq.format_check / summary_line are split out of render_checks so a caller can print per-result and add the verdict at the end. Without this the Verify step printed nothing for minutes — several checks are a cluster round trip, and the in-cluster inference probe creates a pod and waits up to 180s — and it was reported as a hang. Every result goes through the local record() helper, including the early cli_binary() failure, because a streaming caller prints only the summary afterwards and a check that skips the callback is never seen. The inference probe is skipped when the credentials Secret is missing: the probe pod mounts it, so it could only burn its full timeout rediscovering what the Secret check just reported — which is the state every freshly prepared namespace is in.

Tests must never reach a raw prompt. tests/conftest.py has an autouse fixture forcing style._raw_session to None; without it a prompt blocks forever on a keypress, ignoring builtins.input patches, because the raw path does not call input(). tests/test_contained_k8s.py additionally stubs list_contexts/cluster_context/current_namespace — they shell out to real oc, which cost that file seven minutes before being stubbed. tmux is compiled in a builder stage because neither the UBI repositories nor EPEL ship it (EPEL never duplicates a package RHEL carries, and UBI's subset omits it).

Two cluster-side details that fail quietly: a PVC mounts root-owned, so the pod needs an fsGroup read from the namespace's allocated range (hardcoding one fails admission under a MustRunAs SCC); and the workspace unpack marker is per-run, because the PVC outlives the run that filled it and a shared marker makes the next run skip its own upload and execute against stale files.

User-facing guide: docs/contained/index.md.

Observability

Events: All agent invocations and cycle transitions are logged to .factory/events.jsonl as append-only structured events. The agent runner (factory/agents/runner.py) emits agent.started, agent.completed, agent.failed, and agent.timeout events automatically. The heartbeat loop emits cycle.started and cycle.completed.

Dashboard: factory dashboard starts a FastAPI server (default port 8420) that serves a live web UI with SSE-powered event streaming. It scans a projects directory for all .factory/-managed projects and shows real-time agent activity, experiment history, and project scores. Designed to run on an always-on machine.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.