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
- akashgit/remote-factoryword for word, first seen Oct 5, 2026
Other versions1 project uses a version of this.
Word for word · 1
- akashgit/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.
- AGENTS.mdAGENTS.md
- factorySkill
- implementSkill
- pipelineSkill
- pipeline-subagentsSkill
- statusSkill
- studySkill
- workflow-design-v2Skill
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
- Very specific: line 36
All Pydantic models use ConfigDict(strict=True, extra="forbid")
- Very specific: line 47
After pulling new tags, re-run uv sync for editable installs to pick up the new version
- Very specific: line 185
Credential profiles: Use --profile <name> with factory ceo, factory run, or factory agent to load a [credentials.<name>] section.
- Very specific: line 189
Unset: Add a [credentials.<name>.unset] sub-table with vars = ["VAR1", "VAR2"] to remove env vars before injection.
- Very specific: line 192
Override warnings: When a profile overrides an existing env var with a different value, a log.warning("profile_override", key=k, …
- Very specific: line 369
Events: All agent invocations and cycle transitions are logged to .factory/events.jsonl as append-only structured events.
≈ “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.
Files
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 pointTest
pytest -v # Full suite
pytest tests/test_models.py -v # Single file
pytest -k "test_detect" -v # By name pattern
pytest --cov # With coverageTests 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 checkStyle
- Python 3.11+ — use
X | Yunions, notUnion[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.pyandeval/runner.pyare async, the CLI wraps them withasyncio.run() - Structured logging via
structlog— uselog = 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+gSHAbetween releases factory/_version.pyis generated by the hatch-vcs build hook and gitignored- After pulling new tags, re-run
uv syncfor 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.pyconverts graphs to Claude CodeSKILL.mdfiles underskills/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—SwarmEngineorchestrates the evolutionary loop: seeding, tournament selection, mutation, evaluation, convergence detection (plateau, diversity collapse, early stop)evaluator.py—SwarmEvaluatorwithFitnessCache(structural-hash dedup) andCycleRecordCache(content-hash dedup). Supports bothEvaluatorFnprotocol andFeatureBenchInnerLoopevaluation with git worktree isolationmutations.py— 8 structured mutation operators (NODE_INSERT,NODE_REMOVE,EDGE_REDIRECT,PARALLELIZE,SERIALIZE,PARAM_MUTATE,PROMPT_MUTATE,KNOB_MUTATE) withWeightedRandomStrategy, reflection-guided selection, anddefault_knob_expander(Opus via CLI) for runtime expansion ofOptKnobboundspopulation.py—Population(collection management) andMAPElitesArchive(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),NoveltyFilterreflector.py—OuterLoopReflectorperforms two-stage contrastive reflection (top-K vs bottom-K) to identify failure/success patterns and generate mutation suggestionsmode_registry.py—EphemeralModeRegistryregisters candidate workflows as temporary modes (evolve-gen{N}-{id[:8]}) with content-hash integrity checking, target-dir mirroring, and promotion to permanent modesdesigner.py—DesignerAgentgenerates from-scratch workflow designs (minimal, thorough, custom variants)models.py— Pydantic models:SwarmConfig,Individual,EvalResult,GenerationSummary,OuterLoopResult,HyperparameterRecord,MutationRecord,OuterLoopState,AuditResultoverfit.py—OverfitDetectorcompares training vs holdout scores to flag overfittingsubset.py—SubsetSelectorprotocol andFixedSubsetSelectorfor training instance selectionfilesystem.py— Outer loop directory initialization, config/checkpoint persistencefeaturebench_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 viafactory 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
- State detection (
factory/state.py): Checks git,.factory/config.json, andeval_profile.jsonto determine one of 5ProjectStateenum values - Discovery (
factory/discovery/):introspect.py→profile.py→generate.py— detects project language/framework, builds anEvalProfileof dimensions, generateseval/score.py - Eval (
factory/eval/):runner.pyexecutes 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.pycomputes the weighted composite - 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 - Store (
factory/store.py):ExperimentStoremanages the.factory/directory — config, TSV history, per-experiment dirs with hypothesis/eval/diff/verdict artifacts. Auto-registers projects in the global registry onbegin()and updates stats onfinalize() - Registry (
factory/registry.py): Global project registry at~/.factory/registry.json— self-registration pattern, project discovery for ACE/insights without--projects-dir - Report (
factory/report.py): Performance report generation — consolidates experiment records, CEO verdicts, and observations into.factory/performance_report.jsonfor ACE consumption - Checkpoint (
factory/checkpoint.py): Saves and loads CEO state for crash-resilient resume - Analysis (
factory/analysis.py): Experiment comparison (diff) and FEEC analysis (explain) - 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 - 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 overridesModels
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 maskedfactory config edit— open~/.factory/config.tomlin$EDITORfactory config migrate— create starter config from current env vars (requirestomli_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, notsetdefault— the profile wins over shell env vars - Unset: Add a
[credentials.<name>.unset]sub-table withvars = ["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
ValueErroris 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].varsfield must be a list — aValueErroris 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 PRfactory 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 nothingThe 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.pyprobes 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
--initaroundsleep infinityand 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--forwardnames, plus the backend variables the resolved shape requires (factory/contained/credentials.py) — nothing implicit.verifyreports 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
--divisionis opt-in and separately named. Locally it starts an unauthenticatedpodman-mcp-serveron0.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 byfactory contained rm. On the cluster it goes through OpenShiftBuildobjects behind a sidecar container that is the only holder ofocand the ServiceAccount token; that separation is a boundary only while the Role excludespods/exec, whichverifyasserts via a SubjectAccessReview API object —oc auth can-i --ascollapsespods/execontopodsand answers "yes" where RBAC says no. The sidecar runs a different image (FACTORY_CONTAINED_SIDECAR_IMAGE, anocimage) from the agent's; one image for both silently collapses the boundary. - Interactive prompts stall an unattended run. A fresh
~/.claudemakes 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.pypre-records those answers, which the invocation already implies. - All podman knowledge lives in
factory/podman.pyand all cluster knowledge infactory/contained/k8s.py. Both compose commands and do not execute them, which is what makesFACTORY_CONTAINED_DRY_RUN=1print 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.
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
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.

