Skip to content

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:

MANUAL
GITHUB

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:

UNCLAIMED
CLAIMED
RUNNING
RETRY_QUEUED
RELEASED

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_fingerprint is 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_fingerprint is provenance. It digests the content of every version_files path (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:

<data_dir>/repository-skills/v1/<repository-key>/...

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_fingerprint is 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_fingerprint selects 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 reviewed bdfinst/agentic-dev-team notes, pinned to commit 52cc5efd, 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_fingerprint does 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, vite or vitest dependency has no target, or is not named by the applies_to of 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 exact polish.practice_reference_urls entry.

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:

mode: extend | replace
simplify: optional SkillGuidance block
polish:   optional SkillGuidance block

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:

problem
acceptance_criteria
constraints
assumptions
unknowns
dependencies
risk_flags
confidence

Unknown information must remain explicit.

Do not silently invent requirements.

ResearchReport

Only produced when necessary.

Fields approximately:

question
findings
evidence
implications
uncertainty

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
summary
tests_added
commands_run

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:

passed
deterministic_checks
failures
coverage_change
test_findings
confidence

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:

passed
findings
suggested_tests
confidence

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:

complexity = L2

but the controller maps:

L2 → Claude Sonnet 5

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:

MAI fails twice
Sonnet

Sonnet fails twice
Opus

Opus continues failing
NEEDS_HUMAN

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:

~/.software-factory/

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:

AgentRuntime.run(
    request
) -> AgentResult

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:

attempt 1 → fail
attempt 2 → fail
escalation
attempt 3 → success

without: - paid model calls - network - nondeterminism

Keep them simple.

Local verification

Repository configuration defines commands.

Example:

install:
  - bun install

verify:
  - bun run lint
  - bun run typecheck
  - bun test

build:
  - bun run build

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:

factory/<task-id>

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).

~/Library/LaunchAgents/<label>.plist
factory start
--runtime fake by default

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.