Skip to content

Architecture Decisions

ADR-023: Enforce concise controlled writing

All factory-authored prose uses one controller-owned writing policy. This includes agent artifacts, agent prompts, generated issues, pull requests and commit messages.

The factory includes a reviewed subset of the SimpleEnglish v2.0.2 linter at revision 61ee200efbd423050aab982eed94226229891ae0. The MIT license and source notice ship with every package. The factory uses only local deterministic checks. It does not run upstream plugins, hooks or benchmark tools.

The runtime policy checks sentence length, field word limits, filler terms, semicolons, em dashes, and Latin abbreviations. It does not ban uncertainty words such as may or might. Review and research must keep calibrated uncertainty.

Repository documentation uses the pinned SimpleEnglish skill in Strict mode. A local gate checks README.md and every Markdown file in docs/. The gate also checks contractions, modal words, selected perfect tenses, and selected comma-plus--ing clauses. It applies a 20-word limit to procedural sentences. It applies a 25-word limit to descriptive sentences.

CI, Pages deployment, and release validation run the documentation gate. AGENTS.md requires future documentation changes to use the same skill and gate.

The controller rejects invalid model prose and gives one bounded correction prompt. It never silently rewrites an artifact. Publication text fails before Git or GitHub mutation.

Human input, code, identifiers, paths, commands, URLs, quoted errors and raw command output remain exact. Generated pull request text uses the refined specification instead of repeating the original work item description.

These checks apply ASD-STE100 principles. They cannot validate the full standard or its controlled dictionary, so the factory does not claim formal compliance.

ADR-022: Opt-in autonomous project delivery

The explicitly requested delivery boundary is reviewed, CI-green code merged into the configured target branch, not a local integration branch or an open PR. This supersedes the original blanket deferral of autonomous merging only for the opt-in controller-owned delivery path.

The existing WorkflowController still owns every child run, local quality gate, independent review, PR publication and bounded CI repair. A controller merge adapter verifies repository and target allowlists. It checks the reviewed head revision, required checks, and merge eligibility before requesting a merge. It never bypasses branch protection, uses administrator privileges, force-pushes or deploys. Completion requires persisted evidence that the PR actually merged.

Branch push gets one bounded retry for transient Git transport or remote backend failures. Before retrying, the controller reads the exact target branch tip and accepts a lost response only when that tip is the expected commit. Authentication, authorization, policy and non-fast-forward failures are not retried.

PR creation separately gets one bounded retry for transient GitHub transport failures. Before retrying, the controller searches for the exact repository, head, base and run marker. This recovers a PR that GitHub created before the response was lost and prevents duplicate publication.

Every PR revision (such as each CI repair) must pass the configured independent Reviewer or the controller's bounded review-acceptance policy. A controller acceptance is a separate typed artifact, never a rewritten Reviewer approval. It is limited to configured low-risk findings, bound to the exact reviewed tree, and disclosed in the PR. When opt-in automatic merge is enabled, an eligible accepted-with-findings revision can merge after required CI passes. This path does not wait for a human to read the disclosure. The controller refuses to publish or merge a different head. Security, scope and repair-regression findings cannot be accepted. This model review does not impersonate a GitHub user review or bypass repository rules requiring additional human approvals.

Planner, Tester and Reviewer schema failures get bounded same-model correction with the exact validation error. Tester and Reviewer correction calls do not spend implementation attempts. Reviewer repair attempts receive the current work item and earlier blocking findings, and deterministic verification must leave the Git tree unchanged before independent review starts.

Remote project delivery executes tasks serially against the freshly fetched target branch. This is the smallest sufficient way to make sure that every task includes its merged predecessors and avoids inventing a merge-queue scheduler. Local-only project execution retains its existing bounded wave concurrency.

Project recovery reconciles immutable plans, persisted child run identifiers, Git worktrees and GitHub delivery evidence before dispatch. It reuses delivery checkpoints and retry budgets rather than creating duplicate PRs or resetting attempts. Ambiguous in-flight implementation or conflicting workspace state stops with a recorded reason instead of guessing or discarding changes.

Human configuration can authorize exact repository-relative dependency and CI files, but only when those same files are explicitly named in the task plan. Protected files, risk approval, scope limits, verification and independent review remain authoritative. No agent can grant itself an exemption.

