Files
element-call-Github/FEATURES_SPEC/AGENTS.md
fkwpandClaude Fable 5.1 89856df37f Add the FEATURES_SPEC process for turning explorations into specs and PRs
FEATURES_SPEC/AGENTS.md describes how an exploration is distilled into a
feature spec and how a spec is turned into PRs against a current main; the
product spec template is the sidecar a brainstorming session fills in.

The folder is excluded from oxfmt: a product spec is immutable once a spec
leaves draft, and the process documents are prose in their own style.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 15:28:39 +02:00

20 KiB

FEATURE_SPEC — Agent Instructions

This folder holds feature specs distilled from experiments. A spec is the durable artifact of an exploration; the exploration branch is not. Each spec must contain enough information for an agent to produce mergeable PRs against a current main without access to the original branch.

What this folder is not

  • Not a design-doc archive. Specs that are not intended to be implemented do not belong here.
  • Not a place for narrative. Prose that does not constrain the implementation or its verification should be cut.
  • Not a substitute for the PR description. The spec explains why and what must be true; the PR explains this diff.

File naming

YYYY-MM_Snake_Case_Title.md — e.g. 2026-09_Raise_Hand_Improvements.md

The filename is immutable once committed. Renames break links from PRs, issues and the drift log. If scope changes fundamentally, write a new spec and set supersedes: / superseded_by:.

A sidecar YYYY-MM_Snake_Case_Title.product.md holds the product spec — user stories, functional requirements, success criteria. Copy _TEMPLATE.product.md. Product owns it; see "Brainstorming: the product spec".

An optional sidecar YYYY-MM_Snake_Case_Title.notes.md holds lessons learned, narrative, dead-end logs and anything else that is worth keeping but does not constrain the implementation. Rules:

  • The sidecar is never load-bearing. Agents do not read it to derive implementation, and nothing in it may contradict or extend the spec. If a note constrains the work, it belongs in ## Decisions, ## Rejected alternatives or ## Measurements instead.
  • Agents may append to it (an observation from a drift check, a wrong turn during implementation) and must never rewrite or delete existing content.
  • An agent that is blocked may read the sidecar to draft an open question, but quotes what it found rather than acting on it.
  • No sidecar is required. An empty one is worse than none.

Style: facts only

Applies to the spec file only. The product spec is prose by design and the notes sidecar is unconstrained. The spec is re-read on every regeneration, where prose costs context and dilutes the contract, so:

  • Bullets, not paragraphs. Declarative present tense. No motivation, no background, no summary of what the code currently does beyond locating the change.
  • Name things instead of describing them — a path, a symbol, an event type, a command, an event id. A reference is shorter and more precise than a sentence about it. Which kind of reference is allowed depends on the section; see "Reference durability".
  • One line per decision plus at most one sentence of rationale. If the rationale needs more, the extra goes in the sidecar.
  • No judgement adjectives (clean, elegant, proper, obviously) and no hedging (should probably, might be nice). Either it is a constraint or it is not in the spec.
  • ## Problem is at most five bullets.
  • Target under 200 lines for the whole spec; over 300 lines, split the feature or move material to the sidecar.

The test for any line: does it constrain the implementation, constrain its verification, or record a measured fact? If not, delete it or move it to the sidecar.

Reference durability

The spec must stay implementable against a main nobody has seen yet, so which references are allowed depends on how fast they rot. Most to least durable:

  1. Protocol identifiers — m.reaction, m.call.member, m.relates_to, a reaction key. MSC-bound; effectively permanent.
  2. Observable behaviour. Permanent.
  3. Concept names — membership, participation, session.
  4. Test names. Survive file moves and renames, because the test moves with the code.
  5. Exported symbols. Survive refactors sometimes.
  6. File paths. Rot fast.
  7. Line numbers. Never.

Per section:

  • ## Problem, ## Measurements, ## Slicing plan, ## Drift log — any reference, paths and internal symbols included. These describe the codebase as of base. A reference going stale here is a finding for the drift check, not a defect, and removing the concrete references would leave the drift check with nothing to verify.
  • ## Decisions, ## Acceptance criteria — levels 1 to 4 only. No file paths, no internal symbols. This is the part that has to hold against a future main.
  • An acceptance criterion that is not user-observable is tagged [internal] and anchored by its test name, not by the symbol under test. The test name moves with a refactor; the symbol does not.

