Configuration reference¶
The factory loads one YAML file. Without --config it uses the packaged
default, which is byte-identical to config/factory.example.yaml in the
repository.
The loader is strict. An unknown key is rejected rather than silently ignored,
and an invalid file fails with an explicit message and exit code 2.
factory¶
factory:
data_dir: "~/.software-factory"
agent_timeout_seconds: 900
retries:
same_model_attempts: 2
max_total_attempts: 6
| Key | Type | Default | Effect |
|---|---|---|---|
data_dir |
path | ~/.software-factory |
Where runs, workspaces, locks, logs and reusable repository guidance live. ~ is expanded. |
agent_timeout_seconds |
int > 0 | 900 |
Per-agent-invocation timeout. |
retries.same_model_attempts |
int > 0 | 2 |
Per-stage same-model attempt limit for implementation routing and supported typed-output correction. |
retries.max_total_attempts |
int > 0 | 6 |
Hard ceiling on implementation attempts per run. Must be at least same_model_attempts. |
The retry budget is persisted on the run. Restarting the process does not grant a run a fresh budget.
The writing policy is mandatory and has no configuration switch. See Writing policy.
models¶
models:
triage: { model: "gpt-5.6-terra", reasoning: "medium", context_tier: "default" }
refiner: { model: "gpt-5.5", reasoning: "high", context_tier: "default" }
researcher: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
planner: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
workers:
L0: { model: "mai-code-1.1-flash", reasoning: "medium", context_tier: "default" }
L1: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
L2: { model: "claude-sonnet-5", reasoning: "high", context_tier: "default" }
L3: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
tester: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
reviewer: { model: "gpt-5.6-sol", reasoning: "high", context_tier: "default" }
model_profiles:
economy:
triage: { model: "gpt-5.6-luna", reasoning: "medium", context_tier: "default" }
refiner: { model: "gpt-5.6-terra", reasoning: "high", context_tier: "default" }
researcher: { model: "gemini-3.8-flash", reasoning: "medium", context_tier: "default" }
planner: { model: "gpt-5.6-terra", reasoning: "high", context_tier: "default" }
workers:
L0: { model: "mai-code-1.1-flash", reasoning: "medium", context_tier: "default" }
L1: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
L2: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
L3: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
tester: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
reviewer: { model: "gpt-5.6-sol", reasoning: "high", context_tier: "default" }
security:
triage: { model: "gpt-5.6-terra", reasoning: "medium", context_tier: "default" }
refiner: { model: "gpt-5.5", reasoning: "high", context_tier: "default" }
researcher: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
planner: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
workers:
L0: { model: "mai-code-1.1-flash", reasoning: "medium", context_tier: "default" }
L1: { model: "gemini-3.8-flash", reasoning: "high", context_tier: "default" }
L2: { model: "claude-sonnet-5", reasoning: "high", context_tier: "default" }
L3: { model: "claude-opus-5", reasoning: "high", context_tier: "default" }
tester: { model: "gpt-6-astra", reasoning: "high", context_tier: "default" }
reviewer: { model: "gpt-5.6-sol", reasoning: "high", context_tier: "default" }
Every role takes a model, reasoning level and context_tier. The context
tier is default or long_context. The runtime always passes it explicitly
to Copilot, so a persisted interactive CLI setting cannot change a factory
run. Model names and reasoning levels are passed through without a catalog
whitelist, so unsupported combinations fail when Copilot executes.
The top-level models block is the default profile. Additional complete
profiles live under model_profiles. Select one on any agent-invoking command:
factory run ... --model-profile economy
factory project ... --model-profile economy
factory start ... --model-profile economy
factory skill refresh ... --model-profile economy
factory run ... --model-profile security
factory doctor validates the selected profile, and factory service install
stores the selected name in the LaunchAgent arguments. An unknown profile
fails with exit code 2 before a workspace or paid call is created. Profiles
are complete models blocks, not partial overlays. The security profile
uses Astra for adversarial testing and Sol for an independent final review.
It is intentionally much more expensive than economy.
workers must define exactly L0, L1, L2 and L3. Triage assigns the
complexity level and that selects the worker.
The reviewer must come from a different model family
Configuration is rejected if models.reviewer's model family matches any
worker's. Independent review from the same family as the implementer is not
independent enough to be a gate.
Model names appear only in configuration. They are not scattered through the source.
See Model selection, cost and benchmarks for the current Copilot catalog, prices, context and reasoning capabilities, benchmark evidence, and role-specific tradeoffs.
performance¶
The standard mode uses the selected model profile and permits the optional polish pass. The fast mode is an explicit low-risk optimization.
The controller uses the fast mode only for L0 or L1 work with R0 or
R1 risk. Triage must not require research. Planned and changed files must
not include protected, sensitive, manifest, or version files.
If a condition fails, the controller uses the standard path. It records the fallback reason in the run. Both modes keep deterministic verification, the independent Tester, the independent Reviewer, and publishing controls.
The fast_model_profile must name a complete entry under model_profiles.
The controller uses that profile for the Refiner and Planner. It skips the
optional polish pass only while the run remains eligible.
repository¶
repository:
branch_prefix: "factory/"
command_timeout_seconds: 900
commands:
install: []
verify: []
build: []
env_passthrough: []
log_capture_bytes: 32768
max_changed_files: 100
protected_file_patterns: [...]
| Key | Type | Default | Effect |
|---|---|---|---|
branch_prefix |
string | factory/ |
Required prefix for factory branches. Enforced before any push. |
command_timeout_seconds |
int > 0 | 900 |
Timeout per repository command. |
commands.install |
list of strings | [] |
Dependency installation, run first. |
commands.verify |
list of strings | [] |
Lint, types, tests. Run second. |
commands.build |
list of strings | [] |
Build. Run last. |
env_passthrough |
list of env var names | [] |
Extra variables repository commands can read. |
log_capture_bytes |
int > 0 | 32768 |
Max stdout/stderr bytes retained per command, after redaction. |
max_changed_files |
int > 0 | 100 |
Hard ceiling on changed files in one change. |
protected_file_patterns |
list of globs | see below | Paths a change can never touch. |
Commands are strings executed by a non-login /bin/sh. Shell operators such as
&&, pipes, redirects and globs work, but login profiles are not loaded.
Commands never inherit your environment. They get PATH, HOME, LANG and
TERM, plus the names in env_passthrough. Credentials such as GH_TOKEN and
AWS_* are never passed implicitly.
env_passthrough accepts variable names only. Values are read from your
environment at run time and are not stored.
protected_file_patterns¶
Default:
protected_file_patterns:
- ".env"
- ".env.*"
- "**/.env"
- "**/.env.*"
- "**/*.pem"
- "**/*.key"
- "**/*.p12"
- "**/*.pfx"
- "**/id_rsa"
- "**/id_ed25519"
- "**/.npmrc"
- "**/.netrc"
- "**/.pypirc"
- "**/.git-credentials"
- "**/credentials.json"
- "**/secrets.json"
- "**/secrets.yaml"
- "**/secrets.yml"
- "**/.aws/**"
- "**/.ssh/**"
Glob patterns matched against repository-relative changed paths. Setting this key replaces the default list, so include the defaults you still want.
scope_drift¶
| Key | Type | Default | Effect |
|---|---|---|---|
max_replans |
int >= 0 | 1 |
How many times scope drift can send a run back to planning. |
approved_sensitive_files |
list of exact paths | [] |
Human authorization for named dependency/CI files, effective only when the same exact files appear in the task plan's steps. No globs, absolute paths or traversal. |
For example, authorize pyproject.toml, uv.lock, and
.github/workflows/ci.yml to bootstrap a Python repository. This does not waive
risk approval, protected-file policy, file-count/module bounds or independent
review. Migration and infrastructure findings are never exempted by this list.
See Configure a repository for the finding categories and decisions.
review¶
review:
max_rounds: 3
max_accepted_findings: 5
accepted_risks: [R0, R1]
blocked_categories: [SECURITY, SCOPE]
This is an absolute bound on review-driven repair. After three logical reviews,
the controller can continue low-risk work with a small number of correctness or
compatibility findings. If the remaining findings are not eligible, the run
stops in NEEDS_HUMAN at the same limit. The decision is stored separately
from the Reviewer's report and is bound to the exact reviewed tree. Security,
scope, repair-regression and high-risk findings still require a human.
polish¶
polish:
enabled: true
official_documentation_origins:
- "https://docs.pytest.org"
- "https://docs.python.org"
- "https://nodejs.org"
- "https://packaging.python.org"
- "https://react.dev"
- "https://testing-library.com"
- "https://vite.dev"
- "https://vitest.dev"
- "https://www.typescriptlang.org"
practice_reference_urls:
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/a11y-review.md"
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/component-architecture-review.md"
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/js-fp-review.md"
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/quality-reviewer.md"
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/react-reactivity-review.md"
- "https://raw.githubusercontent.com/bdfinst/agentic-dev-team/52cc5efd1c445e71c55b956837c003911346d7e7/plugins/dev-team/agents/refactor-opportunity-review.md"
| Key | Type | Default | Effect |
|---|---|---|---|
enabled |
bool | true in packaged default/example, false when omitted |
Run at most one post-green Implementer polish attempt, informed by the repository's RepositorySkill and any human overlay. |
official_documentation_origins |
list of HTTPS origins | the nine official documentation origins shown above | Authoritative sources for version-specific claims. Non-empty, unique, at most 25 entries. Each must be an HTTPS origin with no path, credentials, whitespace, query, fragment or trailing slash. |
practice_reference_urls |
list of exact HTTPS URLs | the six curated bdfinst/agentic-dev-team review references shown above, pinned to commit 52cc5efd |
Optional curated general-practice references. Can be empty. Unique, at most 12 entries. Each must be an exact HTTPS document URL (a real path, no trailing slash) with no credentials, whitespace, query or fragment. |
Both lists are the complete fetch allowlist for the skill-generation
Researcher. The curated practice references are pinned to an immutable commit
(52cc5efd1c445e71c55b956837c003911346d7e7) rather than a mutable branch, so
the exact reviewed text is what gets fetched. Re-pin deliberately after
reviewing a newer revision. Official documentation, migration guides and release notes are
authoritative. Curated practice references can only contribute generic quality
heuristics, synthesized rather than copied. They never supply version claims,
commands, tools or orchestration, and the controller validates them by exact
URL rather than by origin.
The class fallback for enabled is false, so legacy configurations that omit
polish retain their previous one-pass behavior. The packaged default and
config/factory.example.yaml explicitly enable it. Omitting only the URL lists
keeps the defaults above.
Polish runs only after the first successful deterministic verification and
scope assessment, before testing and review. When eligible, the controller
re-profiles the post-implementation worktree to capture any dependency change
the task made. It loads the generated RepositorySkill stored for that
repository and dependency_fingerprint under factory.data_dir using the
template <data_dir>/repository-skills/v1/<repository-key>/.... Guidance is
never stored in, or loaded from, the target repository or its worktree.
Only when the current fingerprint has no generated skill does the controller
enter a temporary RESEARCHING state. It calls the configured Researcher
(Claude Opus 5 in the default profile) with purpose
GENERATE_REPOSITORY_SKILL. Invalid typed output or provenance receives one
bounded retry carrying the exact rejection reason. An infrastructure failure
also receives one retry. The calls run in the run's own directory rather than the
worktree, receive only the normalized RepositoryProfile and the two
configured URL lists. They never receive changed filenames, source code,
README content, task prose, or the diff. They have web_fetch as their only
tool, and run without repository custom instructions. An existing
generated file is never overwritten. A dependency change selects a new file and
earlier files remain. There is no TTL. Two concurrent first runs for the
same missing fingerprint can each run the bounded initial-plus-correction
sequence. Publication is atomic and no-clobber, so one winner is kept and
revalidated by both. This costs at most one extra sequence.
The repository key derives from the canonical local Git common directory.
Linked worktrees share one directory. A moved or re-cloned repository gets a
new key with no guidance.
If you want to carry its guidance across, use factory skill path before the move.
The RepositorySkill is bound to the profile's dependency_fingerprint and
carries bounded targets, HTTPS source provenance and separate simplify and
polish guidance. On every load, the controller rejects an invalid skill.
Rejection happens if the fingerprint mismatches, or a target is not in the
profile. It also rejects missing official provenance for detected frameworks,
unknown dependency claims, or sources outside configured lists.
Your own house rules go in a repository-level repository-skill-overlay.yaml
in the same storage, outside the target repository. It holds guidance prose
only (mode: extend|replace plus optional simplify and polish blocks), has
no targets, sources, versions or fingerprints, and survives dependency changes.
The factory never creates, rewrites, normalizes, refreshes or deletes it. An
invalid overlay is preserved, warned about, and ignored while valid generated
guidance still applies. factory skill path, factory skill validate and
factory skill refresh operate on these files explicitly. See
Repository skills and overlays.
Nothing here can fail an already-green run. A failed re-profile, rejected
skill, invalid overlay, or stale guidance records a warning on
repository-profile.json. In those cases, the factory skips or disables
polish. Stored guidance that fails revalidation is left on disk
untouched and its warning points at factory skill refresh.
When guidance is accepted, the controller applies it (simplify first, then
version-specific polish) in one existing bounded worker attempt. That attempt
records AttemptTrigger.POLISH, consumes the implementation budget, can make no
edits, and is always fully verified again. Polish never runs during CI repair
and runs only when one later recovery attempt remains. Each run snapshots the
effective skill, valid overlay, and guidance provenance before agents see it.
Mid-run edits affect later runs only.
pull_request¶
pull_request:
enabled: false
remote: "origin"
base_branch: null
draft: true
allowed_hosts:
- "github.com"
| Key | Type | Default | Effect |
|---|---|---|---|
enabled |
bool | false |
Whether to commit, push and open a pull request. |
remote |
string | origin |
Git remote to push to. |
base_branch |
string or null | null |
Base branch. null means the remote's default branch. |
draft |
bool | true |
Open the PR as a draft. |
allowed_hosts |
list of hosts | ["github.com"] |
The remote's host must be in this list. |
Requires gh on PATH. The factory passes OWNER/REPO to gh, which uses
the currently authenticated account and host configuration. Git transport can
use an allowlisted SSH host alias. Never force-pushes. Merging is
controlled separately.
ci¶
| Key | Type | Default | Effect |
|---|---|---|---|
enabled |
bool | false |
Poll the pull request's checks after creation. |
poll_interval_seconds |
int > 0 | 30 |
Delay between polls. |
max_wait_seconds |
int > 0 | 1800 |
Total polling budget. Not unbounded. |
repair_attempts |
int > 0 | 3 |
CI repair budget, separate from the implementation budget. |
ci.enabled: true requires pull_request.enabled: true. The combination is
rejected otherwise. Requires gh.
ci.enabled: false disables factory observation/repair, not GitHub Actions
workflow triggers.
merge¶
| Key | Type | Default | Effect |
|---|---|---|---|
enabled |
bool | false |
Merge after deterministic verification, independent review and CI pass. |
method |
squash, merge, rebase |
squash |
Normal GitHub merge method, never administrator override. |
allowed_repositories |
list of OWNER/REPO |
[] |
Exact repositories authorized for automatic merging, with no wildcards. |
required_checks |
list of check names | [] |
Every named check must be present and successful on the current PR head. Missing, skipped and pending required checks cannot authorize merging. |
Enabling merging requires PR and CI enabled, pull_request.draft: false, an
explicit pull_request.base_branch, nonempty repository/check allowlists and
nonempty repository.commands.verify. GitHub branch rules must permit the
configured merge method and unattended delivery. Required human reviews are
not bypassed. The configured required checks must also be enforced by the
target's active GitHub protection policy, without a bypass that defeats
the server-side gate. A client-side check list alone cannot prevent a check
rerun racing a merge. The policy must require PRs and up-to-date branches.
Classic protection must enforce administrators. Supported active repository
or organization rulesets must have no bypass actors. Missing or unreadable
enforcement metadata is not treated as approval.
Classic PR bypass allowances for users, teams and apps must be explicitly
empty, and outstanding GitHub-required reviews still block the merge.
Conflicting, outdated or otherwise ineligible PRs stop with an
explicit reason. A run is DONE only after the actual merge is confirmed.
Project delivery requires either all three integrations disabled (local integration) or all three enabled (serial PR-to-target delivery). See Projects.
scheduler¶
scheduler:
enabled: false
poll_interval_seconds: 30
max_concurrent_tasks: 1
stall_timeout_seconds: 900
required_label: "agent-ready"
max_runs_per_day: 20
| Key | Type | Default | Effect |
|---|---|---|---|
enabled |
bool | false |
Whether factory start can run at all. |
poll_interval_seconds |
int > 0 | 30 |
Backlog poll interval. |
max_concurrent_tasks |
1 or 2 |
1 |
Concurrent runs. Validated, and higher values are rejected. |
stall_timeout_seconds |
int > 0 | 900 |
Idle time before a run is treated as stalled. Also the default staleness threshold for factory status. |
required_label |
string | agent-ready |
Issues must carry this label to be dispatched. |
max_runs_per_day |
int > 0 or null | 20 |
Runs that can be claimed per UTC calendar day. null disables the cap. |
max_runs_per_day is counted from persisted run timestamps, so it survives a
restart. It is a cost bound: scheduler.enabled and --runtime copilot are
independent knobs, and a daemon with the real runtime can spend at whatever
rate the backlog allows.
Requires gh.
risk¶
risk:
R0: { human_approval: false }
R1: { human_approval: false }
R2: { human_approval: true }
R3: { human_approval: true }
All four levels must be defined. human_approval: true stops a run of that risk
level at NEEDS_HUMAN instead of proceeding automatically.
Risk selects governance. Complexity selects model strength. They are separate:
a one-line change can be R3, and a hard change can be R0.
Cross-field validation¶
The loader rejects a configuration when:
retries.max_total_attemptsis less thanretries.same_model_attemptsmodels.workersdoes not define exactlyL0,L1,L2andL3models.reviewer's model family matches any worker's model familyriskdoes not define exactlyR0,R1,R2andR3ci.enabledis true whilepull_request.enabledis falsescheduler.max_concurrent_tasksis greater than2- any key is not recognized