Review approval is bound to an immutable Git tree. Publication verifies both the staged tree and the committed tree before pushing. Delivery starts from the exact fetched target, not an ahead local checkout, and the original repository/host identity remains fixed throughout the run.

Tree approval alone does not authorize intermediate history. The controller creates a commit from the approved tree and allowed parent. It persists the commit receipt, advances the branch, and pushes that SHA. Recovery can publish only the recorded commit, never an arbitrary current HEAD. Implementers are also denied direct git commit access as defense in depth.

Configured required checks must also be enforced server-side by the target's active protection policy, so a rerun cannot race the final merge. The merge adapter uses a synchronous expected-head merge API, not a CLI operation that can silently enable auto-merge or enqueue work. Unsupported queues and unenforceable policies fail closed before mutation.

Classic PR bypass allowances for users, teams and apps must all be explicitly empty. A PR still reporting REVIEW_REQUIRED cannot merge, even when the factory credential has authority to exercise a repository bypass.

All new capabilities are disabled by default. Merging implementation code does not authorize running a migration, production credential access, or software deployment.

ADR-001: Build one small executable vertical slice

Phase 1 combines the original fake-workflow and Git-worktree milestones.

Reason: - a workflow without a repository boundary proves too little - adding workspaces later forces the runtime and controller APIs to change - one synchronous path is easier to understand and test

The slice keeps the intended stages, typed artifacts, deterministic routing, bounded repair, filesystem persistence and independent fake review.

ADR-002: Keep authority and evidence deterministic

Only WorkflowController changes run state.

Agents return typed outcomes but do not transition runs. The controller derives changed files and patch.diff from Git. This includes newly created files. Verification command results are also controller-produced evidence.

ADR-003: Use one repair budget

Every implementation or repair entry appends an attempt record and consumes one global maximum. Verification and review failures share this budget.

This prevents alternating gate failures from bypassing bounded retry policy.

ADR-004: Defer scheduler architecture

Phase 1 is a synchronous manual command with concurrency one.

Polling, reconciliation, tracker adapters, retry timers, activity heartbeats and multi-task scheduling are deferred until factory start. A per-work-item exclusive lock and subprocess timeouts provide the necessary local safety now.

ADR-005: Treat Symphony as coordination inspiration

The project follows Symphony's control-loop and workspace principles, but is not a conforming implementation. Copilot execution, finite persisted repair budgets, typed SDLC artifacts, controller-owned Git/PR behavior and independent quality gates are deliberate extensions.

ADR-006: PR_READY is a completed endpoint, not a terminal state

Terminal states are DONE, NEEDS_HUMAN and FAILED.

PR_READY stays reachable for pull-request-enabled runs (it transitions to PR_CREATED), so it cannot be terminal. But when pull_request.enabled is false it is where the manual flow legitimately ends.

The controller therefore finalizes it explicitly with finalize_pr_ready. This operation stamps completed_at. workflow.is_run_finished is the single predicate that distinguishes the two completion conditions. The scheduler uses that predicate instead of comparing states directly.

Transitions also clear a stale completed_at/failure_reason whenever a run becomes active again, so a repaired run never carries a completion timestamp from an earlier cycle.

ADR-007: Two separate, persisted retry budgets

AttemptBudget.IMPLEMENTATION covers worktree-editing attempts: implementer failures, deterministic verification failures and reviewer rejections consume retries.max_total_attempts.

AttemptBudget.CI_REPAIR is a separate budget bounded by ci.repair_attempts. It also hard-caps how many times a PR can be updated, so a CI loop cannot push forever.

Both implementation attempt numbers are derived from persisted FactoryRun.attempt_records, never from a local counter. A restarted process therefore cannot widen a budget. A scope replan after successful deterministic verification updates only the ExecutionPlan, re-assesses the existing green diff, and proceeds without rerunning the Implementer. These metadata-only replans are bounded independently by scope_drift.max_replans and persisted in FactoryRun.scope_replans. Legacy scope-triggered attempt records remain counted during recovery.

ADR-008: Lock contention is not a persisted failure

If another run owns a work item's workspace, WorkflowController.run returns a non-persisted FAILED outcome. It explains the work item is active, and writes nothing.