[internal] ACs are legitimate and expected: an invariant that no current consumer can observe is exactly what protects the consumer that does not exist yet. Tagging them keeps visible which ACs defend a product requirement and which defend a code invariant.

Required frontmatter

---
status: draft            # see state machine below
blocked_from: null       # state to return to when leaving `blocked`
title: Raise hand improvements
owner: <human handle>    # a human, always
signoff:                 # join condition for leaving exploration.signoff
  eng: pending
  design: pending
  product: pending
experiment_ref: <branch name or commit sha of the spike, if any>
base: <sha of main the spec was validated against>
repos: [matrix-js-sdk, element-call]    # in dependency order
specs: [MSC4143]         # protocol documents touched, if any
supersedes: []
superseded_by: null
---

base is mandatory and is what makes drift detectable. owner is a human even when every section was drafted by an agent.

Brainstorming: the product spec

The usual entry point is a human handing an agent a one-line feature idea. The output of that session is <stem>.product.md, not code and not a feature spec. Copy _TEMPLATE.product.md and fill it.

Ownership: an agent may create and fill this file during brainstorm. From draft onwards product owns it and agents do not edit it — not to fix wording, not to resolve a marker, not to reconcile it with the code. Agents raise mismatches in the feature spec's ## Open questions.

If product's source of truth lives outside the repo (a tracker, a design doc), put its URL in the header **Source**: line and keep the requirement text and ids in the repo anyway. The ids are what the feature spec references; a link alone cannot be traced.

Agent behaviour during the session:

  • Ask, do not assume. An unanswered question is recorded as [NEEDS CLARIFICATION: <the question>], inline where the answer would go. Filling a gap with a plausible guess is the main failure mode of this state — the marker is a deliverable, not a defect.
  • Ask in one round of at most five questions, ordered by how much the answer changes the shape of the feature. Do not interview.
  • No implementation in the product spec. No file paths, symbols, libraries, event types, API shapes or framework names. If a requirement cannot be stated without them, it is not a product requirement.
  • Every user story must be independently deployable and demonstrable on its own. A story that only makes sense once another story ships is not a story; merge it.
  • More than four priority levels means more than one feature. Split before draft.
  • Never invent an SC number. Ask for the target, or mark it [NEEDS CLARIFICATION].

For protocol and infrastructure work, success criteria may be expressed as observable client behaviour or a wire-level invariant ("a client joining mid-session converges within one sync"), but still never as a code path or an internal API.

FR-### and SC-### ids are immutable once assigned. Product may revise the document at any time, including after in_production, but a changed requirement gets a new id and the old one is marked superseded — never edited in place. Renumbering silently breaks every AC that references it, and there is no way to detect that afterwards.

A new or changed FR after implement does not amend the current spec's scope. It is either a new feature spec or the next iteration; the mapping table simply shows an unmapped FR, which is the intended signal.

Traceability

The product spec states intent; the feature spec states what must be true and how it is checked. They are kept in sync by ids, not by duplication.

  • Every FR-### maps to at least one acceptance criterion. Tag the AC with its origin: AC2 [FR-003] — <observable behaviour>.
  • Every SC-### maps to an entry in ## Measurements with a method. An SC with no measurement blocks exploration.signoff.
  • Every user story maps to at least one slice. Slice order follows story priority (P1 first) unless a technical dependency overrides it, and the override is recorded in the slicing plan with its reason.
  • An AC with no FR origin is allowed and expected — implementation-level invariants have no counterpart in the product spec. The converse is not: an unmapped FR is an incomplete spec.
  • Agents do not resolve a mismatch between the two. The product spec wins on intent, the feature spec wins on verification; a genuine contradiction is blocked.

Status state machine

Transitions are explicit; an agent may perform only the ones marked (agent).

brainstorm ─► draft ─► exploration ─► implement ─► qa ─► in_production
                           │              │        │
                           └──────────────┴────────┴──► blocked ──► (blocked_from)
                                                           │
                                                           └──► abandoned

Exploration has sub-states, written dotted in status:

