Forge has one artifact: the spec. It is born cheap and grows.
The states and the legal transitions between them live in the binary:
forge workflow prints them in lifecycle order, with what each state means
and who has to act next. This page explains the process around them and
keeps no second copy of the machine.
The backlog is not a folder: it is the specs in proposed and accepted.
An epic is not a type: it is a spec that has children. Collapsing those into
one artifact removes the copying, and the drift that copying causes.
Only forge writes status, and every move appends a line to the spec's
History section saying who did it and why.
forge accept SPEC-004 # into the queue
forge approve SPEC-004 # the contract is rightAnyone can run either. --by defaults to git config user.name, so
nobody has to name themselves to move their own work forward. Forge does
not have a maintainer list to check a handle against, the same way git
does not check whether you were allowed to author a commit.
The scrutiny a team wants happens in the pull request review it already
does, not in a second Forge-specific approval. forge validate still
catches what is objectively wrong regardless of who touched what: a
missing contract, an uncovered promise, a dependency cycle, a contract
that drifted after it was approved.
A contract may raise questions instead of answering them. They live in the
spec's ## Open questions section, one per line, or None. when there are
none:
## Open questions
- OQ1: does the retry window close on the first success or on the last?forge approve refuses while any question remains and forge validate
warns, so a contract is never approved around an open decision. Settle each
question, or write None..
Criteria live in the spec as a list:
## Acceptance criteria
- AC1: a saved card can be reused without retyping it
- AC2: charging twice with the same request id charges onceThey must be verifiable by someone who did not write them: a command, a
test, a request and its response. forge approve refuses while a criterion
names none of those, so a vague criterion is settled before code starts. The
reviewer marks each one with evidence, and forge check reports every
criterion with no task in tasks.md or no evidence line in review.md;
forge validate raises the same coverage as a warning, and as an error at
done when the evidence is missing.
forge new picks the next free number from the spec folders committed under
.forge/specs/ on every remote-tracking ref, not just main, so a number a
parallel branch already pushed is skipped. The read is best-effort, not a
reservation: a branch whose refs are not present locally, or two branches that
mint the same number before either pushes, can still collide, and two specs
with the same title share a folder and surface only as a git conflict at
merge. forge new and forge accept never fetch, so in a repository with
more than one person run git fetch before forge new to read the latest
branches. forge accept keeps the id when the only match is the spec's own
published branch, and forge validate still fails on a real duplicate once it
lands.
A spec does not start from an empty repository. The architect records what
already exists in the spec's ## Existing state section
(.forge/specs/<id>/spec.md): what this builds on, what it reuses, the
conventions that apply, and the duplication it avoids. Planning reads that
one survey instead of filling a second one. Before splitting the spec into
phases, the orchestrator checks it against the delivered work: first forge capabilities, which derives the current contracts grouped by capability and
marks what each one supersedes, then the contracts it points at, forge status for what is open, and the code that already does part of the job.
The architect names the modules it builds on in the contract too, so the
reuse is written where it survives archiving. forge validate warns when a
live spec has not surveyed the existing state.
A spec that spans several deliverables becomes a parent:
forge new "Card payments" # SPEC-002
forge new "Saved cards" --parent SPEC-002 --covers AC1,AC3 # SPEC-004
forge new "Reconciliation" --parent SPEC-002 --covers AC4 # SPEC-005Coverage is declared by the child and computed by the tool. The parent file is never edited when a child advances, which is why two branches closing different children never conflict.
$ forge status SPEC-002
coverage
AC1 SPEC-004 (implementing)
AC3 SPEC-004 (implementing)
AC4 SPEC-005 (accepted)
AC2 NOT COVERED
NOT COVERED is a warning while everything is open and an error once any
child is closed: that is the moment where a promise silently disappears.
depends_on: [SPEC-003, SPEC-011@contract]
needs:
- "a retry engine"
blocked_by_external:
- what: "production credentials"
who: anadepends_on: [SPEC-003]waits for that spec to bedone.depends_on: [SPEC-011@contract]waits only for its contract to be approved. This is how a front end and a back end are built in parallel against the same agreed shape.needsis a dependency on something that has no spec yet. The only way out is to create it and replace the text with its id, which is what stops "we will look at it later" from meaning "never".blocked_by_externalis out of Forge's hands, so it makes it visible with an owner.
forge start refuses while any of these is open, lists what is ready
instead, and accepts --force when you decide otherwise. The exception is
written into the spec, where the reviewer will see it.
When forge approve runs, it stores a fingerprint of the contract. Every
spec that starts against it with @contract records which fingerprint it
agreed to.
If the contract changes afterwards, forge validate fails and names the
specs building against a version that no longer exists. It also fails if a
contract is edited after approval without being approved again. This is the
classic back-and-front integration failure, caught in the pull request
instead of in staging.
By default the work reaches origin only at forge submit, so a spec in
flight lives on one machine. forge push <id> closes that gap: it commits
the pending tree with the subject chore(<id>): checkpoint <state> and
pushes the spec branch with its upstream set. Run it after each phase and
before a session ends, so another machine can fetch the branch and resume.
With push: on in .forge/project.md's frontmatter, forge advance runs
the same checkpoint at every state boundary; without that scalar forge advance never touches git or the network. It refuses main and master,
so the checkpoint only ever publishes a spec branch.
.forge/specs/SPEC-004-saved-cards/
├── spec.md the spec, its contract and the existing-state survey
├── plan.md the phases
├── tasks.md the phases as checkboxes, ticked as they land
└── review.md verdict and evidence per criterion
Every spec owns a folder. The plan, tasks and review are written as the work
moves and stay in the tree when the spec is done; forge archive no longer
deletes anything, because the folder is the durable record. The spec's
spec.md holds the contract, the existing-state survey, the history and the
state.
That is the durable record a later spec reads to know what exists: the
contract of every done spec (with the interfaces and the modules it
builds on), the decisions that outlive a spec, and the conventions. The
plan, tasks and review of a closed spec stay beside its contract, so a
future task can read what was surveyed, what each phase did and what the
review found; the contract is still what a future task is expected to read
first.
A spec ends as a pull request. forge submit <id> pushes the branch and
opens it through gh, recording the number and URL on the spec; when gh
is missing it prints the git push and gh pr create commands. Nothing in
Forge merges a pull request: a person reviews and merges on the forge host.