Persisting a junk FAILED run pollutes the store, counts against nothing, and later forces reconciliation to explain a run that never did work. Since no workspace is prepared and no artifact is written, there is nothing to corrupt or recover.

ADR-009: Research runs and does not escalate

Phase 1 escalated needs_research=true to NEEDS_HUMAN because no researcher existed. The researcher now runs exactly once per run, its ResearchReport is persisted, it is handed to the planner, and the run continues. Research is never re-run, so a task cannot repeatedly pay for it.

ADR-010: The independent tester returns a TestReport

Earlier wiring mapped the tester role onto VerificationReport. That conflated a model's judgement with deterministic, factory-produced evidence, which directly contradicts "a model does not approve its own work".

The tester now returns TestReport (advisory), while VerificationReport remains exclusively controller-produced. Tester and reviewer receive the authoritative diff, the controller-derived changed-file list and the deterministic report. Neither ever receives the implementer's ChangeSet summary.

ADR-011: Conservative scheduler recovery

A persisted, non-terminal run found at startup is escalated to NEEDS_HUMAN through WorkflowController.recover_abandoned_run rather than auto-resumed.

Auto-resuming spends a paid attempt on a run whose true state cannot be established cheaply. Escalating preserves every artifact and the workspace, consumes no budget, and leaves a human in control. The scheduler itself still never mutates run state.

ADR-012: Worktree administration is serialized per source repository

git worktree add and git worktree prune both rewrite repository-global administrative metadata. With scheduler.max_concurrent_tasks = 2, two runs can prepare workspaces simultaneously. The prepare() sequence runs under a per-source-repo flock in Git's common directory. The lock is therefore shared even when factory processes use different data directories. Per-work-item workspace locks remain separate and are what prevent duplicate active work within one factory data directory.

ADR-013: Tracked work is dispatched at most once

The generic Scheduler prevents concurrent duplicates and otherwise assumes a tracker withdraws an item once work starts. GitHub Issues do not withdraw items. An issue stays open and keeps its agent-ready label, while the factory holds no write access.

Without an additional rule, the tick after a run reaches DONE/NEEDS_HUMAN/FAILED dispatches the same issue again under a new FactoryRun with an empty attempt_records list. This causes an unbounded loop of paid work that mints a fresh retry budget every cycle and defeats ADR-003/ADR-007.

service.AlreadyRunFilter therefore makes any tracker item with a persisted FactoryRun (finished or not) ineligible. Re-running is an explicit operator action: archive or remove the previous run, or invoke factory run --work-item-id by hand. This keeps the rule durable across restarts without adding GitHub write permissions or a database.

ADR-014: Phase 15 is opened selectively, not as a whole

Phase 15 was a single "later integrations" bucket. That made it impossible to say yes to delivery work without appearing to say yes to Temporal, Postgres, Kubernetes, Jira and autonomous deployment.

Phase 15 is therefore split into numbered sub-phases with independent statuses. Exactly five are open: 15.0 factory CI, 15.1 tag-driven release, 15.2 macOS packaging and the launchd service, 15.5 local monitoring/health and 15.11 the read-only dashboard.

Every other sub-phase (staging 15.3, deployment 15.4, Docker 15.6, remote workers 15.7, Postgres 15.8, Temporal 15.9, Jira 15.10, and Kubernetes 15.12) stays deferred. Nothing in the open sub-phases can depend on a deferred one, and no deferred item is unblocked by proximity.

The selection is operational, not architectural: it makes the existing factory installable, observable and inspectable on one MacBook. It does not widen what the factory is allowed to do autonomously.

ADR-015: CD means publishing release artifacts, never deploying

"Continuous delivery" in this project stops at a published GitHub Release. A version tag builds artifacts and attaches them. Nothing installs, restarts, promotes or self-updates, and there is no mutable pointer a client follows automatically. Autonomous deployment stays banned by AGENTS.md.

The release workflow checks whether the tag's release already exists and refuses to replace its artifacts. GitHub release immutability is also enabled for new releases. Existing releases from v0.3.0 onward report immutable=true. Older historical releases remain mutable through the platform. SHA256SUMS and build-info.json remain required consumer checks for the downloaded bytes and their build provenance.