exploration.scoping ─► exploration.iterating ─► exploration.signoff
        ▲                       │                      │
        └───────────────────────┴──────────────────────┘
  • brainstorm → draft: human. Requires <stem>.product.md to exist with at least one P1 story carrying acceptance scenarios, every FR and SC id assigned, and every remaining [NEEDS CLARIFICATION] copied into the feature spec's ## Open questions.
  • draft → exploration.scoping: human. Requires ## Problem and ## Out of scope. Scoping before the branch exists is what keeps exploration from becoming the long-lived branch this folder replaces.
  • exploration.scoping → .iterating: human. experiment_ref must name the branch.
  • .iterating → .signoff: human. Requires ## Decisions, ## Acceptance criteria, ## Rejected alternatives and ## Measurements to be complete, no [NEEDS CLARIFICATION] left in the product spec, and full traceability (below).
  • .signoff → implement: only when all three sign-offs in signoff: are ok. This is a join, not a sequence — a single reviewer cannot advance it. On entering implement, the exploration branch stops being a base: it is reference material, and implementation branches off current main (see "Turning a spec into PRs").
  • .signoff → .iterating: human, on any sign-off returning changes. Reset the rejecting role to pending; leave the others as they are.
  • implement → qa: (agent) when every acceptance criterion is covered, as recorded in ## PRs, and the feature flag is enabled in the QA environment.
  • qa → in_production: human. The flag is on by default in production.
  • qa → implement: (agent) when a QA finding needs code. See "QA feedback".
  • any active state → blocked: (agent) when drift or an unresolved question prevents progress. Requires a drift-log entry and blocked_from: <state>. Do not guess and continue. Leaving blocked returns to blocked_from, never to a fixed state.
  • any state → abandoned: human only.

in_production is terminal. A production regression does not reopen the spec: it either is an ordinary bug fix with no spec, or it is a new spec. The acceptance criteria remain as the regression contract (see "QA feedback").

QA feedback

The point of the qa state is not to record that testing happened. It is to make the spec a better regeneration source than it was before implementation. So:

  • Every reproduced QA finding becomes either a new acceptance criterion or a new entry in ## Rejected alternatives. Never a prose note, never only a bug tracker link.
  • (agent) may append ACs during qa, with provenance: AC7 [qa: <issue ref>] — <observable behaviour> plus its check. Appending is allowed; rewriting or reordering existing ACs is not.
  • A finding that invalidates a decision does not become an AC. It sets blocked — a decision is human-owned in every state.
  • A finding that is out of scope goes to ## Out of scope with its issue ref, so the next regeneration does not treat it as a gap.

This also applies after in_production: a production bug that reveals a missing AC is appended with [prod: <ref>] provenance without any state change.

Required sections

Order matters — agents read top-down and the first three sections are the contract.

  1. ## Problem — up to five bullets. What breaks today, for whom, observably, with the file or symbol where it breaks.
  2. ## Decisions — the choices the exploration settled, each with a one-line rationale. Human-owned. An agent must never add, remove or reword a decision. If a decision is missing, set blocked and ask.
  3. ## Acceptance criteria — the load-bearing section. Each item must be mechanically checkable and name its check:
    • AC1 [FR-002] — <observable behaviour> / check: yarn vitest run src/reactions/ReactionsReader.test.tsx -t "backfill"
    • AC2 [FR-002] — <observable behaviour> / check: manual, <exact steps>
    • AC3 [internal] — <invariant> / check: yarn vitest run <file> -t "<test name>" An acceptance criterion without a check is a wish; reject the spec (blocked).
  4. ## Rejected alternatives — what the experiment tried and why it failed, including measurements. This exists so regeneration does not re-walk dead ends. If this section is empty for a spec with an experiment_ref, the spec is incomplete.
  5. ## Measurements — numbers from the experiment with the method used to obtain them (command, hardware, load profile). Numbers without a method are not usable as regression baselines and must be marked unverified. A baseline states its direction: a count that unrelated work grows — tests passing, test files — is a floor (≥ n); a cost — latency, CPU, bundle size — is a ceiling (≤ n). An exact count is stale as soon as main moves and is not a drift finding.
  6. ## Out of scope — explicit non-goals, so agents stop expanding the diff.
  7. ## Slicing plan — ordered, independently mergeable PRs (see below).
  8. ## Drift log — append-only, agent-written.
  9. ## PRs — append-only list of #<n> — <state> — <ACs covered>.

Optional: ## Open questions (blocks exploration.signoff), ## Migration / compat (mandatory whenever wire format, event schema, or a published API changes).

Turning a spec into PRs

