A change-control system for governed AI-assisted development.
The factory enforces that all work is scoped, intentional, and verified through an automated pipeline before it is considered done.
It is not a project management tool. It is a governance artifact store with a single-command pipeline that plans, implements, reviews, and verifies.
AI agents can implement code. They cannot judge whether a change is safe to ship. The factory separates intent (which humans define) from execution (which agents perform autonomously).
Every change must declare its intent and scope before implementation begins. Verification (build, lint, test) gates every completion.
The factory flow:
specs/<spec-id>.md → run.ts <spec-id> → plan → develop (with code review) → QA verify → done
Human gate: exactly one — authoring the spec. The factory derives the
intent artifact from the spec at run time. (Hand-authored
intents/<id>.json files are still accepted for backward compatibility.)
Everything after run.ts is autonomous.
When evolving factory itself, this repo uses the factory-development workflow
documented in docs/decisions/workflow.md with queue and memory state in
docs/decisions/QUEUE.md and
docs/decisions/MEMORY.md.
run.ts is the pipeline entrypoint for host projects that consume factory; this repo
is deliberately not self-hosted by that pipeline.
Operator vs. agent. Operators run one command:
run.ts <spec-id>. The lifecycle scripts (start,request-review,review,complete) are the underlying protocol surface; in autonomous mode the pipeline calls them as library functions. They are also available as a manual surface for humans or self-driving agents who want to walk a single packet through its states by hand. See Agent protocol below.
- Node.js >= 20
- pnpm (or npm/yarn — adjust
factory.config.jsonaccordingly)
# Add factory as a git submodule (hidden — tooling only)
git submodule add https://github.com/custodyzero/factory.git .factory
# Run setup (installs deps, copies templates, creates artifact dirs, configures hooks)
./.factory/setup.sh
# Configure for your project
# Edit factory.config.json — set project_name and verification commandsEdit factory.config.json at the project root:
{
"project_name": "my-project",
"factory_dir": ".factory",
"artifact_dir": "factory",
"memory": {
"root_dir": "memory",
"cache_dir": "cache",
"suggestion_dir": "suggestions",
"max_additional_files": 4,
"max_file_bytes": 16384,
"max_cache_entries": 20
},
"verification": {
"build": "dotnet build",
"lint": "true",
"test": "dotnet test"
},
"validation": {
"command": "npx tsx .factory/tools/validate.ts"
},
"infrastructure_patterns": [
".factory/",
"factory/",
".github/",
"package.json",
".gitignore",
"CLAUDE.md",
"AGENTS.md",
"README.md",
"LICENSE"
],
"completed_by_default": {
"kind": "agent",
"id": "claude"
},
"personas": {
"planner": {
"description": "Decomposes intent into feature and packet artifacts",
"instructions": [],
"model": "high"
},
"developer": {
"description": "Implements the change",
"instructions": [],
"model": "high"
},
"code_reviewer": {
"description": "Reviews code changes for correctness, design, and contract adherence",
"instructions": [],
"model": "medium"
},
"qa": {
"description": "Verifies acceptance criteria are met",
"instructions": [],
"model": "medium"
}
},
"pipeline": {
"providers": {
"codex": {
"enabled": true,
"command": "codex",
"sandbox": "workspace-write"
},
"claude": {
"enabled": true,
"command": "claude",
"permission_mode": "bypassPermissions"
},
"copilot": {
"enabled": false,
"command": "gh",
"prefix_args": ["copilot", "--"],
"model_map": {
"high": "claude-opus-4-6",
"medium": "GPT-5.4",
"low": "claude-haiku-4-5"
}
}
},
"persona_providers": {
"planner": "claude",
"developer": "codex",
"code_reviewer": "claude",
"qa": "claude"
},
"completion_identities": {
"developer": "codex-dev",
"code_reviewer": "claude-cr",
"qa": "claude-qa"
},
"max_review_iterations": 3
}
}Personas declare a model tier — high, medium, or low — representing
the desired capability level. These are provider-neutral. Each provider
translates tiers to concrete model IDs via its model_map. If no model_map
is configured, the provider's default model is used.
The pipeline supports multiple agent CLI providers:
| Provider | Command (+ prefix_args) | Notes |
|---|---|---|
claude |
claude |
Anthropic Claude Code CLI |
codex |
codex |
OpenAI Codex CLI |
copilot |
gh + prefix_args: ["copilot", "--"] |
GitHub Copilot CLI (multi-model via model_map) |
Each persona is mapped to a provider in pipeline.persona_providers.
Custom providers can be added — any CLI that accepts a prompt argument works.
Factory now supports a thin host-project memory layer under the artifact tree:
factory/memory/MEMORY.md— small always-loaded indexfactory/memory/architectural-facts/factory/memory/recurring-failures/factory/memory/project-conventions/factory/memory/code-patterns/factory/memory/suggestions/— pipeline-generated suggestions that require human review before promotionfactory/cache/— transient machine cache, safe to delete and rebuild
The pipeline loads the index plus a few targeted memory files into planner, developer, reviewer, and QA prompts. It does not auto-promote suggestions into durable memory.
Host-project memory is for stable project context that is expensive to rediscover but not already the state of record elsewhere in factory artifacts.
Good durable memory:
- architectural invariants
- recurring failure modes
- stable project conventions
- reusable local code patterns
Bad durable memory:
- current packet or feature status
- run-by-run narration
- temporary debugging notes
- anything already reconstructible from
factory/packets/,factory/features/,factory/completions/, orfactory/events/
Recommended operating model:
- Keep
factory/memory/MEMORY.mdshort. Use it as an index, not a dump. - Add durable notes to the category directories when the fact is stable enough to matter across future runs.
- Review
factory/memory/suggestions/after meaningful runs and promote only the entries that deserve to become durable memory. - Ignore or delete
factory/cache/freely. It is machine cache, not project knowledge.
Promotion is intentionally manual. The pipeline may suggest memory updates, but humans decide what becomes durable memory.
The pipeline is the single entry point for all factory work:
npx tsx .factory/tools/run.ts <spec-id> [<spec-id>...]run.ts accepts one or more spec IDs and drives the pipeline to completion
across all of them in dependency order. Internally:
- Plan — Planner agent decomposes the spec into a feature with dev/qa packet pairs
- Develop — For each dev packet (in dependency order):
- Developer agent implements
- Code reviewer agent reviews (different identity)
- Feedback loop if changes requested (bounded by
max_review_iterations) - Completion recorded (build/lint/test verification)
- Verify — For each QA packet:
- QA agent verifies (different identity from dev)
- Completion recorded
- Done — Feature marked complete, summary printed
The orchestrator is responsible for sequencing; agents are responsible for
implementation. run.ts calls the lifecycle scripts as library functions
to advance state. Agents call the same lifecycle scripts as CLIs to signal
state transitions back to the factory — see Agent protocol.
run.ts is idempotent. If the pipeline fails mid-execution, fix the issue
and re-run the same command. The pipeline derives its resume point from
artifact state on disk — completed packets are skipped, in-progress packets
resume from their current lifecycle status. The lifecycle scripts are
idempotent in the same way: re-invoking on a state that already satisfies
the request prints "already done" and exits 0.
- Developer and code reviewer use different identities
- QA agent uses a different identity from the developer (FI-7)
- Identities are configured in
pipeline.completion_identities
The operator workflow is: write a spec, run the pipeline.
Create specs/add-health-endpoint.md:
---
id: add-health-endpoint
title: Add /health endpoint
---
# Health endpoint
Expose a `/health` endpoint so load balancers can verify the service is running.
## Acceptance
- `GET /health` returns 200 with `{ "status": "ok" }`
- Response time is under 50 ms
- Endpoint is covered by API testsThe body is markdown the planner reads. The frontmatter gives the factory
the metadata it needs to sequence work (id, title, optional
depends_on). See Authoring specs
for the full guide.
npx tsx .factory/tools/run.ts add-health-endpointrun.ts:
- Translates
specs/add-health-endpoint.mdintofactory/intents/add-health-endpoint.json(1:1, derived state) - Invokes the planner; the planner writes a feature artifact and matched dev/qa packet pairs
- Invokes the developer agent, then the code reviewer, then runs build / lint / test verification and records the dev completion
- Invokes the QA agent and records the QA completion (different identity from the developer per FI-7)
- Marks the feature complete and prints a summary line (including total cost — see Cost visibility)
If anything fails, fix the issue and re-run the same command. The pipeline is idempotent.
The pre-commit hook (FI-7) ensures every started packet has a completion
before commit. The factory artifacts (specs/, factory/intents/,
factory/features/, factory/packets/, factory/completions/) ride
alongside your implementation as the governance trail.
Pass multiple spec IDs to run them in dependency order:
npx tsx .factory/tools/run.ts spec-a spec-b spec-cTopological order is computed from each spec's depends_on frontmatter.
Cycles are rejected at orchestrator entry. All transitive dependencies
must be passed explicitly — auto-resolution is out of scope.
Existing factory/intents/<intent-id>.json files (with inline spec or
referenced spec_path) continue to work. run.ts accepts an intent ID
the same way it accepts a spec ID. New work should prefer specs because
markdown is easier to author and review than JSON.
Approval gate. For intent-driven runs the status field is the
human governance gate. run.ts accepts approved, planned, and
delivered; it rejects proposed, superseded, and any missing or
unknown value with a clear error pointing at the intent file.
approved is what an operator sets on first authoring; planned and
delivered are accepted so idempotent reruns of an intent that
already progressed past planning continue to work. See Artifact
Types → Intent below for the full per-status semantics.
Spec-driven runs do NOT consult the derived intent's status;
authoring the spec at specs/<id>.md IS the gate.
The two intent shapes still supported during the transition:
Inline spec — for short, self-contained intents:
// factory/intents/customer-dashboard.json
{
"id": "customer-dashboard",
"title": "Customer dashboard",
"spec": "Provide a dashboard where users can view account activity and billing status.",
"constraints": [
"Preserve the existing public API",
"Split work into auditable dev/qa packet pairs"
],
"status": "approved",
"feature_id": null,
"created_by": { "kind": "human", "id": "alice" },
"created_at": "2025-01-15T09:00:00Z"
}Referenced spec — for long, human-authored Markdown specs that already
live in docs/specs/:
// factory/intents/016-platform-targets.json
{
"id": "016-platform-targets",
"title": "Platform Targets & Application Layer",
"spec_path": "docs/specs/016-platform-targets-and-application-layer.md",
"constraints": [
"Architectural change — must be phased per the spec",
"Preserve all invariants listed in the spec's §7"
],
"status": "approved",
"feature_id": null,
"created_by": { "kind": "human", "id": "alice" },
"created_at": "2026-04-11T09:00:00Z"
}spec_path is resolved relative to the project root, must be relative,
must not escape the project root, must point to an existing non-empty
file, and is mutually exclusive with spec. validate.ts enforces these
rules; plan.ts reads the file at plan time and hands its full contents
to the planner.
The factory has four artifact types. Each is a JSON file validated against
a schema in .factory/schemas/ (or schemas/ when working in the factory
repo itself).
All artifact paths below are relative to the artifact root. In submodule
installs this is factory/ (e.g., factory/packets/my-packet.json). When
factory is the project, this is the repo root.
A high-level spec or problem statement that the planner decomposes into a feature and dev/qa packet pairs.
intents/<intent-id>.json
Required fields:
id— kebab-case identifier (must match filename)title— one-line summary of the requested outcomespec(orspec_path) — planner input describing the desired system behavior or changestatus—proposed,approved,planned,delivered, orsupersededcreated_by— who created the intentcreated_at— ISO 8601 timestamp
Status semantics by run-input source:
- Spec-driven runs (
run.ts <spec-id>): the orchestrator generates the intent withstatus: "proposed". That value is a generator-set artifact, NOT a governance signal — the spec authoring is the approval.run.tsaccepts it and continues. - Intent-driven runs (
run.ts <intent-id>, hand-authored intents):run.tschecks the status field as a governance gate.approved— grants run authority. This is what an operator sets when hand-authoring an intent for the first time.planned/delivered— accepted for idempotent reruns of intents that already progressed past the plan phase.proposed— REJECTED with an actionable error (the operator must edit the file and setstatus: "approved").superseded— REJECTED; the intent is terminal.- missing / unknown — REJECTED.
Optional fields:
constraints— planner constraints or non-goalsfeature_id— generated feature linked to this intentplanned_at— when planning completed
A scoped unit of work. Declares what is changing, why, and which packages are affected.
packets/<packet-id>.json
Every packet has a kind: dev (implements a change) or qa (verifies
a dev packet's acceptance criteria were met). Each dev packet in a feature
must have a corresponding QA packet (FI-8).
A QA packet sets verifies to the ID of the dev packet it reviews, and
lists that dev packet in dependencies so the factory sequences them
automatically — QA only becomes ready after dev completes.
Required fields:
id— kebab-case identifier (must match filename)kind—devorqatitle— one-line summaryintent— what is changing and whyacceptance_criteria— testable conditions for completenessscope.packages— which packages are affectedowner— who is responsiblecreated_at— ISO 8601 timestamp
QA-specific fields:
verifies— ID of the dev packet this QA packet reviews (required forqa, forbidden fordev)
Lifecycle status (dev packets):
draft → ready → implementing → review_requested → changes_requested → review_approved → completed
Review states (review_requested, changes_requested, review_approved) apply only to dev packets.
QA packets follow: draft → ready → implementing → completed.
Optional fields:
started_at— when work began (normally set bytools/start.ts)status— lifecycle status (see above)branch— git branch name for code review (set byrequest-review.ts)review_iteration— number of review round-trips completed (default 0)dependencies— packet IDs that must be completed firstmodel— model tier override (high,medium,low)instructions— additional agent instructions (merged with persona instructions)feature_id— parent feature IDtags— freeform labels
Evidence that a packet's implementation is done. Created by complete.ts,
not by hand.
completions/<packet-id>.json
Required fields:
packet_id— must reference an existing packetcompleted_at— ISO 8601 timestampcompleted_by— identity ({ kind, id })summary— what was doneverification—{ tests_pass, build_pass, lint_pass, ci_pass }(all booleans)
A planned execution unit that decomposes into dev/qa packet pairs.
features/<feature-id>.json
Required fields:
id— kebab-case identifier (must match filename)intent— what the project should do when this feature is completeacceptance_criteria— feature-level success conditionsstatus—planned,executing,completed,deliveredpackets— ordered list of packet IDs (dev and qa)created_by— identity
Human authors spec → run.ts plans, develops, reviews, verifies → done
not_started → in_progress → completed
A packet moves through states based on which artifacts exist:
| State | Condition |
|---|---|
not_started |
No completion, started_at is null |
in_progress |
No completion, started_at is set |
completed |
Completion record exists |
FI-1 — One completion per packet.
FI-4 — Completion requires verification. Build, lint, and test must have been run before a completion is recorded.
FI-7 — Commit-time completion enforcement and identity separation. A commit must not include implementation files while any started packet lacks a completion. Enforced by the pre-commit hook. A QA packet must not be completed by the same identity that completed its dev counterpart.
FI-8 — Every dev packet in a feature must have a QA counterpart.
For each dev packet in a feature, a QA packet with verifies pointing to that dev packet
must exist in the same feature. Abandoned/deferred packets are exempt.
FI-9 — No cyclic packet dependencies. The dependency graph across all packets must be a DAG. Cycles cause permanent blocked state.
- Packet
kindmust bedevorqa - QA packets must set
verifiesto a valid dev packet ID - Dev packets must not set
verifies - Packet and feature
acceptance_criteriamust be non-empty - Packet IDs must match filenames (kebab-case)
- Feature
packetsmust reference existing packet IDs - Identity objects must have
kindandidfields - Orphaned completions are errors
When installed as a submodule at .factory/, tool paths use .factory/tools/....
When working in the factory repo itself, use tools/... directly.
The factory has three commands operators run.
npx tsx .factory/tools/run.ts <spec-id> [<spec-id>...]Single-command pipeline. Plans each spec, executes dev packets with code review, runs QA verification, marks each feature complete. Idempotent — safe to re-run after failures. Accepts intent IDs for backward compatibility with hand-authored intents.
npx tsx .factory/tools/status.ts # human-readable report
npx tsx .factory/tools/status.ts --json # machine-readable JSON
npx tsx .factory/tools/status.ts --feature <id> # scoped to a featurenpx tsx .factory/tools/validate.tsSchema validation + referential integrity + invariant enforcement.
The lifecycle scripts below are the protocol surface for moving a packet through its states. The same scripts back two modes:
- Autonomous mode —
run.ts <spec-id>. The orchestrator callsstart,request-review, andcompleteas library functions while driving the develop / verify phases. Agents perform the work but do not invoke those three CLIs themselves; the prompts the factory ships explicitly say so.review.tsis the one exception — the code reviewer calls it to record its verdict, because that's how the pipeline learns approve vs. request-changes. - Manual mode — humans (or self-driving agents) invoke the lifecycle CLIs directly to walk a packet through its states. This is the back-compat surface and the way to drive a stuck packet forward when the autonomous run bailed out.
All four lifecycle scripts are idempotent — re-invocation on the same state is a no-op.
npx tsx .factory/tools/start.ts <packet-id>Claims a packet and marks it started before implementation begins.
npx tsx .factory/tools/request-review.ts <packet-id>
npx tsx .factory/tools/request-review.ts <packet-id> --branch <branch-name>Transitions a dev packet from implementing (or changes_requested) to
review_requested. Captures the current git branch (or uses --branch
override) and sets the branch field on the packet. Increments
review_iteration on re-requests after changes_requested.
npx tsx .factory/tools/review.ts <packet-id> --approve
npx tsx .factory/tools/review.ts <packet-id> --request-changesRecords a code review decision on a dev packet in review_requested
status. --approve transitions to review_approved (developer can now
call complete.ts). --request-changes transitions to
changes_requested (developer addresses feedback, then calls
request-review.ts again). Review feedback lives in git (branch diffs,
git notes) — not in factory artifacts.
npx tsx .factory/tools/complete.ts <packet-id> [--summary "..."]Runs verification (build, lint, test), then creates a completion record.
Dev packets must be in review_approved status before completion.
npx tsx .factory/tools/execute.ts <feature-id>
npx tsx .factory/tools/execute.ts <feature-id> --jsonStateless action resolver for feature-level execution. Used by agents
under manual control or by run.ts when driving the develop/verify phases.
npx tsx .factory/tools/plan.ts <spec-or-intent-id>
npx tsx .factory/tools/plan.ts <spec-or-intent-id> --jsonPlanner handoff resolver. Reads a spec or intent artifact and tells the planner whether it needs to decompose work, wait for approval, or hand off to the pipeline.
Feature lifecycle:
planned → executing → completed → delivered
Packet lifecycle:
Dev packets: draft → ready → implementing → review_requested → changes_requested → review_approved → completed
QA packets: draft → ready → implementing → completed
Operator path:
- Author
specs/<spec-id>.md - Run
npx tsx .factory/tools/run.ts <spec-id>— drives plan, develop, review, verify, done
Agent path inside a run (managed by run.ts, not the operator):
- Dev agent:
start.ts→ implement →request-review.ts→ code_reviewer runsreview.ts --approve→complete.ts - QA agent:
start.ts→ verify →complete.ts - Natural flow per story: dev packet (developer ↔ code_reviewer loop) → QA packet (qa)
The full factory-native flow runs autonomously from spec to completed feature:
- Human authors
specs/<spec-id>.md(preferred) or, for backward compatibility,factory/intents/<intent-id>.json(withspecorspec_path) - Run
npx tsx .factory/tools/run.ts <spec-id> [<spec-id>...] - Plan phase — orchestrator translates spec → intent (1:1) and invokes the planner; the planner writes:
- one
factory/features/<feature-id>.jsonartifact withstatus: "planned" - dev/qa packet pairs in
factory/packets/ - packet dependencies, change classes, and acceptance criteria
feature.intent_idlinkage
- one
- Develop phase — for each dev packet (in dependency order):
- Developer agent implements and signals via
request-review.ts - Code reviewer agent calls
review.ts --approveor--request-changes - On
--request-changes, developer reworks; loop bounded bymax_review_iterations - On approval, completion is recorded with the developer's identity
- Developer agent implements and signals via
- Verify phase — for each QA packet:
- QA agent verifies (distinct identity from dev — FI-7)
- Completion is recorded with the QA identity
- Done — feature marked complete, summary printed (with total cost where reportable)
Pipeline properties:
- Idempotent — re-running resumes from artifact state on disk
- Provider-agnostic — codex, claude, copilot (configure via
pipeline.providers) - Failover-aware —
persona_providersaccepts an ordered list for cross-CLI failover; abstraction providers may declare within-CLImodel_failover(see Provider failover) - Recovery-aware — known failure scenarios are auto-recovered with bounded retries; lint/test failures always escalate (see Recovery)
- Cost-visible — every run reports total cost; configurable caps at run/packet/per-day scope (see Cost visibility)
- Observable — typed events stream to
factory/events/<run-id>.jsonl(see Event observability) - No human gates after spec authoring — completion IS acceptance
- Bounded review —
max_review_iterations(default 3) caps rework cycles
When installed in a host project as a git submodule:
. # Host project root
├── factory.config.json # Project-specific configuration
├── CLAUDE.md # AI instructions for the project
├── AGENTS.md # Agent operating constraints
├── .factory/ # Factory submodule (hidden, tooling only)
│ ├── schemas/ # JSON schemas for all artifact types
│ ├── tools/ # Factory tooling
│ │ ├── config.ts # Configuration loader
│ │ ├── run.ts # Pipeline entry point
│ │ ├── validate.ts # Schema + integrity validation
│ │ ├── status.ts # Status & next action
│ │ ├── plan.ts # Planner handoff resolver
│ │ ├── execute.ts # Feature execution resolver
│ │ ├── start.ts # Packet claim command
│ │ ├── complete.ts # Completion record generator
│ │ ├── request-review.ts # Code review request
│ │ ├── review.ts # Code review decision
│ │ ├── completion-gate.ts # Pre-commit FI-7 enforcement
│ │ ├── output.ts # Terminal output formatting
│ │ └── test/ # Tooling tests
│ ├── hooks/ # Git hooks
│ │ └── pre-commit # Completion gate + validate
│ ├── templates/ # Setup templates
│ ├── setup.sh # Installation script (Linux/macOS)
│ ├── setup.ps1 # Installation script (Windows)
│ └── docs/
│ └── integration.md # Detailed integration guide
├── specs/ # Human-authored specs (markdown + frontmatter)
├── factory/ # Factory artifacts (visible, one directory)
│ ├── intents/ # Derived from specs (or hand-authored back-compat)
│ ├── features/ # Planned execution units
│ ├── packets/ # Work unit declarations
│ ├── completions/ # Implementation evidence
│ ├── events/ # Per-run event streams (JSONL)
│ ├── cost/ # Per-invocation cost records
│ └── escalations/ # Structured failure records when recovery escalates
└── src/ # Host project source (any language)
Key separation: Tooling lives in .factory/ (hidden submodule).
Artifacts live in factory/ (visible, single directory). The factory_dir
config field points to the tooling submodule, artifact_dir points to
the artifact directory. Tools resolve paths via resolveArtifactRoot()
and resolveFactoryRoot().
# From your project root
git submodule add https://github.com/custodyzero/factory.git .factory
./.factory/setup.shThe setup script:
- Installs factory dependencies (isolated in
.factory/node_modules/) - Copies template
factory.config.json,CLAUDE.md, andAGENTS.mdto your project root (no-clobber) - Creates
factory/directory with artifact subdirectories - Configures
git config core.hooksPath .factory/hooks
See docs/integration.md for detailed integration guide.
Factory is open source under Apache 2.0 and will remain so permanently.
The Apache 2.0 license governs the source code. It does not grant rights to use Factory brand assets. See the CustodyZero brand repository for brand usage policy.