Two native macOS builds are produced (arm64 on macos-15 and x86_64 on macos-15-intel) as separate PyInstaller onedir archives. universal2 is rejected. It requires universal wheels for every native dependency, produces a larger artifact, and turns packaging problems into total build failures. Building each slice natively on its own runner keeps failures isolated and diagnosable.

The release contains a wheel, sdist, SHA256SUMS, and build-info.json. These record tag, commit, runner image, Python, and PyInstaller facts to trace build provenance.

Artifacts are unsigned or ad-hoc signed. Developer ID signing and notarization are deferred. They need a paid account and secrets in CI, which are not justified for this tool. The consequence is that Gatekeeper will quarantine a downloaded archive, so release notes must say so plainly and document the manual step. Silence here looks like a broken build.

A frozen artifact is not self-sufficient. It bundles Python and the factory, but git must exist on PATH. In addition, gh is required only for enabled GitHub features. The copilot executable is required only for --runtime copilot. Preflight therefore validates prerequisites for enabled features, so the default offline run does not demand tools it will never call.

ADR-016: The local dashboard is a bounded exception to the V1 ban

AGENTS.md bans a web dashboard in V1. One narrow exception is granted. Inspecting runs, states, attempts, and metrics by reading JSON files is worse than viewing a page.

The exception holds only within these boundaries: - loopback bind, explicit start command, disabled by default - read-only: GET only, no route mutates runs, workspaces or configuration - token protected, token generated per start and never logged - Python standard library only: no framework, no npm, no bundler, and no build step - no command logs and no diffs rendered, because repository content and near-secrets can leak into a browser in those places. Data minimization is applied twice. The detail view uses an allowlisted typed model. The request handler allowlists fields again before responding

The ban itself is unchanged for everything else. This tool is a local viewer, not a control plane. It cannot approve or retry runs, cannot enable integrations, and has no multi-user concept. If a change requires a write path, a framework, or a network listener, that requires a new ADR.

ADR-017: Health and metrics are derived, never accumulated

Persisted run artifacts remain the single source of truth. Health and metrics are pure functions over the run store, computed on demand.

No counter store, no time-series database and no separate metrics file is introduced. A derived view cannot drift from the runs it describes, can be recomputed after any crash, and is trivially testable against a fixture store. Health and metrics are strictly read-only: they never repair a lock, prune a worktree or transition a run. They report those as findings for an operator.

Cost is deliberately not fabricated. Token usage and cost appear only when the runtime actually reported them. Otherwise the value is unknown, never zero and never inferred from a hard-coded price table. A confidently wrong spend number is worse than no number. Copilot invocations request the CLI's experimental usage-output file and persist typed InvocationRecord telemetry. Raw premium-request cost and nano-AIU remain separate persisted units. The dashboard can derive an AI usage value in USD from nano-AIU for display. It uses GitHub's fixed AI Credit conversion. It is labeled as usage value rather than invoice spend because included or pooled credits can cover it. AttemptRecord remains the implementer retry ledger and links to its invocation rather than being overloaded with every agent call.

Monitoring stays local: structured JSON logs bounded in size inside the data directory, with the same credential redaction already applied to command output. No exporter, no cloud backend, no telemetry leaves the machine.

ADR-018: The launchd service is an opt-in user agent

Running factory start continuously is a launchd job, but a deliberately timid one.

It is a per-user LaunchAgent under ~/Library/LaunchAgents, installed only by an explicit CLI command. It is never a root LaunchDaemon, never installed by extracting an archive, and never installed as a side effect of running the factory. A background process that can spend money and push branches must be an explicit, reversible act.

The installed job defaults to --runtime fake, so an accidentally loaded agent costs nothing until someone deliberately changes it. Because launchd gives agents a minimal environment, the installer captures an explicit PATH snapshot. Otherwise the service fails to find git, gh, or copilot, which looks like a factory bug. The installation command also refuses unsuitable configuration. The configuration must enable the scheduler, and factory doctor must report no problems. A service that cannot work is worse than no service.

Logging goes to the factory's bounded rotating structured log under the configured data directory. Launchd's stdout/stderr are pointed at /dev/null precisely because launchd never rotates what it captures. KeepAlive is Crashed-only, so no exit code (including the configuration-error code 2) can create a restart loop. Uninstall unloads the agent and removes the plist while leaving runs and workspaces untouched.