Run these steps in order. Do not skip step 2.

  1. Fetch and pin. git fetch origin and record origin/main's sha as head. Never start from the exploration branch, and never rebase it — the branch is reference material, not a base.
  2. Drift check against base..head:
    • Do all files, symbols and modules named in the spec still exist?
    • Have the invariants in ## Decisions been contradicted by changes in main?
    • Do the commands in ## Acceptance criteria still run? Write the result as a drift-log entry. On a contradicted decision: set blocked, do not implement.
  3. Re-derive the slicing plan if drift changed it. Each slice must:
    • build and pass CI on its own,
    • be reviewable in one sitting (target < 400 changed lines, hard stop at 800 — split further rather than exceeding it),
    • map to at least one acceptance criterion, and
    • land behind a flag or be inert if it is not the last slice of a user-visible change.
  4. Implement one slice at a time, branch spec/<spec-slug>/<n>-<short> off the pinned head. Tests for the slice's ACs go in the same PR as the code. A PR merges only when every check of every AC it lists passes on that PR: automated checks in CI, manual checks performed by a human reviewer with the result recorded in the PR description. An AC is covered once such a PR has merged.
  5. Open the PR linking the spec path and listing the ACs it covers. Update ## PRs and status.
  6. Do not touch merged or reviewed slices. See the regeneration rule.

The regeneration rule

Regeneration from spec is only allowed before a human has reviewed the PR.

Once review has started, the PR is a shared artifact: rebase, fix forward, push incremental commits. Regenerating discards the reviewer's context and silently invalidates their approvals. If regeneration is unavoidable after review, close the old PR with an explanation and open a new one — never force-push a fresh generation over a reviewed branch.

Cross-repo and protocol work

When repos has more than one entry, PRs are opened in repos order and each PR body states which upstream PR it depends on. Merging out of order is a spec violation, not an optimisation.

When specs is non-empty:

  • the implementation PR references the section of the MSC it implements,
  • unmerged MSCs require the implementation to be feature-flagged and documented as unstable (unstable prefixes, not stable identifiers), and
  • any deviation from the MSC text is recorded in ## Drift log, not silently resolved in favour of the code.

Drift-log format

Append-only, newest last:

### 2026-09-14 — drift check (base 4f2a1c9 → head 9b17e02)
- `ReactionsReader.getLastReactionEvent` now takes a `CallMembership` instead of an
  event id → AC2 and AC3 check commands still valid, slice 3 needs rework
- BLOCKING: decision D2 assumes a membership event change may keep a hand raised; main
  now clears all reaction relations on membership replacement. Needs owner decision.

Drift entries are never rewritten or deleted, including entries that were later resolved. The log is the record of why the implementation diverged from the exploration.

Hard rules

  • Never fill a [NEEDS CLARIFICATION] marker with a guess, in any state.
  • Never put file paths, symbols or library names in the product spec.
  • Never reference a file path or an internal symbol in ## Decisions or ## Acceptance criteria. Behaviour, protocol identifiers, concepts and test names only.
  • Never edit <stem>.product.md after draft, and never renumber an FR or SC id.
  • Never add a line that does not constrain the implementation, constrain its verification, or record a measured fact. It goes in the sidecar or nowhere.
  • Never modify ## Decisions, ## Problem, or ## Acceptance criteria. These are the human contract. Propose changes in ## Open questions and set blocked.
  • Never invent measurements or acceptance checks. Absent is better than plausible.
  • Never widen scope past ## Out of scope, even when the fix looks trivial.
  • Never open a PR while status is draft, any exploration.*, or blocked.
  • Never delete a spec file. Terminal states are in_production or abandoned.
  • If the spec and current main disagree about how the system works, main wins as description, the spec wins as intent — record the difference and stop.

Spec template

---
status: draft
blocked_from: null
title:
owner:
signoff:
  eng: pending
  design: pending
  product: pending
experiment_ref:
base:
repos: []
specs: []
supersedes: []
superseded_by: null
---

## Problem

## Decisions
- D1 — <decision> — <why>

## Acceptance criteria
- AC1 [FR-###] — <observable behaviour>
  - check: <command or exact manual steps>
- AC2 [internal] — <invariant>
  - check: <command, anchored by test name>

## Rejected alternatives
- <approach> — <why it failed> — <evidence>

## Measurements
- <metric>: <value> (method: <command / setup>)

## Out of scope

## Slicing plan
1. <slice> — covers AC1 — depends on: none

## Drift log

## PRs