spec-gate
Spec layer between overview.md and sprints — testable criteria, auto contract gate.
SKILL.mdskills/shared/spec-gate/SKILL.md
GitHubSpec Gate
Sits between docs/backlog/overview-<scope-slug>.md (business scope) and docs/backlog/sprints/ (execution
plan). One spec per feature/capability, written by product-analyst, in business language but with
a testable acceptance format. It becomes the scope boundary every downstream agent reads instead of
re-interpreting the overview or the sprint task description.
Document Structure
docs/specs/
├── <feature>.md ← business spec (product-analyst)
└── <feature>-contract.md ← technical contract (software-architect, only when the gate fires)
scripts/spec-lint.sh checks both files and the sprint tasks that link them — § Mechanical Check.
<feature>.md — Spec File
Structure from templates/spec-template.md (installed: .dev-team-agents/templates/spec-template.md):
- Frontmatter:
status:(§ Spec Status),touches: [backend, frontend, database, mobile](layers this feature affects — list only the ones that actually change),depends_on: [<feature>, ...](other specs this one needs, or[]) - User Story: as a [role], I want [capability], so that [outcome]
- Acceptance Criteria: one or more
Given / When / Thenblocks, each headed**Scenario S<n>: <name>**— this is the only place "done" is defined; nothing outside it is in scope. IDs are stable: never renumbered, never reused; a removed scenario's ID stays retired - Out of Scope: explicit exclusions, same discipline as the scope overview
No stack, schema, or API shape in this file — that is the contract's job, not the spec's.
Gate Rule (mechanical, not requested)
Immediately after product-analyst writes a spec, evaluate:
touches.length > 1 OR the spec introduces a new API/schema/integration point
True → the orchestrating main conversation spawns software-architect automatically to write
<feature>-contract.md from .dev-team-agents/templates/contract-template.md. This does not require the user to ask for technical input — the gate
replaces that dependency. software-architect writes only the interface: request/response
shapes, schema fields, error format, and the technical dependency order between specs. It does not
touch the scope overview, does not restate the business rule, and does not make product decisions.
False (single layer, no new integration surface) → skip the contract; the sprint task links the spec alone.
Spec Status
| Status | Set by | When |
|---|---|---|
draft |
product-analyst |
The spec is written |
approved |
product-analyst, or the command that stopped on the draft spec |
The user approved its criteria (relayed by /devteam:plan or /devteam:spec, or asked at the draft stop below) |
implemented |
whichever command marks a task [x] Done (§ Task Resolution) |
Every scenario of the spec is cited by a [x] Done task's **Covers**: and no [SPEC-DRIFT] is open |
implemented (retroactive) |
product-analyst, at the Close of /devteam:spec |
Every scenario ID's latest qa-specialist verdict is matches (§ Retroactive Spec) |
- Execution starts only from an
approvedorimplementedspec. A linkeddraftspec stops the command before any agent is spawned: ask the user to approve it (AskUserQuestion) or run/devteam:planon it — never implement against unapproved criteria. - An amendment (Living Spec below) never changes
status; the Amendment Log records it. A business amendment still needs the user's confirmation of the new criteria before execution continues — that confirmation is the gate, not a status change. - A spec with no
status:key predates this field: treat it asapproved. The edit that adds the key also gives every scenario itsS<n>ID — from then onspec-lintblocks a missing one.
Task Resolution
A command that receives a sprint task instead of a free-text description resolves it before anything else:
$ARGUMENTS names |
Resolve to |
|---|---|
TASK-NNN |
The one ### TASK-NNN block under docs/backlog/sprints/ — two matches across scopes → ask which scope (AskUserQuestion) |
A sprint file or <scope-slug> <n> |
Every task of that sprint — this is /devteam:sprint's input; a single-task command asks which task |
| A spec path | The spec itself, with no task |
From the task, read **Spec**: (spec, plus contract when listed) and **Covers**: (the scenario
IDs this task delivers; a task written before the field existed covers the whole spec). The task's
scope is those scenarios only — the Scope Lock below
applies to them, not to the whole spec. A task whose **Spec**: is none keeps its own
**Acceptance Criteria**: and runs without this skill's gates.
The implementation commands (backend, frontend, fullstack, mobile, fix) flip the task's
**Status**: — [~] In progress when they spawn its agents, [x] Done at session close only when
no BLOCKER is open (fix: after its review gate; a declined review leaves [~]) — and mirror the sprint's row in docs/backlog/sprints/sprints.md (In progress
on the first task, Done on the last). /devteam:sprint sets [x] Done after the task's merge
instead (its Step 5). qa and tester read the task and never flip it. Right after marking a task
Done, the same command checks its spec against the implemented row of § Spec Status and sets
status: implemented when it holds.
When a spec was resolved, every qa-specialist prompt carries the spec path, the contract path when
one exists, and the task's **Covers**: IDs — that is what the Spec Sync Gate runs on.
Scope Lock — Execution Agents
Every coding agent (backend-developer, frontend-developer, mobile-developer,
database-specialist) reads the spec (and contract, if one exists) linked from its sprint task
before writing code, and treats the Given/When/Then blocks as the implementation boundary:
- Implement what the criteria require — nothing the criteria don't cover.
- If the task seems to need something the spec doesn't state (a field, a flow branch, an error
case), stop and ask — via
AskUserQuestionif it has a finite set of reasonable answers, otherwise flag it to the user in plain text. Do not assume and proceed. - If the spec and its contract disagree, the contract wins on interface shape; a business
contradiction goes back to
product-analyst, not a silent implementation choice.
Test-First Derivation (mechanical, not requested)
When a command's test gate resolves to running tests (TESTS_REQUIRED=yes or the key absent) and the task links a spec, tests are written before the implementing developer agent runs, not after:
- The test author for the track —
backend-test-specialist,frontend-test-specialist, ormobile-developerfor Flutter/native suites — reads the spec'sGiven/When/Thenblocks (only the task's**Covers**:IDs when a task was resolved) — the same ones the developer will treat as the implementation boundary — and writes one failing test per scenario, tagged per § Traceability. No implementation exists yet; these tests assert against the interface described in the spec (and contract, if one exists), not against code. - These tests are expected to fail (red) — that is the point. Commit them as-is; do not adjust assertions to make them pass against a stub.
- Only after the red tests exist does the developer agent implement, iterating until every derived test is green plus the rest of its own scope.
- On a spec that touches several layers, each track's test author derives only the scenarios whose
Thenis observable at its layer (the spec'stouchesand the contract say which) — a scenario is never tested twice at two layers for the same assertion. - If a
Given/When/Thenblock cannot be turned into a concrete assertion (the criterion is genuinely ambiguous about expected output, not just under-specified on an edge case), that is the<HARD-GATE>in Scope Lock firing early — stop and ask, do not write a vague or tautological test to move forward.
Every implementation command applies this per track: backend, frontend, and mobile alike. A track whose tests the developer agent writes itself (mobile-developer on Flutter/native) runs it as two spawns of that agent — tests only, then implementation.
This does not replace the Scope Lock rule above — the developer still treats the spec as the boundary — it only moves the test-specialist's read of the spec to before the code instead of after. Commands without a linked spec, or with TESTS_REQUIRED=no, are unaffected and keep the existing test-after order.
Traceability
Every test derived from a scenario carries the token [spec:<feature>#S<n>], where <feature> is
the spec's filename without .md:
- In the test's description string when the framework has one (
it('[spec:checkout#S2] rejects an expired card', …),@DisplayName, a pytestids=/docstring). - Otherwise in a one-line comment directly above the test — the exception
comments-policyrecords for this token. - One test may carry several tokens; a scenario may be covered by several tests.
The token is how the Spec Sync Gate maps criteria to evidence, and how spec-lint reports an
implemented spec whose scenario no test references.
Retroactive Spec
A feature that already exists gets its spec through /devteam:spec <feature>, never through
/devteam:plan (which scopes new work). Same format, same IDs, same statuses — no extra key. The
§ Gate Rule does not run: the interface already exists, so a contract is written only on request.
| Step | Owner | Rule |
|---|---|---|
| Draft | product-analyst |
Reads the code, tests, and docs of the feature and writes status: draft. Scenarios describe behavior observable from outside — what a user or client does and sees — never the code path. A behavior with more than one plausible reading is an Open Decision, not a guess |
| Approve | product-analyst, on the user's approval |
status: approved — verification starts only from here |
| Verify | qa-specialist |
Executes the feature against every scenario ID and returns one verdict each: matches, or diverges with what it observed. A scenario it could not exercise is diverges with the reason — never left out. Read-only |
| Decide | the user, per diverges ID |
Describe current behavior → product-analyst rewrites the scenario as built, logged - YYYY-MM-DD | product-analyst | S<n> rewritten as built | retroactive: user accepted the observed behavior; the user confirms the new wording, and qa-specialist re-verifies that ID. Keep the intended behavior → the scenario stays and is a bug, offered as /devteam:fix; never fixed inside this flow |
| Tag | the test author per touches layer (§ Test-First Derivation, step 1) |
Runs only after Decide, on the final wording. Adds [spec:<feature>#S<n>] to existing tests that already assert a scenario — description string or one-line comment only, no assertion changed — and lists the scenarios no test covers. Missing tests are offered only for matches scenarios, written only on the user's choice, change no production code, and a new test that fails today turns its scenario back into diverges |
| Close | product-analyst |
Sets status: implemented when every ID's latest verdict is matches; otherwise leaves approved and names the IDs holding it there |
A rewrite as built is a business amendment (Living Spec below) and, like every amendment, does
not change the status — the Close row does, from the verdicts. Scenarios left untested because the
user declined new tests are accepted: spec-lint keeps warning about them, and the summary lists
them.
Offer, never run. When /devteam:qa, or /devteam:fix on a non-trivial bug, targets an
existing feature that has no spec, it offers /devteam:spec <feature> with AskUserQuestion
(offer first, continue without as the second option). An accepted offer runs /devteam:spec
through its Close step, skips its own /devteam:fix offers, and hands back to the calling command
with the spec linked.
QA Validation
qa-specialist validates behavior against the linked spec's Given/When/Then blocks — the same
criteria the execution agent read, not a re-derived interpretation of the sprint task or the
original request. A criterion the spec doesn't cover is out of scope for the PASS/FAIL verdict;
note it as an exploratory finding instead (see test-strategy skill).
Living Spec — Amendment Protocol
The spec stays the source of truth after implementation too. When execution discovers that
reality doesn't match a Given/When/Then — a missing scenario, a wrong assumption, a field that
needs to change — the spec is amended in place, never silently coded around and never abandoned.
- Business-level divergence (the criteria are wrong or incomplete, no interface shape changes):
the executing agent (
backend-developer,frontend-developer,mobile-developer,database-specialist, orproduct-analystin a retroactive spec) edits the spec'sAcceptance Criteria/Out of Scopedirectly. - Interface-level divergence (a contract exists and the schema/API shape no longer holds): the
executing agent does not edit
<feature>-contract.mditself — it flags the mismatch to the user andsoftware-architect, who amends the contract and logs it in the contract's own### Amendment Log. - Either way, append one line to the spec's
### Amendment Log:- YYYY-MM-DD | <agent> | <what changed> | <why>. An amendment with no logged reason does not count — the spec is stale, not living, until the log entry exists. - Amending is not optional when a real divergence is confirmed — an agent that implements around a stale criterion without updating it has violated the scope lock above just as much as one that implemented something the spec never asked for.
Spec Sync Gate (mandatory, end of work)
Before the mandatory review handoff (code-reviewer + qa-specialist) on a spec-linked task is
considered complete, qa-specialist verifies:
- Every
Given/When/Thenin the spec still matches what was actually built — and, whenTESTS_REQUIREDis notno, at least one test carries its[spec:<feature>#S<n>]token. A scenario with no token is reported as uncovered, not silently passed. - Every
Amendment Logentry carries a reason — no blank or placeholder entries.
Report a mismatch as [SPEC-DRIFT] in the QA Report and treat it as a [BLOCKER] — deploy is
blocked until the spec is corrected, not until the QA report is edited to match the code.
Accepted drift is not the end of the story. A [SPEC-DRIFT] blocker resolves one of two ways:
the code is changed to match the spec, or the user explicitly accepts the built behavior and the
spec is amended to match it (Living Spec above). The second case is a standardization decision, not
just a spec fix — if the accepted behavior represents a pattern the team wants to hold onto (a naming
convention, a reusable shape, a path rule), route it to skills/shared/reuse-guidelines/SKILL.md
(via /devteam:rule or its passive-capture path) in addition to the Amendment Log entry. The
Amendment Log records that this spec changed; the registry is what stops the same drift from
recurring silently in the next feature.
Sprint Linkage
Each TASK-NNN in a sprint file (backlog-template skill) carries a **Spec**: field pointing at
docs/specs/<feature>.md (and <feature>-contract.md when it exists) and a **Covers**: field
listing the scenario IDs it delivers. The task does not restate the criteria — the spec is their
only home. Task ordering across waves respects depends_on from the linked specs, not just
file-level collisions.
Mechanical Check
scripts/spec-lint.sh (installed: .dev-team-agents/scripts/spec-lint.sh) runs from the Stop hook
and on demand. It is a no-op when docs/specs/ does not exist.
| Blocks (exit 1) | Warns only |
|---|---|
status/touches outside their allowed values |
A spec with no frontmatter or no status: key (legacy) — its structural findings, missing depends_on targets included, are reported here too |
A scenario without an S<n> ID, or a duplicated ID |
An implemented spec's scenario no test references — skipped when TESTS_REQUIRED=no |
depends_on naming a missing spec, or a dependency cycle |
|
| An Amendment Log line with an empty or placeholder reason | |
A task's **Spec**: path missing, or a **Covers**: ID absent from that spec |
|
A <feature>-contract.md with no <feature>.md beside it |
Fix what it reports in the spec or the task — never by deleting the scenario or the log line.