Architecture¶
Objective¶
Build a local autonomous software engineering factory that can eventually process backlog work through the entire SDLC.
The implemented pipeline is:
project brief (optional)
↓
typed smallest-sufficient task DAG
↓
task (generated, manual or GitHub issue)
↓
prepare worktree
↓
deterministic repository profile
↓
triage
↓
refine
↓
research (only when triage asks for it, at most once)
↓
plan
↓
implement
↓
deterministic verification (install → verify → build)
↓
scope-drift governance
↓
one bounded implementer polish pass (when enabled)
↓
deterministic verification again
↓
scope-drift governance again
↓
independent tester
↓
independent reviewer
↓
ready for PR
↓
pull request (opt-in: pull_request.enabled)
↓
CI observation (opt-in: ci.enabled)
↓
bounded CI repair
↓
confirmed merge (opt-in: merge.enabled)
↓
done
Everything after "ready for PR" is strictly opt-in. With the packaged
configuration a run performs no network access at all and completes at
PR_READY.
The optional fast mode applies only to low-risk L0 and L1 work. It can use
a faster Refiner and Planner profile. It skips the optional polish pass.
Deterministic verification, the independent Tester, and the independent
Reviewer remain mandatory.
Still out of scope (deferred Phase 15 items): - staging (15.3) - deployment/promotion (15.4) - Jira, Postgres, Temporal, Docker/Kubernetes workers, remote workers
Implemented (requested Phase 15 sub-phases, see PLAN.md):
- 15.0 factory CI for this repository
- 15.1 tag-driven release of native macOS artifacts
- 15.2 macOS runtime packaging and an opt-in user launchd service
- 15.5 local monitoring and health (factory doctor, factory status)
- 15.11 a read-only, loopback-only local dashboard (factory dashboard)
Phase 16 is also implemented: deterministic repository capability profiling and an optional bounded post-green polish pass.
Those phases make the factory installable, observable and inspectable on one MacBook. Phase 18 adds opt-in autonomous delivery under ADR-022. Only the controller can merge an allowlisted, independently reviewed PR after required checks pass for its exact head. Production execution and deployment remain out of scope.
The project path is also implemented. The command factory project invokes the
Planner with purpose DECOMPOSE_PROJECT. It validates a task DAG of at most
12 items, optionally creates GitHub issues, and executes ready tasks. It does
not add a second SDLC state machine: project state is limited to planning,
execution and aggregate outcome, while every child remains an ordinary
FactoryRun.
With merge.enabled, project tasks execute serially through PR publication,
bounded CI repair and confirmed merging. The project fetches and
fast-forwards its own integration worktree to the target branch before each
task, so dependent tasks start from merged predecessors. It never advances
the user's source checkout. The default local-only mode retains wave
concurrency and cherry-pick integration.
High-level architecture¶
Task Source
│
▼
Workflow Controller
│
┌──────────┼──────────┐
│ │ │
Policy Routing Run Store
│ │ │
└──────────┼──────────┘
│
▼
Agent Runtime
│
┌──────────┼────────────┬────────────┐
▼ ▼ ▼ ▼
Claude Gemini GPT MAI
│
▼
Local Workspace
│
Git Worktree + Shell
│
▼
deterministic verification
Later:
│
▼
GitHub
│
▼
GitHub Actions
Domain concepts¶
WorkItem¶
Represents the software task.
Minimum properties:
id
external_id
source
title
description
acceptance_criteria
constraints
labels
priority
complexity
risk
Initial sources:
Jira comes later.
Controlled writing¶
All model-authored prose passes a controller-owned writing policy before the
factory accepts the artifact. The policy uses selected deterministic checks
from SimpleEnglish revision
61ee200efbd423050aab982eed94226229891ae0.
The policy checks:
- Sentence length
- Total words in bounded artifact fields
- Filler terms
- Semicolons
- Em dashes
- Latin abbreviations
The factory gives one bounded correction prompt when the role supports typed output correction. The controller records each invocation. It never silently rewrites facts.
Issue bodies, pull request bodies, titles and commit messages pass the same checks before Git or GitHub mutation. Project child work items contain only the task description. They do not repeat the full project brief in every agent prompt.
The policy preserves uncertainty, identifiers, paths, commands, URLs and quoted errors. It follows ASD-STE100 principles but does not prove formal compliance.
FactoryRun¶
One execution of a WorkItem.
Suggested properties:
id
work_item_id
state
attempt_records
workspace_path
branch_name
created_at
updated_at
last_activity_at
lease
completed_at
failure_reason
commit_sha
pull_request_url
attempt_records is the durable retry budget: every implementation, polish or
repair attempt appends exactly one record carrying its budget
(IMPLEMENTATION/CI_REPAIR) and triggered_by reason. Attempt numbers are
always derived from this persisted list, never from an in-process counter, so a
restart cannot grant a run a fresh budget.
lease records the host/pid currently executing the run, and
last_activity_at is refreshed on every transition so the scheduler can detect
a stalled run without inspecting lock files.
Delivery records base_commit_sha, reviewed_tree_sha and
reviewed_commit_sha. Before pushing, the controller persists
pending_commit_sha: the exact commit it created from the approved tree and
authorized parent. Recovery uses that receipt even after a crash before the
branch ref advances, rather than inferring approval from local history. The
receipt is cleared only after successful publication records the published
commit.
ProjectBrief and ProjectPlan¶
ProjectBrief is the optional high-level intake above WorkItem.
ProjectPlan is one immutable, typed decomposition containing a flat list of
ProjectTask objects. Task ids are contiguous positive integers and
dependencies can reference earlier ids only, which guarantees an acyclic graph
without a graph framework.
The project planner must choose the fastest sufficient delivery approach. It uses one task per reviewable pull request and reuses existing mechanisms. Split only for independent capabilities, hard prerequisites, or safe parallel work. A shared product goal or safety boundary is not sufficient reason to pack several capabilities into one issue. Tests, documentation, setup and cleanup stay with their functional outcome rather than becoming process-only issues.
Deterministic plan validation bounds task acceptance criteria and rejects an oversized single-task description before issue creation or implementation. The planner receives one bounded correction attempt with the rejection reason. Dependencies are merge-before-start gates in remote delivery and integration-before-start gates locally. Dependency-free ready tasks can run in parallel isolated worktrees within the configured concurrency cap.
Each child WorkItem also carries a factory-generated boundary naming the
outcomes assigned to sibling tasks. Task planners, replanners and implementers
must not pull those outcomes forward. Execution plans use concrete
repository-relative path prefixes in expected_scope.modules. Prose labels,
globs and traversal are rejected before implementation. Small supporting files
beside the planned paths are allowed when the implementation also changes its
planned area. The plan's file-count range is advisory. The factory-owned
repository.max_changed_files setting remains the hard publication ceiling. A
completely mismatched path set triggers bounded metadata replanning, while
migrations, infrastructure, and unapproved dependency or CI changes stop
immediately.
When an implementation is already deterministically green but its scope metadata is inaccurate, the bounded scope replan updates the ExecutionPlan and re-assesses the existing diff. It does not rerun the Implementer or discard verified work. The initial and revised plans are retained as audit evidence. They do not require prediction of every incidental file. Independent Tester and Reviewer gates still evaluate the resulting change before publication.
ProjectExecution is mutable coordination evidence stored separately from the
immutable brief and plan. The factory derives its outcome from child
FactoryRun states, integration results, and one final deterministic
verification of the fully composed integration branch. An agent cannot declare
the project complete. In merge mode, every child must include confirmed merge
evidence in the fetched target history. Explicit factory project --resume
reconciles persisted child identifiers and delivery checkpoints without
replanning, PR recreation, or budget resets. Ambiguous interrupted agent
work stops at NEEDS_HUMAN.
Workflow states¶
The implemented SDLC states are exactly:
CREATED
TRIAGING
REFINING
RESEARCHING
PLANNING
IMPLEMENTING
VERIFYING
REVIEWING
PR_READY
PR_CREATED
CI_RUNNING
CI_DIAGNOSIS
DONE
NEEDS_HUMAN
FAILED
There is no REPAIRING, PLAN_READY or BLOCKED state. Repair is a
bounded transition back to IMPLEMENTING (or back to PLANNING for scope
drift). A blocked task enters NEEDS_HUMAN with a recorded reason.
There is also no POLISHING state or POLISHER role. When enabled, an
eligible polish attempt transitions VERIFYING → RESEARCHING if no
RepositorySkill exists. It then transitions RESEARCHING → IMPLEMENTING with
trigger POLISH, followed by normal VERIFYING. When reusable guidance
already exists, no research call is made. RESEARCHING remains a temporary
transition, not a new role. If research or validation fails, the run stays on
its green path. The reason is recorded as a profile warning, and the
controller transitions to REVIEWING.
The workflow controller owns every transition. The full table is declared as
data in workflow.ALLOWED_TRANSITIONS and enforced on every call:
CREATED → TRIAGING
TRIAGING → REFINING
REFINING → RESEARCHING | PLANNING
RESEARCHING → PLANNING | IMPLEMENTING
PLANNING → IMPLEMENTING
IMPLEMENTING → VERIFYING
VERIFYING → REVIEWING | IMPLEMENTING | PLANNING | RESEARCHING
REVIEWING → PR_READY | IMPLEMENTING
PR_READY → PR_CREATED
PR_CREATED → CI_RUNNING | DONE
CI_RUNNING → DONE | CI_DIAGNOSIS
CI_DIAGNOSIS → IMPLEMENTING
Every non-terminal state can additionally escalate to NEEDS_HUMAN (a business
decision: eligibility, risk, scope, exhausted budget, non-repairable CI) or
FAILED (an operational agent/infrastructure failure).
Terminal states are DONE, NEEDS_HUMAN and FAILED.
PR_READY is not terminal. When pull requests are enabled it continues to
PR_CREATED. When they are disabled it is the completed endpoint of the manual
flow and the controller finalizes it explicitly by stamping completed_at.
workflow.is_run_finished is the single predicate that expresses this, and the
scheduler uses it rather than a raw state comparison.
Scheduling state¶
Scheduling ownership is separate from detailed SDLC state. Scheduler owns
reservations, task order, bounded concurrency, and stall detection entirely
in-memory. It never mutates a FactoryRun. FactoryService composes it with
GitHubIssueProvider and WorkflowController, and dispatches through a thread
pool bounded by scheduler.max_concurrent_tasks (1 or 2).
Two configured bounds are enforced, and both are supplied by the composition root rather than assumed by the scheduler:
scheduler.max_concurrent_tasks how much may run at once (1 or 2, in memory)
scheduler.max_runs_per_day how much may be claimed (per rolling UTC
per day day, counted from
persisted runs)
The daily ceiling is counted from persisted FactoryRun.created_at timestamps,
so it survives a restart instead of resetting with the process. A tick stopped
by it reports rate_limited rather than looking like an empty backlog, and
reconciliation of already-running work is unaffected.
Recovery is conservative: a persisted, non-terminal run left behind by a dead
process is transitioned to NEEDS_HUMAN through the controller, never
auto-resumed. No paid retry is spent, the persisted budget is untouched, and the
workspace plus artifacts stay on disk.
Potential concepts:
Avoid conflating: - what SDLC step is happening - whether the scheduler owns this task
Artifacts¶
RepositoryProfile¶
Produced deterministically after the worktree is prepared and before
TRIAGING, and again before an eligible bounded polish attempt. It records:
detector_version
manifest_fingerprint
dependency_fingerprint
markers
version_files
technologies
test_tools
package_managers
dependencies
warnings
The profiler walks repository-local paths, prunes generated/vendor directories and reads only an allowlist of bounded manifests. It never uses a shell, network or imports.
Each dependencies entry is a direct declaration with exact evidence:
ecosystem, name, declared version, an optional exact
resolved_version/resolution_path, manifest path and dependency group. The
parsed manifests are:
| Manifest | Ecosystem | Recorded as |
|---|---|---|
pyproject.toml (project.dependencies, project.optional-dependencies.*, dependency-groups.*) |
Python | one declaration per requirement, grouped by table. requires-python becomes the python runtime target |
pyproject.toml (tool.poetry.dependencies, tool.poetry.dev-dependencies, tool.poetry.group.*.dependencies) |
Python | one declaration per entry, grouped by table. Also marks the poetry package manager |
requirements.txt, requirements-*.txt |
Python | one declaration per requirement in group requirements. Marks the pip package manager |
setup.cfg, tox.ini |
Python | technology and pytest evidence only, with no versions |
package.json (dependencies, devDependencies, peerDependencies, optionalDependencies, packageManager) |
npm | one declaration per entry, grouped by table |
Exact versions are resolved from uv.lock, package-lock.json and
pnpm-lock.yaml when unambiguous. An ambiguous resolution records a warning
instead of a version. poetry.lock, yarn.lock, bun.lock/bun.lockb,
Pipfile.lock and pylock.toml mark their package manager where applicable
and are fingerprinted as version_files, but exact graph parsing is not
claimed for them.
Two SHA-256 fingerprints are recorded and they are not interchangeable:
dependency_fingerprintis semantic. It digests the detected technologies, test tools, package managers and normalized dependency declarations. It is the identity a generated skill is stored and reused under.manifest_fingerprintis provenance. It digests the content of everyversion_filespath (package.json,pyproject.toml, requirements files and lockfiles). Formatting or comment-only manifest edits change it without invalidating a skill.
There is no fixed built-in skill catalog and no repository-provided plugin system. See RepositorySkill below for how version-specific guidance is generated, reused and customized.
RepositorySkill¶
Generated for the repository as a whole, not selected from a catalog, and not
scoped to one task's changed files. Guidance is used only when polish.enabled
and the bounded polish attempt is eligible.
Storage and reuse¶
Generated skills are stored under factory.data_dir in repository-scoped
storage, keyed by the canonical local repository identity and the profile's
dependency_fingerprint. Storage follows the template:
They are never written into the target repository or its worktree, and the
factory never auto-loads a skill from the target repository. Use
factory skill path --repo PATH to discover the real paths.
The repository key derives from the local Git common directory. All linked worktrees of one checkout share a skill directory. No remote URL is consulted. Moving or re-cloning a repository selects a new key with no guidance. Guidance at the old path is neither followed nor deleted. A human can copy the directory or recreate guidance deliberately.
A normal run reuses guidance instead of researching it:
- a generated skill matching the current
dependency_fingerprintis loaded and reused - generation runs only when the current fingerprint has no generated skill
- an existing generated file is never overwritten
- every load is validated in full (schema, agreement with the current profile, and every cited source against allowlists), not only at generation time
- a changed
dependency_fingerprintselects a new generated file. Earlier files remain on disk - there is no TTL and no time-based expiry
Reuse bounds research per fingerprint, not per process. Two concurrent first runs for the same missing fingerprint can each make one bounded generation sequence: an initial Researcher call and one retry after failure. Invalid output or provenance carries its exact bounded rejection reason into the retry. An infrastructure failure receives one ordinary retry. Publication is atomic and no-clobber, so one result wins, the other run loads the winner, and both revalidate the winner in full before using it. The race costs at most one extra sequence (two calls). Correctness, stored state and the overlay are unaffected.
Generation¶
When generation is required, the controller re-profiles the worktree and enters
a temporary RESEARCHING state. It calls the Researcher (Claude Opus 5 by
default) with purpose GENERATE_REPOSITORY_SKILL. Any failure gets one bounded
retry. Invalid typed output or provenance includes the exact bounded rejection
reason so the Researcher can correct it. A second failure safely skips polish.
That invocation is web-only and deliberately blind to the repository. It runs
with the run's own persistence directory as its working directory, not the
worktree, and its only tool is web_fetch. Repository custom instructions are
disabled for it. It receives the normalized RepositoryProfile, the two
configured URL lists and the factory-owned generation rules. It never receives
changed filenames, source code, README content, task prose or the diff. It can
fetch only:
polish.official_documentation_origins: official documentation, migration guides and release notes. These are authoritative for every version claim.polish.practice_reference_urls: exact curated general-practice references (by default the reviewedbdfinst/agentic-dev-teamnotes, pinned to commit52cc5efd, not a mutable branch). They can contribute generic quality heuristics only, synthesized rather than copied, and never version claims, tools, commands or orchestration.
It returns one typed artifact, persisted as repository-skill.json in the
generated storage and snapshotted into the run:
generator_version
dependency_fingerprint
generated_at
targets
official_sources
practice_sources
simplify
polish
uncertainties
targets are bounded package/runtime versions with evidence paths.
official_sources and practice_sources are HTTPS citations from the
respective configured lists. Each names, in applies_to, the detected
dependencies it grounds, and a practice source can instead use the single
generic marker repository. simplify and polish are each a bounded
SkillGuidance (summary, guidance, things to avoid, validation). The model
itself refuses a skill that has neither an official source nor an explicit
uncertainty, and refuses an official source claiming generic applicability.
The controller then validates the artifact deterministically (on generation and on every later load) and rejects it when:
- its
dependency_fingerprintdoes not match the profile it was generated from, - a target is not an exact profiled dependency declaration (ecosystem, name, declared version, resolved version),
- target evidence paths are not profile
version_files, manifest paths or resolution paths, - a detected
python,pytest,react,react-dom,viteorvitestdependency has no target, or is not named by theapplies_toof at least one accepted official source, - a source claims applicability to a dependency the profile did not detect, or
- a cited source falls outside
polish.official_documentation_origins(compared by origin) or is not an exactpolish.practice_reference_urlsentry.
Rejection never fails an already-green run. The controller appends the reason
to the persisted profile's warnings. It skips polish, and the run continues
to testing and review. When a stored generated skill fails revalidation, a
warning names the file. The file stays as written. Run factory skill refresh
to replace it. The same rule applies when the re-profile fails.
Before testing and review, the controller re-profiles once more.
It disables the skill if profiling fails.
It also disables the skill if the fingerprint changed after guidance loading.
The controller records the reason as a profile warning.
Human overlay¶
Human customization is a separate repository-level
repository-skill-overlay.yaml, kept in the same repository-scoped storage
outside the target repository. It is guidance prose only:
It declares no targets, sources, versions or fingerprints, so it is not bound
to a dependency state and survives dependency changes. extend adds the
overlay's guidance to the generated guidance. replace makes the overlay's
blocks the guidance for the sections it provides. The factory never creates,
rewrites, normalizes, refreshes or deletes this file. An invalid overlay is
preserved exactly as written, recorded as a warning and ignored for that run,
while valid generated guidance can still apply.
Three commands support guidance.
factory skill path --repo PATH discovers the generated and overlay paths.
factory skill validate --repo PATH validates current files without changes.
factory skill refresh --repo PATH [--runtime fake|copilot] refreshes generated
guidance only. It is the only command that can replace generated guidance.
The read-only dashboard has no skill or overlay write path.
Per-run snapshots¶
Before any agent consumes guidance, the run stores create-once snapshots:
repository-skill.json the effective guidance actually used
repository-skill-overlay.json the overlay exactly as read, when valid
repository-skill-use.json provenance: repository key, dependency
fingerprint, selection source, overlay mode and
whether it applied, and content hashes
The provenance record carries hashes and selection facts rather than guidance text, so the audit trail stays small and comparable across runs. Human edits made while a run is in flight therefore affect later runs only.
The effective guidance reaches only the polish Implementer, Tester and Reviewer, and is never available before the initial green baseline. It is advisory and cannot alter tools, models, workflow states, retry budgets, quality gates, commands, permissions, dependencies or scope.
TriageResult¶
Fields approximately:
factory_eligible
complexity
risk
requirements_quality
needs_research
dependencies
unknowns
confidence
Specification¶
Fields approximately:
Unknown information must remain explicit.
Do not silently invent requirements.
ResearchReport¶
Only produced when necessary.
Fields approximately:
ExecutionPlan¶
Fields approximately:
summary
steps:
- id
- goal
- likely_files
- validation
expected_scope:
modules
estimated_files_min
estimated_files_max
test_strategy
risks
ChangeSet¶
Fields approximately:
changed_files and the actual Git diff are derived by the controller from the
workspace. They are not trusted agent claims. Git evidence must include
untracked files.
VerificationReport¶
Deterministic, factory-produced evidence only. Nothing in it is an agent claim.
Fields approximately:
TestReport¶
Independent AI tester judgement, deliberately a separate artifact so a
model's opinion can never be mistaken for deterministic evidence. passed is
advisory: gating still uses the VerificationReport.
Fields approximately:
CIReport¶
Normalized, persisted CI evidence (ci.json). Produced by the controller from
gh pr checks output. It is expressed with plain strings so the domain layer
has no dependency on the gh adapter.
Fields approximately:
overall
checks:
- name
- status
- description
- details_url
- failure_category
- log_excerpt
observed_at
repair_attempts_used
timed_out
ReviewReport¶
Fields approximately:
approved
suggested_changes
blocking_findings:
- category
- message
- locations:
- path
- start_line
- end_line
prior_finding_dispositions:
- finding_id
- status: RESOLVED | UNRESOLVED | WITHDRAWN
- rationale
repair_regressions:
- category
- message
- locations
The old string blocker fields remain readable for historical artifacts, but
new Reviewer output must use the typed fields. The controller assigns stable
finding ids and persists the open set in the run's ReviewLedger. Every
implementation attempt that reaches review also records the immutable tree
that was reviewed. After review.max_rounds logical reviews, the controller
can persist a separate, tree-bound ReviewAcceptance and continue with
explicit findings. It never changes ReviewReport.approved to claim approval
the Reviewer did not give.
Complexity model¶
L0¶
Mechanical work: - formatting - lint - straightforward Sonar finding - simple rename - trivial CSS adjustment - obvious duplication - simple type error
Default worker: MAI-Code-1.1-Flash
L1¶
Normal isolated task.
Default: Gemini 3.8 Flash
L2¶
Examples: - cross-module change - difficult defect - significant new functionality - complicated integration behavior
Default: Claude Sonnet 5
L3¶
Examples: - architecture - unfamiliar subsystem - large ambiguity - repeated failures
Default: Claude Opus 5
Potentially invoke research first.
Risk model¶
R0¶
Examples: - documentation - formatting - harmless refactor - visual-only adjustment
R1¶
Normal application behavior.
R2¶
Examples: - authentication - authorization - database migration - public API - security-sensitive behavior - dependency changes
R3¶
Examples: - production infrastructure - secrets - destructive migration - deployment control - critical security behavior
Risk controls required gates.
It does not directly select the worker model.
Initial agents¶
Triage¶
Model: GPT-5.6 Terra
Permissions: - repository read
Output: TriageResult
Specification Refiner¶
Model: GPT-5.5
Permissions: - repository read
Output: Specification
Researcher¶
Model: Claude Opus 5
Invoke only when required.
Permissions: - repository read - research capability
Output: ResearchReport
The same role also serves the GENERATE_REPOSITORY_SKILL purpose when an
eligible polish attempt finds no reusable generated skill for the current
dependency fingerprint. That invocation has no repository read. It runs in the
run directory and has only web_fetch.
Configured official documentation origins and curated practice references
restrict that tool. The call sees only the normalized profile and source lists.
It returns a RepositorySkill.
Planner¶
Model: Claude Opus 5
Permissions: - repository read - read-only commands where useful
No source modifications.
Output: ExecutionPlan
Implementer¶
Model selected by complexity.
Permissions: - assigned workspace - repository edit - shell - tests
Output: ChangeSet
Receives effective repository guidance only during the optional post-green polish attempt. It applies simplification first and version-specific polish second. The initial implementation attempt receives none.
Tester¶
Model: Gemini 3.8 Flash
Receives:
- WorkItem brief
- Specification
- ExecutionPlan
- controller-derived diff and changed files
- deterministic VerificationReport
- read-only repository access
The implementer's ChangeSet (including its summary) is never provided: the
tester sees only controller-derived Git evidence plus deterministic results.
Receives the same post-green repository guidance as the polish Implementer, once loaded or generated and while still current. Receives none before that point, and none when the guidance was disabled as stale.
Output: TestReport
Reviewer¶
Model: GPT-5.6 Sol
Receives:
- WorkItem brief
- Specification
- ExecutionPlan
- controller-derived diff and changed files
- deterministic VerificationReport
- independent TestReport
- implementation snapshot number
- typed, controller-owned open Reviewer findings from this run
- the exact Git diff since the previous reviewed tree during repair review
- read-only repository access
Never receives the implementer's ChangeSet summary.
Receives the same post-green repository guidance as the polish Implementer, once loaded or generated and while still current. Receives none before that point, and none when the guidance was disabled as stale.
Checks: - correctness - requirements - edge cases - regression risk - security - unnecessary complexity - maintainability - API compatibility - scope drift
Output: ReviewReport
The first review establishes the initial blocker set. A repair review is targeted. It must explicitly disposition every open finding. New defects caused by the repair remain blocking and join the ledger. A newly noticed older defect can expand repair scope in one bounded adoption round. Later drip-fed findings are recorded as advisory rather than consuming every implementation attempt. Overlapping locations keep an earlier finding unresolved even if the Reviewer rephrases it.
The controller derives repair approval from the ledger. It does not trust the
Reviewer's approved boolean during a repair. Low-risk correctness or
compatibility findings can be accepted after the configured review-round or
attempt limit. The acceptance is persisted in review-acceptance.json, bound
to the exact reviewed tree, and shown in the pull request. Security, scope and
repair-regression findings, high-risk work, excessive findings and complete
blocker-replacement loops still stop in NEEDS_HUMAN with
review-impasse.json.
Failure Investigator¶
Model: Claude Opus 5
Not part of happy-path V1.
Later invoked after repeated implementation or CI failures.
Model router¶
Model routing must be deterministic configuration.
Agents can recommend:
but the controller maps:
Do not let arbitrary agent output choose arbitrary models.
Policy engine¶
Do not build a large policy framework in V1.
Start with explicit functions/configuration.
Eventually policies answer questions such as:
may_run_task(...)
required_checks(...)
should_research(...)
can_retry(...)
should_escalate(...)
requires_human(...)
may_create_pr(...)
Keep business policy outside prompts.
Retry policy¶
Initial proposal:
same implementation model attempts: 2
maximum total implementation and repair attempts: 6
later CI repair attempts: 3
Escalation:
Every entry into IMPLEMENTING appends one attempt record. Verification,
review and the optional post-green polish consume the same monotonic
implementation budget so no path can evade the limit. Polish runs at most once,
only after the first successful deterministic verification, never during CI
repair, and only when one later recovery attempt remains available. It can make
no edits. Deterministic verification and scope assessment always run again.
Actual limits belong in configuration.
Review repair has additional convergence bounds. One repair review can adopt a
batch of late findings. The packaged policy allows three logical reviews and
at most five accepted findings for R0 and R1. At that limit, eligible
findings become explicit review debt and the run continues. Security, scope,
repair regressions, R2/R3, and two consecutive complete blocker-replacement
cycles are not eligible. When acceptance is unsafe, the controller writes
review-impasse.json and stops in NEEDS_HUMAN.
Local workspace¶
Base directory:
Suggested layout:
~/.software-factory/
├── projects/
│ └── PROJECT-ID/
│ ├── project-brief.json
│ ├── project-plan.json
│ ├── execution.json
│ └── logs/
├── runs/
│ └── RUN-ID/
│ ├── run.json
│ ├── work-item.json
│ ├── repository-profile.json
│ ├── triage.json
│ ├── specification.json
│ ├── research.json
│ ├── execution-plan.json
│ ├── change-set.json
│ ├── patch.diff
│ ├── verification.json
│ ├── test-report.json
│ ├── review.json
│ ├── review-acceptance.json
│ ├── review-impasse.json
│ ├── ci.json
│ ├── logs/
│ └── attempts/
│ └── NN/
│ ├── change-set.json
│ ├── patch.diff
│ ├── verification.json
│ ├── test-report.json
│ ├── review.json
│ ├── review-acceptance.json
│ └── review-impasse.json
└── workspaces/
└── TASK-ID/
└── repository worktree
Persistence¶
V1 uses filesystem persistence.
Provide a small RunStore interface with behavior conceptually similar to:
- save_run()
- load_run()
- list_runs()
- save_artifact()
- load_artifact()
Initial implementation: FileRunStore
A future implementation is possibly: PostgresRunStore
Do not implement a database until needed.
Writes are atomically replaced and versioned because filesystem data is the V1 recovery source of truth.
Workspace abstraction¶
Provide something conceptually like WorkspaceProvider.
Operations: - prepare() - get_path() - diff() - cleanup()
Initial implementation: GitWorktreeWorkspace
Do not build generic remote-worker abstractions yet.
Workspaces use sanitized, root-contained paths and are preserved by default. Cleanup must refuse paths outside the configured workspace root. A short-lived exclusive lock prevents simultaneous ownership of the same work item.
Agent runtime abstraction¶
Conceptually:
AgentRequest includes the role, configured model and reasoning level, typed
context, assigned workspace path where applicable, and a timeout.
Production runtime: CopilotAgentRuntime (--runtime copilot). It builds a
role-scoped prompt, runs the copilot CLI with constrained tools, and validates
one typed artifact from the final response. Malformed output is an explicit agent
failure, never a silent pass. Planner, Tester and Reviewer schema failures get
bounded same-model correction with the exact validation reason. Tester and
Reviewer corrections do not spend implementation attempts.
Default runtime: FakeAgentRuntime (--runtime fake). It is the CLI default so
no command can make a paid call by accident, and it is the only runtime the test
suite uses.
The domain and workflow layers must not depend on Copilot-specific SDK objects.
Fake agents¶
Fake agents are deterministic test doubles.
They allow tests such as:
without: - paid model calls - network - nondeterminism
Keep them simple.
Local verification¶
Repository configuration defines commands.
Example:
The factory runs deterministic checks after implementation.
When polish.enabled is true, verification is followed by at most one
IMPLEMENTER polish attempt. That attempt uses guidance for the current
dependency fingerprint and any human overlay. A bounded web-only research call
happens only when that fingerprint has no generated guidance yet. The full
deterministic verification and scope assessment then run again before the
tester and reviewer. If research or guidance validation fails, polish is
skipped with a recorded warning and the already-green run proceeds unchanged.
The packaged default and example enable polish. A legacy configuration that
omits the section uses the model fallback of false.
The small command runner is part of Phase 1. Empty command lists pass. This behavior keeps repositories usable before they add factory-specific configuration.
Scope drift¶
Compare plan expectations with actual Git diff.
Deterministically detect at least: - files outside expected modules - dependency file changes - migration creation - CI/workflow modification - infrastructure modification
The execution plan's file-count range remains useful planning metadata but is not a scope gate. The configured repository changed-file ceiling is enforced separately before publication.
Later add: - public API detection - authentication/authorization changes
Unexpected scope causes REPLAN or NEEDS_HUMAN depending on risk.
Assessment runs after deterministic verification passes and before the tester,
reviewer or any publishing. REPLAN returns the run to PLANNING at most
scope_drift.max_replans times (counted from persisted attempt records
triggered by SCOPE), then escalates. The risk/sensitive-scope gate is
re-evaluated at the PR boundary, together with a deterministic publish gate
enforcing repository.max_changed_files and repository.protected_file_patterns.
The controller also compares the Git tree before and after repository
verification. Any generated, staged or rewritten file returns the run to
IMPLEMENTING with explicit repair context before Tester or Reviewer runs.
Verification-generated paths must be removed or made intentionally part of the
implementation. Verification itself must not silently change the reviewed tree.
Git ownership¶
Agents edit files.
Controller owns: - worktree creation - branch creation - commit - push - PR creation
Branch push tolerates one transient Git network or remote-backend failure. Before retrying, the publisher reads the target branch tip. It accepts a push only when that tip matches the expected commit. Authentication, authorization, policy and non-fast-forward failures are not retried.
PR creation separately tolerates one transient GitHub CLI or network failure. Before retrying, the publisher looks up repository, head, base, and run marker. This recovers a successful remote request and prevents duplicate PR creation.
Agents must not directly push protected branches.
Initial branch naming:
GitPublisher never force-pushes, never merges, never mutates repository
configuration or remotes (only git remote get-url is permitted), and refuses
remotes whose host is outside pull_request.allowed_hosts. A CI repair pushes
an additional normal commit to the same branch. It updates the existing PR
rather than creating a new one.
PullRequestMerger is a separate, opt-in controller boundary. It requires an
explicit target branch, repository allowlist, named required checks and local
verification commands. It checks the PR identity, current head, current checks
and merge eligibility, uses an expected-head merge guard, and confirms the
merged commit before DONE. It never overrides branch protection or turns a
pending merge request into a success claim.
GitHub credentials are read from the controller's own environment and handed to
gh through the child environment only. CopilotAgentRuntime independently
removes GH_TOKEN, GITHUB_TOKEN, and related credential variables from every
agent subprocess, so no agent ever sees them.
Command surface¶
One CLI, with an explicit split between commands that can change something and commands that cannot:
factory --version version only, no side effects
factory run mutates: creates a run, a worktree, artifacts
factory start mutates: dispatches runs (opt-in scheduler)
factory runs / show read-only
factory doctor read-only apart from the data-dir write probe
factory status read-only (does not create the data dir)
factory dashboard read-only server, explicit and blocking
factory service install mutates: one per-user LaunchAgent plist
factory service status read-only
factory service uninstall mutates: removes that plist only
run, start and dashboard attach the bounded structured log under
<data_dir>/logs once configuration and the data directory are resolved. The
dashboard token is printed to stdout and never logged.
Exit codes are uniform. Code 2 indicates an invalid configuration, a missing
prerequisite, or refused installation. Code 1 indicates an unsuccessful run,
unknown run id, or doctor check error.
Observability¶
Record every agent invocation: - run_id - role - model - reasoning - context tier - started_at - completed_at - duration - attempt - result - token usage if available - cost if available
Record task metrics: - first-pass success - total attempts - human intervention - time to ready-for-PR - review findings - final status
JSON + structured logs are the substrate. Structured logs are written locally, bounded in size, inside the configured data directory, with the same credential redaction already applied to captured command output. Nothing is exported: no telemetry backend, no exporter, no network egress.
The Copilot runtime requests its usage-output file and persists the reported
input, output, thought, and cache token counts, nano-AIU, premium-request cost
and timing fields. An unreported value stays unknown and is never defaulted to
zero. Persisted telemetry remains in raw runtime units. The dashboard can derive
an AI usage value in USD from nano-AIU for display. It does not reconstruct an
invoice from a price table (ADR-017). These invocation records are separate from
implementer AttemptRecords, so telemetry cannot alter retry budgets.
Health and metrics¶
Persisted run artifacts are the source of truth, so health and metrics are derived on demand rather than accumulated. There is no counter store and no time-series database.
Metrics are pure functions over the run store:
runs by final state
first-pass success rate
attempts per run
scope replans
CI repair cycles
escalations to NEEDS_HUMAN
stage and run durations
factory status renders both surfaces (human-readable or --json) and is
strictly read-only: it will not even create the data directory.
Health reports operational facts about this machine:
factory doctor
data directory writable
configuration valid
git available
prerequisites present for enabled features only
factory status / dashboard
stale work-item locks
orphaned worktrees
non-terminal runs left behind by a dead process
factory service status
launchd service registered / not registered
Both are strictly read-only. They report a stale lock, an orphaned worktree or an abandoned run as findings. Repairing one remains an explicit operator action through the controller, exactly as in ADR-011.
Delivery and packaging¶
Delivery ends at a published release artifact. A v* tag builds a GitHub
Release. It does not install, restart, promote or self-update anything
(ADR-015).
version tag
↓
release quality gate (format + lint + types + tests + dependency audit)
↓
build + validate distributions and native macOS artifacts
↓
attest public-repository artifacts
↓
GitHub Release (workflow refuses to replace an existing one)
↓
human downloads and extracts
The release workflow fails if the tag's release already exists, so a re-run
cannot replace published artifacts. GitHub release immutability is enabled for
new releases, and existing releases from v0.3.0 onward report
immutable=true. Older historical releases remain mutable through the
platform. SHA256SUMS and build-info.json still let a consumer verify the
downloaded artifact and its build provenance.
A release contains:
software-agent-factory-<version>-macos-arm64.tar.gz PyInstaller onedir
software-agent-factory-<version>-macos-x86_64.tar.gz PyInstaller onedir
software_agent_factory-<version>-py3-none-any.whl
software_agent_factory-<version>.tar.gz
SHA256SUMS
build-info.json
The two macOS archives are built natively on their own runners. There is no
universal2 build.
Artifacts are unsigned or ad-hoc signed. Developer ID signing and notarization are deferred, so Gatekeeper quarantine is a documented, expected condition and release notes must explain it.
A frozen runtime bundles Python and the factory, not the toolchain:
required always git
required if enabled gh (pull_request.enabled / ci.enabled /
scheduler.enabled)
required if chosen copilot (--runtime copilot)
Preflight validates prerequisites for enabled features only, so a default
offline run never demands gh or copilot. gh covers every GitHub-touching
feature. It covers the backlog daemon: scheduler.enabled polls GitHub Issues
through gh.
factory doctor runs the full report. factory run and factory start apply
the same rule as a cheap PATH-only gate that fails with one explicit line and
exit code 2 before any work starts.
Local service¶
Continuous operation is a per-user launchd LaunchAgent, installed by an
explicit CLI command and by nothing else (ADR-018).
No root LaunchDaemon, no automatic installation, no installation as a side
effect of extracting an archive or running a command. The installer captures an
explicit PATH snapshot because launchd agents inherit a minimal environment.
It requires scheduler.enabled and a clean factory doctor report.
launchd's own stdout/stderr go to /dev/null: the factory writes its own
bounded rotating structured log under <data_dir>/logs/factory.log, and a
launchd-captured stdio file is never rotated. KeepAlive is Crashed-only, so
no exit code (including the CLI configuration-error code 2) can produce a
restart loop.
Uninstall unloads the agent and removes the plist. It leaves runs and workspaces intact.
Local dashboard¶
AGENTS.md bans web dashboards in V1. One narrow, explicitly requested
exception exists (ADR-016) and it is a viewer, not a control plane.
factory dashboard explicit command, disabled by default
↓
127.0.0.1 only
↓
token required (generated per start)
↓
GET only, read-only
Implemented with the Python standard library: no web framework, no npm, no bundler, no build step. It renders the run list, run detail, workflow state, attempt history and the derived metrics above. It renders no command logs and no diffs at all, because repository content and near-secret material can leak into a browser in those places.
Data minimization is applied twice, independently. The detail provider builds a
typed RunDetail containing only summary fields, completion facts and attempt
metadata (never failure_reason, agent reasoning or a raw artifact). The
request handler then allowlists the fields it renders, so a future provider
mistake still cannot leak content. A run that does not exist, or whose id is
not even shaped like one, is a 404.
It cannot approve, retry, cancel or reconfigure anything. Authority stays with
WorkflowController.
Long-term architecture¶
The current abstractions permit later addition of: - Jira - staging - deployment - Postgres - remote workers - Kubernetes - Docker sandboxes
Do not implement those merely to prove future compatibility.