Software Factory is a durable product-manufacturing workspace for turning ideas and incomplete repositories into verified, releasable products.
It is designed for a founder or small engineering team that wants a repeatable operating loop:
Idea or incomplete repository
→ product brief
→ implementation plan
→ controlled branch
→ bounded implementation
→ repository-specific verification
→ preview artifact
→ approval gate
→ release and health check
→ rollback if required
→ outcome review
→ institutional learning
The system is intentionally evidence-first. It does not treat a successful build as proof that a product is complete, useful, secure, or ready for production.
This repository contains two services that enforce the same tenant model independently.
TypeScript service (src/, server.ts) |
Rust runtime (software_factory/) |
|
|---|---|---|
| Role | Product workspace: jobs, approvals, evidence, audit | High-throughput telemetry ingestion and transformation |
| Storage | Single-writer JSON ledger (src/services/durableStore.ts) |
Appwrite |
| State | Tenant registry is in-memory | Tenant profiles in memory, credentials at startup |
| Scaling | One writer. Do not run replicas. | Stateless; horizontal scaling is safe |
| Port | 3000 | 8080 |
The TypeScript ledger is protected by a process-lifetime writer lock. A second process against the same data directory exits 75 rather than starting, because the adapter rewrites the whole file on every change and two writers would silently overwrite each other.
The data directory is a readiness check in its own right, and a failure there is fatal at
startup with the same exit 75. Before a port is bound the service writes a bounded payload
into the directory, flushes it with fsync, and reads it back — so a directory that is
read-only, not creatable, or not durable is refused rather than discovered on the next real
write — and the volume's free space is checked against a 64 MiB floor, because a near-full
volume accepts the probe write and then fails the whole-file rewrite that follows.
npm ci
cp .env.example .env # then fill in real values; see below
npm run build
npm startEvery credential is required. The service exits non-zero at startup rather than starting with a placeholder, because a service that starts unconfigured fails quietly at 3am.
| Variable | Required | Notes |
|---|---|---|
FACTORY_TENANT_CREDENTIALS |
yes | tenantId:secret,tenantId:secret. One secret must map to exactly one tenant. |
FACTORY_API_KEY |
yes | platform_operator credential, for cross-tenant operations. |
FACTORY_DATA_DIR |
no | Ledger location. Single-writer. |
FACTORY_WORKSPACE_ROOT |
production | Bounded directory the service may write to. |
FACTORY_MODEL_ALLOWED_HOSTS |
production | Egress allowlist for model providers. Add generativelanguage.googleapis.com to enable live Gemini. |
FACTORY_ALLOWED_SECRET_REFS |
production | Names a job may substitute into a request. Add GEMINI_API_KEY to enable live Gemini. |
ALLOW_INSECURE_LOCAL |
no | Only for local work; refused when NODE_ENV=production. |
A request carries its credential in x-tenant-id and x-api-key. A credential issued for
one tenant is refused with 403 TENANT_CREDENTIAL_MISMATCH for any other, and a tenant with
no provisioned credential is unwritable.
cd software_factory
cargo build --release
GEMINI_API_KEY=... \
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 \
APPWRITE_PROJECT_ID=... \
APPWRITE_API_KEY=... \
TENANT_API_KEYS="tenant_re_8841:...,tenant_hc_1042:..." \
./target/release/software_factoryAll five are required; the process exits 1 naming the missing one. GEMINI_MOCK=1 enables
the deterministic mock for local work, and its output is labelled "mock": true so a
fabricated response cannot be mistaken for inference. A key bound to two tenants is rejected
at startup.
Endpoints: /health (liveness, public), /ready (readiness, 503 when unconfigured),
POST /api/v1/telemetry/ingest, GET /api/v1/tenants/me.
npm test # unit + source-invariant tests
npm run verify:docs # documentation gates
npm run verify:fresh-clone # a clean clone installs and verifies (needs bubblewrap)
npx tsc --noEmit # types
npm run verify:layer1 # 34 checks against the built server
npm run verify:layer2 # 26 checks: tenant isolation
npm run verify:layer3 # 24 checks: error contract, writer lock
cd software_factory
cargo test # 15 tests
cargo clippy --all-targets -- -D warnings
cargo fmt --check
python3 scripts/verify-k8s.py # 58 manifest assertionsThe three harnesses start the built server and drive it over HTTP, then assert nothing is
left listening. A unit test cannot pass while the real request path is unguarded; several
defects in 4.7.0 were found only by the harnesses. The sandbox tests execute verification
commands inside bubblewrap: a run whose doctrine enables the sandbox refuses to start unless
bwrap is present and can actually unshare, so a host without it fails loudly rather than
running verification commands unsandboxed. npm run verify:fresh-clone proves the same suite
passes from a clone that has none of the working tree's gitignored files.
These are real and are not scheduled work:
- The tenant registry is in-memory. Provisioned tenants do not survive a restart, and the TypeScript service cannot be scaled horizontally. A credential that stops matching a tenant is the only thing that keeps a stale tenant from being served.
- The Docker image has never been built. It is configured to run as non-root and to contain no secrets; that is verified by inspection, not by a build.
- Plaintext tenant credentials are held in process memory for the process lifetime.
- The k8s NetworkPolicy permits egress on 443 to
0.0.0.0/0. Narrow it before production. - The Rust runtime has no metrics endpoint.
See sprints/sprint-15-production-hardening.md for the full record, including the fixes
that were reverted to confirm the tests would fail without them.
The repository has completed the founder-workspace and bounded-harness implementation program. The current system is suitable for a controlled daily pilot on registered, low-risk projects.
The capabilities below are the code's own declaration of what it can do. This is the same manifest the
API serves at GET /api/factory/capabilities and the evidence page renders; the table is generated by
npm run render:readme, so it cannot list a capability the code does not declare, and npm run verify:docs fails when this file drifts from the manifest. Every live entry names the command that
demonstrates it; every blocked entry names the operator who must unblock it. The richer workspace
surfaces — jobs, approvals, releases, outcomes — are described under Operating the factory below.
| Capability | Status | Demonstrated by / owner |
|---|---|---|
| Unattended execution loop | live | npm run verify:loop |
| Durable failure classification | live | npm test -- test/loop-failure-class.test.ts |
| Unrelated-writes guard (shadow) | live | npm test -- test/unrelated-writes.test.ts |
| Bounded learning digest | live | GET /api/loop/learning-digest |
| One model-egress door | live | npm test -- test/egress-confinement.test.ts |
| Tenant-scoped control plane | live | npm test -- test/loop-control-plane.test.ts |
| Fail-closed readiness | live | GET /api/factory/health |
| Capability manifest | live | GET /api/factory/capabilities |
| Declarative skills registry | live | GET /api/factory/skills |
| Hosted frontier model | blocked | blocked — owner: operator |
| Live GitHub pull request | blocked | blocked — owner: operator |
| Sandboxed execution in the shipped image | blocked | blocked — owner: operator |
| External cloud runtime | blocked | blocked — owner: operator |
9 live · 4 blocked · 0 planned. Every live entry names the command that
demonstrates it; every blocked entry names the operator who must unblock it. A capability appears
here only when it is declared in src/services/capabilityManifest.ts — this table is generated by
npm run render:readme, so it cannot describe a capability the code does not.
The system is not yet an unrestricted autonomous production company. A hardened container or microVM executor, a provider-specific hosted deployment adapter, production secret injection, and full local reality verification remain explicit boundaries and readiness gates.
Most development automation optimizes for code generation. Software Factory optimizes for operational completion:
- What is the product supposed to accomplish?
- What remains unfinished?
- What evidence proves each acceptance criterion?
- Which actions are reversible?
- Which actions require approval?
- What failed, who owns the next action, and how many retries remain?
- What happened after release?
- What should the next product inherit or avoid?
The result is a durable operating memory for product work rather than another transient coding assistant.
Software Factory is a modular TypeScript/Express application with a React operations surface and a durable local ledger. The architecture is organized around four boundaries:
- Control layer — Constitution, ADRs, product briefs, implementation plans, completion tasks, approval policies, and evidence contracts.
- Agency layer — Repository preparation, explicit branch modification, verification, previews, context refresh, release preparation, and bounded autonomy.
- Runtime layer — Tenant-scoped jobs, durable persistence, failure records, audit events, quality snapshots, outcomes, and recovery state.
- Adapter layer — Verification profiles, filesystem release, future hosted deployment, execution sandboxes, model providers, and domain-specific integrations.
The core domain is kept separate from replaceable infrastructure so local-first operation can grow into hosted execution without changing the product contract.
- Node.js 22+
- npm
- Git
npm install
cp .env.example .envAt minimum, configure a strong FACTORY_API_KEY for protected API access. Keep credentials outside the repository and never place them in briefs, evidence, prompts, logs, or context records.
npm run verifyThis runs TypeScript checking, the serialized test suite, and the production build.
npm run devThe server exposes the API and serves the frontend operations surface. Production mode uses the generated bundle:
npm run build
npm startRegister a Git repository once inside the approved workspace boundary:
curl -X POST http://localhost:3000/api/workspace/projects \
-H 'authorization: Bearer YOUR_FACTORY_API_KEY' \
-H 'x-tenant-id: founder' \
-H 'content-type: application/json' \
-d '{"tenantId":"founder","name":"My Product","repositoryPath":"/workspace/my-product","kind":"personal"}'curl http://localhost:3000/api/workspace/inbox \
-H 'authorization: Bearer YOUR_FACTORY_API_KEY' \
-H 'x-tenant-id: founder'The inbox surfaces projects, blocked jobs, unfinished jobs, pending approvals, and stale operational work.
A continuation report identifies missing briefs, plans, branches, verification, previews, and completion tasks rather than restarting the repository from scratch.
curl http://localhost:3000/api/workspace/jobs/JOB_ID/continuation \
-H 'authorization: Bearer YOUR_FACTORY_API_KEY' \
-H 'x-tenant-id: founder'A job cannot be delivered until required completion tasks are complete, evidence exists, and the required approval is durable. Reversible actions can be automated within policy; irreversible actions require an explicit decision.
After delivery, record measurable results such as time saved, adoption, client acceptance, defects, incidents, or learning. These records are the input to future quality improvement.
The loop takes one task document and runs PLAN → IMPLEMENT → VERIFY → COMMIT → REPORT for every work unit in it, without asking anything, until each unit is committed, stuck, or the run hits a hard stop.
npm run factory:run -- --repo /path/to/target --task /path/to/task.md| Exit code | Meaning |
|---|---|
0 |
every unit committed, each under a verification command that exited 0 |
1 |
the run refused to start (dirty tree, bad document, doctrine drift) |
2 |
the run finished, and at least one unit is STUCK or BLOCKED |
3 |
the run halted at a hard stop (wall clock, attempts, commits, resources, doctrine) |
Options: --tenant, --attempt-cap (lowers doctrine's cap, never raises it), --resume <runId>,
--report-dir, --json. The run writes <runId>.md and <runId>.json to
FACTORY_LOOP_REPORT_DIR (default .data/loop-runs/) whatever its outcome — including a
refusal.
What makes it safe to leave alone:
- The rulebook is configuration, not prose.
doctrine/holds the stage order, the attempt cap (3), the hard stops, the commit policy, the model assignments per role, and a hook list naming the gate that enforces each rule.doctrine/manifest.jsonpins every file's sha256; a doctrine that no longer matches its manifest is refused before any work starts. - Gates are code, and both directions are checked. Every hook id resolves to an
implementation in
src/services/loopGates.ts, every rule's enforcement gate is declared by some stage, and a gate no stage declares is a test failure. - The target cannot supply its own rules. The doctrine lives outside the target repository;
the target's
AGENTS.md,.claude/,opencode.jsonand friends are hashed into a quarantine manifest and never read, and every process the loop spawns getsOPENCODE_DISABLE_PROJECT_CONFIG=1. - Evidence is executed, never narrated. A unit is complete when its verification command ran
and exited 0. The commit body records those commands and their exit codes, plus a
Proof: sha256:digest; a model's own account is recorded and discarded. - Failure is bounded, classified, and visible. Three attempts per unit, each quoting the previous refusal
back to the model and rolling the tree back first; then STUCK, and the run continues with the
next independent unit. Every failed attempt carries a machine-readable class —
security,verification,review,model,environment,policy, orunknown— derived purely from its refusal code and stage (no model), persisted on the record so it survives--resume, and summarised in the report's Failure classes section. - A unit that cannot fail its own check, or cannot run it, is refused before the model is
dialled. A unit whose verification command already exits 0 on the starting tree is a no-op —
nothing it does can satisfy the proof — so
no-op-unitrefuses it once instead of burning the attempt cap. A sandboxed command that dies with exit 127 because the sandbox masked its toolchain (for example a version-managernode/npmliving under the masked/home) is named at the boundary that observed it (SANDBOX_TOOLCHAIN_HIDDEN), not retried three times. - A comment must not claim a value its code does not use.
diff:comment-claimcompares each added comment against the line it annotates; a comment naming a value the code does not use refuses the unit. - A change nobody declared is observed. The commit stages every change in the tree, so a
formatter's rewrite or a tool's side effect would land under a message describing only the claimed
work.
unrelated-writescompares the working tree against the implementer's claimed files (and an allowlist) and names any file that changed without being claimed. It ships in shadow mode —loop.json:unrelatedWrites.enforcedefaults tofalse, so the write is recorded and nothing is refused until an operator turns enforcement on. - A failed command reaches the next attempt with its file:line, not just an exit code, so the model is told where to look.
The format of the input is specified in docs/TASK_DOCUMENT_FORMAT.md. The rules the loop enforces are in doctrine/DOCTRINE.md. What the review stage can and cannot prove is stated honestly in docs/ADVERSARIAL_REVIEW_SCOPE.md.
The loop has been run end to end against real repositories, and the reports are kept. The model was
served by a local Ollama daemon (gpt-oss:20b-cloud); Ollama's -cloud models forward to a remote
host, so what this repository's egress guard observes is the loopback address of the daemon, not the
model's true location — which is exactly why the hosted path is declared blocked in the capability
manifest above. The figures below are the runs' own measurements, not extrapolations.
- Software Factory ran the loop on itself — run
looprun_cd5b2f682f3ca0f7c99ab731, 2/2 verified commits on attempt 1. - A real i18n batch on Daftari — run
looprun_e8c12bcd603a16fe1dd9d02c68d, 4/4 verified commits, all first attempt, 66 gates executed with 0 refused; 217.6s wall for 4 units. Daftari'scheck-i18nand typecheck were both green after. - A die-and-resume drill — run
looprun_875b855f07096ade7d02c68dwas SIGKILLed mid-verification and resumed (--resume) to exactly one commit with the interrupted path restored to its last committed state.
The gate feedback loop, a refusal carried into the next attempt, and the reviewer refusing an unrelated-file change are recorded in sprints/sprint-22-trustworthy-unattended-loop.md.
npm run verify:loop # doctrine manifest, gate wiring, 7 end-to-end scenarios, CLI contract
npm run doctrine:manifest # re-pin after editing doctrine/ (then re-review the diff)The profile detector selects a repository-specific verification contract:
| Profile | Typical checks |
|---|---|
| Node/npm | Declared verify, lint, test, and build scripts |
| Android Gradle | Unit tests, lint, and offline release assembly |
| Rust Cargo | Format check, tests, and release build |
| Python | Ruff and pytest when declared |
| Git integrity | Diff and repository integrity fallback |
Unsupported or ambiguous projects should be explicitly configured rather than silently executed with guessed commands.
The loop can now be started, listed, inspected, and watched from an authenticated HTTP API or from the in-browser operator panel.
- REST:
POST /api/loop/runs— start a run. Headers:X-API-Key: <tenant key>,X-Tenant-Id: <tenant>. Body:{ "tenantId": "<tenant>", "repo": "/path/in/workspace", "taskDocument": "/path/in/workspace/task.md" }→202 { runId, repo, taskDocument, status: "running" }.POST /api/loop/goals— draft a parser-accepted task document from a natural-language goal. Body:{ "tenantId": "<tenant>", "repo": "...", "goal": "...", "providerId": "...", "model": "..." }→201 { taskDocument: "/path/.factory-goal-drafts/...", draft, title, milestones, providerId, model }.GET /api/loop/runs— list all runs for the authenticated tenant.GET /api/loop/runs/:id— return the durable run record for the authenticated tenant (404 for a foreign run).GET /api/loop/runs/:id/events— SSE stream of recorded events, ending withevent: done.
- Browser panel: the Unattended Loop tab in the frontend lets an operator paste a tenant API key (kept in memory only, never persisted to storage or the URL), draft a goal, submit a run, and tail its live stream.
Software Factory follows these non-negotiable policies:
- Every durable record is tenant-scoped.
- Production and other irreversible actions require approval.
- The repository remains the source of truth.
- Secrets are injected through the runtime environment, not persisted in job context.
- Verification and repair are bounded by time, output, retry, and action budgets.
- Failed work becomes visible with a failure class, owner, and next action.
- Required unfinished work prevents delivery completion.
- Context promotion requires explicit review and evidence.
- A release must have a health check and rollback target.
src/ React application, API routes, services, models
server.ts Express production entrypoint
src/services/ Durable jobs, workspace, execution, release, recovery, outcomes
src/api/routes/ Authenticated tenant-scoped APIs
doctrine/ Stage order, attempt cap, models per role, rules and hooks (outside the target)
test/ Deterministic unit and integration tests
docs/ Constitution, ADRs, contracts, and operating playbooks
sprints/ Sequential implementation records
software_factory/ Rust runtime foundation and domain adapters
appwrite-functions/ Optional Appwrite integration functions
- Harness Engineering Contract
- Founder Operating Playbook
- Product Foundry Context
- Repository Context Index
- Generated Codebase Index
- Task Document Format
- Adversarial Review — True Scope
- Machine-Building Contract
- Sprint 12: Real-World Readiness
- Sprint 14: Machine Building
- Engineering Constitution
- Architecture Decision Records
- Changelog
The next readiness gates are deliberately concrete:
- Replace the restricted local execution adapter with a container or microVM boundary for untrusted code.
- Connect one hosted deployment target with isolated secret injection.
- Add real post-deployment observation and infrastructure rollback.
- Complete five founder jobs and three client-like dry runs with evidence packs.
- Measure cycle time, defect rate, repair rate, recovery time, and product outcomes across launches.
- Expand autonomy only by approved action class and verified project profile.
The system should earn broader autonomy through successful evidence, not through a larger prompt or a higher claimed percentage.
Read CONTRIBUTING.md before making changes. Every change should preserve the Constitution, relevant ADRs, tenant isolation, evidence contracts, and green verification.
Software Factory is released under the MIT License.