Use when producing a technical design spec for a feature or Jira story. Enforces consistent spec structure covering data model, API surface, permissions, offline-sync rules where applicable, and web/mobile UI. Run before writing-plans.
64
75%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./plugins/sdlc/skills/writing-specs/SKILL.mdCore principle: A spec says WHAT to build — not HOW to implement it line by line. Every section must be concrete enough that an implementation agent can execute without asking questions.
Announce at start: "I'm using the writing-specs skill to produce this spec."
writing-plans consumes this output)save_path := docs/superpowers/specs/<STORY-KEY>.md # story key is globally unique -> any agent derives this path with no Jira lookup, e.g. docs/superpowers/specs/CER-2037.md
branch := spec/<STORY-KEY>
pr_title := docs(spec): <STORY-KEY> <story summary>
PR merged -> post breadcrumb comment on the story # human reference only; other agents still derive save_path from the story key, not from this commentbash ${CLAUDE_PLUGIN_ROOT}/scripts/jira-site-guard.sh || exit 1
acli jira workitem comment create --key <STORY-KEY> \
--body "Spec: docs/superpowers/specs/<STORY-KEY>.md | PR: <PR_URL>"Guard: If you did not successfully fetch the Jira ticket before calling this skill, STOP and return to fetch it first. Never generate spec content from repository files alone.
Artifact encoding contract: unpadded tables, no section dropped, one-line N/A, verbatim contracts, rationale as annotation, prose < 10 lines between headings. plugins/sdlc/refs/artifact-encoding.md
# [Feature Name] — Technical Spec
**Story:** [JIRA-KEY — link or key]
**Feature:** [link to docs/features/*.md if available]
**Date:** YYYY-MM-DD
## Overview
[What this builds and why — 2 sentences max. No implementation detail.]
## Data Model
### New Entities
| Field | Type | Nullable | Default | Notes |
| --- | --- | --- | --- | --- |
| id | uuid | No | gen_random_uuid() | PK |
| ... | ... | ... | ... | ... |
Entity base class: choose the correct base class per the project's entity/ORM conventions (see project-context) — state explicitly which and why.
### Modified Entities
[Which existing entity, which fields added/removed/changed — one row per change]
### Relationships
[Text ERD — explicit cardinality]
- Organisation (1) → (many) [Entity] via organisation_id
- [Entity] (1) → (many) [ChildEntity] via [entity]\_id
## API Surface
### New Endpoints
| Method | Path | Auth/Route Type | Description |
| --- | --- | --- | --- |
| GET | /[entity] | [auth type] | List all for org |
| POST | /[entity] | [auth type] | Create |
| GET | /[entity]/:id | [auth type] | Get by ID |
| PATCH | /[entity]/:id | [auth type] | Update |
| DELETE | /[entity]/:id | [auth type] | Soft delete |
Auth/route type — state exactly one per endpoint, using the project's route/auth conventions (see project-context). Distinguish at minimum:
- authenticated, tenant/org context required
- authenticated, full user object resolved (for creates and auditable mutations)
- public, no auth (only for genuinely public endpoints)
### Request / Response Shapes
```typescript
// POST /[entity]
interface Create[Entity]Body {
field: string; // required
optionalField?: string;
}
interface [Entity]Response {
id: string;
field: string;
createdAt: string;
}
```
All TypeScript interfaces must be fully typed. No `any`.
### Permissions
| Role | List | Get | Create | Update | Delete |
| --- | --- | --- | --- | --- | --- |
| [role-1] | ✓ own org | ✓ own org | ✓ | ✓ own | ✗ |
| [role-2] | ✓ assigned | ✓ assigned | ✗ | ✗ | ✗ |
| [role-3] | ✓ org | ✓ org | ✓ | ✓ | ✓ |
Derive role/permission names from the project's permission source (see project-context) — never invent. Locate that source and enumerate the actual roles before filling this table.
## Backend Implementation
**API / infra layer:** the project's API/infra definition (see the project-context workspace→path table) — state the exact file.
**Application layer:** state the exact files this introduces, following the project's layering conventions (implementations, mappers, DTOs, or the project's equivalents), one per operation.
**Handlers / entry points:** the project's request entry points — thin, one per operation; state exact paths.
**Permissions:** the project's permission source (see project-context) — list the exact permission keys to add.
## Web UI
Omit this section if not web-scoped.
**Pages:**
| Route | File | Description |
| --- | --- | --- |
| /[domain] | src/pages/[domain]/index.tsx | List view |
| /[domain]/[id] | src/pages/[domain]/[id].tsx | Detail view |
**Components:** the project's web component location (see project-context) — list names and responsibilities.
**Data hooks:** the project's server-data fetching layer (per the web stack in project-context) — list the data hooks/queries needed, one per operation.
**State:** client state changes (per the project's web stack in project-context) — list which stores/slices change and why.
**Route permissions:** the project's route-permission config (see project-context) — which roles can access which routes.
## Mobile UI
Omit this section if not mobile-scoped.
**Screens:**
| Route | File | Description |
| --- | --- | --- |
| /(app)/[domain] | app/(app)/[domain]/index.tsx | List screen |
| /(app)/[domain]/[id] | app/(app)/[domain]/[id].tsx | Detail screen |
**Online reads:** REST via the project's API client — same endpoints as Web UI.
**Offline reads:** if the project uses offline sync (see project-context), state the read hook/location and the data it exposes. Every offline read MUST apply a tenant/org-scoped filter so a device only ever sees its own tenant's data (e.g. a JOIN or WHERE constraint on the owning tenant/org id) — describe the exact scoping filter for this entity.
**Offline writes:** if the project uses offline sync, list the offline write/transaction builders needed (one per mutating operation) and state their location per the project's sync layer (see project-context).
## Offline Sync
Omit this section entirely if the project has no offline sync — see project-context.
If the project uses offline sync, the spec MUST cover:
- **Which entities sync** — list every entity that must be available offline.
- **Scoping / tenant-isolation filter** — the rule that limits each device's synced data to its own tenant/org. State it explicitly per entity. Bucket/partition scoping in the sync config alone is often insufficient — the read path must also enforce a tenant/org-scoped filter (see Offline reads above).
- **Read hooks** — where offline reads live and what each exposes.
- **Write hooks / transaction builders** — list each one needed, what it uploads to, and which API endpoint it calls.
Define these using the project's offline-sync technology and config location (see project-context); state the exact files.
## Error Handling
| Scenario | Behaviour | HTTP Status |
| --- | --- | --- |
| [Entity] not found | Return 404 | 404 |
| Unauthorised role | Return 403 | 403 |
| Validation failure | Return 400 with field errors | 400 |
| [Domain-specific error] | [behaviour] | [status] |
**Offline behaviour:** [what happens if user submits offline write while disconnected — does it queue, fail, or show an error?]
## Out of Scope
- [Thing that sounds related but is NOT in this spec]
- [Future enhancement explicitly excluded]
## Open Questions
[Any decision still needed — each must have a suggested default so implementation can proceed]
- [ ] [Question] — Suggested default: [answer]Always derive from the project's permission source (see project-context) — never invent. Enumerate the actual roles before writing the Permissions table.
gate := [
no TBDs — every open question has a concrete answer OR a flagged decision with a suggested default,
every new endpoint has a row in the Permissions table,
every offline-synced entity has an Offline Sync section with an explicit tenant/org-scoped filter (only if the project uses offline sync),
entity relationships state exact cardinality (1:1, 1:N, M:N),
auth/route type stated for every endpoint, using the project's conventions,
all TypeScript interfaces fully typed — no `any`,
spec says WHAT to build — no HOW (no line-by-line implementation instructions),
Web / Mobile / Offline Sync sections omitted if not applicable to this story,
entity base class choice stated and justified per the project's conventions
]
before saving -> every item in gate must hold| Anti-pattern | Fix |
|---|---|
| "Add a column to the table" | "Add field x: string to [Entity] entity + generate migration" |
| Inventing a role name | Check the project's permission source first (see project-context) |
| Missing tenant/org-scoped filter in an offline read | Always enforce a tenant/org-scoped filter on every offline read |
| Editing a generated sync schema/types file in the spec | Never — it's generated; spec the sync-config/rule change instead |
any in TypeScript interfaces | Fully type every field |
| TBD without a suggested default | Add "Suggested default: X" so impl can proceed |
| Spec contains implementation code | Move to agent instructions, not spec |
| Offline Sync section absent for an offline-synced entity | Add the section — the sync implementer needs it |
7741177
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.