ADR-019: Repository capabilities are deterministic profiling plus on-demand skill research

Supersedes the original ADR-019, which selected advisory skills from a fixed, versioned built-in catalog. That catalog is removed.

Superseded in part by ADR-021: generated guidance is repository-wide, reusable and stored outside the target repository, and a separate human overlay exists. The profiling, sandbox rules, and validation decisions below still stand. The "regenerated fresh for every eligible run, no cross-run cache" part does not.

Repository awareness is a controller-owned scan, not an agent discovery step. After the worktree is prepared and before TRIAGING, the factory walks repository-local paths and reads a small allowlist of bounded manifests. It does not run a shell command, import target code, contact the network or trust repository-provided instructions.

The resulting versioned RepositoryProfile is persisted as repository-profile.json. It records technologies, tools, package managers, markers, warnings, and version files. It also records declarations, ecosystems, versions, manifests, and dependency groups. Declarations are parsed from pyproject.toml, requirements.txt, and package.json. These include PEP 621 tables, Poetry tables, and package manager declarations. setup.cfg and tox.ini contribute Python and pytest evidence only. Exact versions are resolved from uv.lock, package-lock.json and pnpm-lock.yaml when unambiguous, and an ambiguous resolution records a warning rather than a version. poetry.lock, yarn.lock, bun.lock/bun.lockb, Pipfile.lock and pylock.toml identify their package manager where applicable and are fingerprinted as version_files without claiming exact graph parsing.

The profile carries two distinct SHA-256 fingerprints. dependency_fingerprint is semantic: it digests technologies, test tools, package managers and the normalized dependency declarations, and it is the identity that binds a generated skill. manifest_fingerprint is provenance: it digests the content of the version files, so reformatting a manifest changes it without invalidating guidance that is still correct.

There is no fixed skill catalog. When polish.enabled is true, the controller re-profiles the post-implementation worktree after verification. If needed, it transitions through RESEARCHING to invoke the Researcher for guidance. Invalid typed output or provenance receives its exact bounded rejection reason in one retry. Infrastructure failure receives one ordinary retry. A second failure safely skips polish.

That call is bounded and web-only. It runs in the run directory instead of the worktree. Its only tool is web_fetch. It sees only the normalized profile, configured URL lists, and generation rules. It never sees changed filenames, source code, README content, task prose or the diff. polish.official_documentation_origins (official documentation, migration guides, release notes) is authoritative for every version claim. Curated polish.practice_reference_urls are pinned to commit 52cc5efd. They contribute generic quality heuristics only, synthesized rather than copied. They never supply version claims, commands, tools or orchestration. Fetched pages are untrusted data.

It returns one typed RepositorySkill with the dependency_fingerprint, bounded targets, and HTTPS sources. It includes simplify and polish guidance, and uncertainties. The type itself refuses a skill with neither an official source nor an explicit uncertainty, and refuses an official source that claims only generic applicability.

The controller validates deterministically. It rejects fingerprint mismatches, unprofiled targets, or evidence paths outside the profile. It also rejects missing official provenance for frameworks or unallowlisted sources.

The run is green before polish. A failed re-profile, rejected skill, or stale skill records a profile warning. The factory skips polish and proceeds to review. The skill reaches only the polish Implementer, Tester and Reviewer, never before the initial green baseline, and is regenerated fresh for every eligible run. There is no cross-run cache or plugin system. The context is advisory: it changes no tools, models, workflow states, quality gates, commands, permissions or routing. reaches only the polish Implementer, Tester and Reviewer, never before the initial green baseline, and is regenerated fresh for every eligible run. There is no cross-run cache or plugin system. The context is advisory: it changes no tools, models, workflow states, quality gates, commands, permissions or routing.

ADR-020: Post-green polish is one bounded implementation attempt, informed by on-demand skill research

Superseded in part by ADR-021. Polish applies reusable guidance and human overlays. Research runs only when the fingerprint has no guidance. The bounded, simplify-then-polish shape stands.

When polish.enabled is true, verification schedules skill research and one IMPLEMENTER pass with AttemptTrigger.POLISH. It applies RepositorySkill: simplify first, then version-specific polish second. It uses existing worker routing and implementation budgets. It can make no edits. Full deterministic verification and scope assessment run again before review.

