specification-first work
rule
agree on the user outcome and owning contract before substantial implementation. code implements an approved decision; it does not silently make one.
why
code written before the contract buries product decisions in code: shipped behavior becomes the only authority for questions nobody answered, and every later consumer reverse-engineers intent from an unrelated service instead of reading a specification. the invariant: code never silently resolves a material product choice.
change: introduce a `suspended` account statespec-first: spec defines sign-in, invoice, restore transitions and acceptanceevidence → service implements the approved contract.code-first: service adds the flag → behavior becomes the decision → lateragents reverse-engineer boundaries and make conflictingassumptions.
consequence
a service adds a suspended flag without specifying whether suspended users
can sign in, receive invoices, or be restored. the code's behavior becomes the
only authority:
- "can suspended users reset their password?" is answered by diving into an unrelated service's login logic, not a spec.
- two agents fixing bugs in the same subsystem make different assumptions → silent incompatibilities.
- a third team reverse-engineers the boundaries from code instead of approved intent.
- with three incompatible implementations, refactoring requires coordination across every consumer — no shared specification exists to update once.
workflow
- update the owning specification, decision, handbook, schema, or other non-code artifact first — the visible handoff for every other agent. every Markdown or MDX artifact begins with parseable frontmatter.
- identify the owner: product, domain, API, data, or UI contract.
- state current fact, target, non-goals, and acceptance evidence.
- resolve material choices before irreversible work.
- add the active change to each code file's change ledger before editing its implementation.
- update the owner again with the behavior change and its final evidence.
do not
- build a large slice from scattered chat history.
- treat an agent draft as user approval.
- leave an open decision hidden in code.
- start by editing implementation while the contract or non-code change record is still absent or stale.
good and bad
bad
// account service - implementation without specificationasync function suspendAccount(userId: string) {await db.update(accounts).set({ status: "suspended" }).where(eq(accounts.id, userId));// undefined: can suspended users sign in? receive invoices? be restored?// the implementation accidentally becomes the product decision}
good
# specifications/account-states.mdxtitle: account state suspendedtransitions:- from: activeto: suspendedtrigger: admin_request | fraud_detectionbehavior:signIn: falseinvoiceDelivery: falserestoration: admin_onlynon_goals:- automatic restoration after period- partial access during suspensionacceptance_evidence: |POST /accounts/{id}/suspend → status=200, account.state=suspendedPOST /auth/login with suspended account → status=403, error=account_suspended
// account service - implements approved specificationasync function suspendAccount(userId: string) {await db.update(accounts).set({ status: "suspended" }).where(eq(accounts.id, userId));// verification links to named state transitionawait logger.info("account_suspended", { userId, state: "suspended" });}
the specification defines suspended, its transitions, sign-in/invoice
behavior, non-goals, and acceptance evidence first; the service then
implements that approved contract.