Polish never runs during CI repair and is scheduled only when one later implementation attempt remains available to recover from a regression. It introduces no POLISHING state and no POLISHER role. The temporary VERIFYING → RESEARCHING → IMPLEMENTING → VERIFYING sequence remains authoritative and visible in persisted attempt records. The generated skill is provided only to that attempt's Implementer, Tester and Reviewer and is regenerated fresh for every eligible run.

Because polish is an improvement on an already-verified change, its failure modes are non-fatal by design. An unverifiable or stale skill is discarded with a recorded warning. The run continues on its existing green path rather than failing or escalating.

The configuration model defaults omitted legacy polish sections to disabled for compatibility. The packaged default and example enable it, so their normal fake run records an initial implementation attempt and one polish attempt.

ADR-021: Repository guidance is two artifacts: generated and reusable, plus a human overlay

Repository guidance has two producers with different trust levels. It uses two separate artifacts rather than one shared file.

Generated guidance is repository-wide and reusable. It describes the repository, not the task, so the Researcher receives the normalized RepositoryProfile and the configured source lists only. It never sees changed filenames, source code, README content, task prose or the diff. 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>/.... Guidance is never stored in or loaded from target repositories. Target code cannot inject guidance into the factory, and the factory does not mutate target checkouts.

A run reuses the generated skill matching the current fingerprint and makes no research call. Generation runs only when a fingerprint has no generated skill. Existing files are never overwritten. A dependency change selects a new file. There is no TTL, because time does not invalidate guidance. A dependency change invalidates guidance. Reuse is not trust. Schema, profile agreement, and sources are revalidated on every load. A corrupted file is rejected, left on disk, and reported with a warning pointing to factory skill refresh.

This bounds research per fingerprint, not per process, and that choice is deliberate. Two concurrent first runs for the same missing fingerprint can each run one sequence: an initial Researcher call and one retry after failure. Publication is atomic and no-clobber, so one result is kept, the loser loads the winner, and both revalidate it in full before use. Cross-process serialization needs extra locking machinery and risks stalls. To save at most one sequence (two calls), the race is accepted. It cannot corrupt storage, produce competing files, change which guidance is used, or affect the overlay.

Repository identity is the local Git common directory. All linked worktrees of a checkout share one skill directory. No remote URL is consulted. Two clones of the same remote are legitimately different local repositories with different profiles. The visible consequence is that moving or re-cloning a repository selects a new key with no guidance. That is preferred over guessing identity from a remote. The factory neither follows moved repositories nor deletes orphaned guidance. Operators can copy guidance directories or recreate them deliberately. Use factory skill path to find paths.

Human customization is a separate overlay. A repository-level repository-skill-overlay.yaml lives in the same repository-scoped storage, outside the target repository. It carries guidance prose only, with mode: extend|replace plus optional simplify and polish guidance blocks. It has no targets, sources, versions or fingerprints. Version-specific claims stay the Researcher's job, grounded in official documentation. The overlay is where house rules live. Because it is unbound to a dependency state, it survives dependency changes and keeps applying when a new generated file is selected.

The factory never creates, rewrites, normalizes, refreshes or deletes the overlay. It is a human's file, and a tool that reformats or regenerates it destroys intent and discourages its use. An invalid overlay is preserved exactly as written, recorded as a warning, and ignored for that run. Valid generated guidance can still apply, so one YAML mistake does not silently drop all guidance.

Explicit commands, no hidden writes. The command factory skill path discovers file paths. The command factory skill validate validates current files. Both are read-only. factory skill refresh --repo PATH [--runtime fake|copilot] refreshes generated guidance only and never touches the overlay. The read-only dashboard gains no skill or overlay write path (ADR-016 stands).

Runs snapshot what they used. Before agents consume guidance, each run stores immutable snapshots of effective skills, overlays, and metadata. This records loaded files, fingerprints, and skip reasons. Runs remain explainable even though guidance files are editable. Mid-run edits affect later runs only.

Nothing about this grants authority. Guidance remains advisory prompt text for one post-green attempt. Full deterministic verification runs again afterwards. Guidance cannot change tools, models, states, budgets, permissions, gates, or scope. Failure to load or validate guidance skips polish with a warning instead of failing green runs.