@pavp/storywright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "storywright",
3
+ "version": "0.1.0",
4
+ "description": "PM skills for Claude Code — turn ambiguous inputs (prompts, screenshots, Figma links) into Jira-ready user stories.",
5
+ "author": "pavp",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/pavp/storywright",
8
+ "repository": "https://github.com/pavp/storywright",
9
+ "skills": [
10
+ "skills/story-generate",
11
+ "skills/story-refine",
12
+ "skills/story-split",
13
+ "skills/story-from-figma",
14
+ "skills/_components/clarification-questions",
15
+ "skills/_components/acceptance-criteria",
16
+ "skills/_components/invest-checklist",
17
+ "skills/_components/definition-of-done",
18
+ "skills/_components/business-rules",
19
+ "skills/_components/edge-cases",
20
+ "skills/_components/analytics-events",
21
+ "skills/_components/risks-and-dependencies",
22
+ "skills/_components/jira-wiki-formatter"
23
+ ],
24
+ "tags": ["product-management", "user-stories", "jira", "agile", "invest"]
25
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Story Skills contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # storywright
2
+
3
+ PM skills for Claude Code that turn ambiguous inputs — vague prompts, half-baked stories, screenshots, Figma links — into **Jira-ready user stories** with acceptance criteria, edge cases, risks, analytics, and Definition of Done.
4
+
5
+ > Inspired by [`deanpeters/Product-Manager-Skills`](https://github.com/deanpeters/Product-Manager-Skills) (CC BY-NC-SA 4.0). Clean-room MIT rewrite — no copied content. Frontmatter shape, body skeleton, and splitting-pattern selection draw on patterns from that repo; prose, component taxonomy, dual-format output, and risk model are this repo's own. Release pipeline modeled on [`pavp/wavefront`](https://github.com/pavp/wavefront). Methodological credit: Bill Wake (INVEST, 2003), Mike Cohn (*User Stories Applied*, 2004), Dan North (BDD / Given-When-Then), Richard Lawrence & Peter Green (*Humanizing Work* splitting patterns).
6
+
7
+ ## What it is
8
+
9
+ A **skills pack for Claude Code**, not a runtime. No LLM inside. Skills are Markdown files that Claude Code reads as instructions. The npm package is a thin installer that copies them to `~/.claude/skills/storywright/`.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @pavp/storywright
15
+ storywright install
16
+ ```
17
+
18
+ Restart Claude Code so the skills are picked up.
19
+
20
+ Alternatives:
21
+
22
+ - **Git clone + symlink** for contributors:
23
+ ```bash
24
+ git clone git@github.com:pavp/storywright.git
25
+ ln -s "$(pwd)/storywright/skills" ~/.claude/skills/storywright
26
+ ```
27
+ - **ZIP upload to claude.ai**:
28
+ ```bash
29
+ storywright zip story-generate
30
+ # → dist/story-generate.zip — upload via Claude.ai UI
31
+ ```
32
+
33
+ ## Use
34
+
35
+ In Claude Code:
36
+
37
+ ```
38
+ generate a user story: Permitir login con Google
39
+ ```
40
+
41
+ ```
42
+ refine this story:
43
+ <paste a half-baked story>
44
+ ```
45
+
46
+ ```
47
+ generate stories from this Figma: https://www.figma.com/file/…
48
+ ```
49
+
50
+ ```
51
+ this story is too big, split it:
52
+ <paste a story that visibly mixes flows>
53
+ ```
54
+
55
+ Outputs always include both `story.jira-wiki.md` (Jira wiki markup) and `story.standard.md` (CommonMark).
56
+
57
+ ## Skills
58
+
59
+ ### Top-level
60
+ | Skill | When to use |
61
+ |---|---|
62
+ | `story-generate` | Ambiguous prompt, screenshot, or fresh story request |
63
+ | `story-refine` | Existing story that's incomplete or weakly specified |
64
+ | `story-split` | Story that fails INVEST on I / E / S — too big |
65
+ | `story-from-figma` | Figma file or frame URL → one or more stories |
66
+
67
+ ### Components (composed by the top-level skills)
68
+ - `clarification-questions` — minimum critical questions
69
+ - `acceptance-criteria` — Given/When/Then ACs
70
+ - `invest-checklist` — INVEST self-check with verdict
71
+ - `definition-of-done` — DoD checkbox block
72
+ - `business-rules` — policy invariants
73
+ - `edge-cases` — boundary/network/concurrency/permission/state
74
+ - `analytics-events` — funnel events with payload taxonomy
75
+ - `risks-and-dependencies` — risks + blocking deps
76
+ - `jira-wiki-formatter` — dual-format renderer
77
+
78
+ ## Multimodal
79
+
80
+ | Input | Runtime | Notes |
81
+ |---|---|---|
82
+ | Text | Native | Always available |
83
+ | Images (PNG/JPG) | Claude vision | Drop file into chat |
84
+ | Figma links | MCP Figma server | See `skills/story-from-figma/mcp-figma-notes.md` |
85
+
86
+ ## CLI
87
+
88
+ ```bash
89
+ storywright install # copy skills to ~/.claude/skills/storywright/
90
+ storywright list # show available + installed skills
91
+ storywright validate # lint skill files (frontmatter + structure)
92
+ storywright zip <skill-name> # build a ZIP for Claude.ai upload
93
+ storywright uninstall # remove from ~/.claude/skills/
94
+ ```
95
+
96
+ ## Multi-provider stance
97
+
98
+ Skills are written in **format-neutral Markdown** with optional `<claude-specific>` XML blocks. Non-Claude LLMs ignore the XML; Claude reads it. No adapters shipped — downstream concern.
99
+
100
+ ## Releases
101
+
102
+ `semantic-release` + Conventional Commits + GitHub Actions + npm Trusted Publishing (OIDC). Push to `main` → bump → publish. Single `latest` channel for v1.
103
+
104
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const scriptsDir = resolve(__dirname, "..", "scripts");
8
+
9
+ const [, , cmd, ...rest] = process.argv;
10
+
11
+ const COMMANDS = {
12
+ install: "install-skills.mjs",
13
+ uninstall: "uninstall-skills.mjs",
14
+ list: "list-skills.mjs",
15
+ zip: "zip-skill.mjs",
16
+ validate: "validate-skills.mjs",
17
+ };
18
+
19
+ function usage(exit = 0) {
20
+ console.log(`storywright — PM skills for Claude Code
21
+
22
+ Usage:
23
+ storywright install Copy skills to ~/.claude/skills/storywright/
24
+ storywright uninstall Remove skills from ~/.claude/skills/
25
+ storywright list Show available + installed skills
26
+ storywright zip <skill-name> Build a ZIP for Claude.ai upload
27
+ storywright validate Lint skill files (frontmatter + structure)
28
+
29
+ Repo: https://github.com/pavp/storywright`);
30
+ process.exit(exit);
31
+ }
32
+
33
+ if (!cmd || cmd === "--help" || cmd === "-h") usage(0);
34
+ if (!COMMANDS[cmd]) {
35
+ console.error(`Unknown command: ${cmd}\n`);
36
+ usage(1);
37
+ }
38
+
39
+ const script = resolve(scriptsDir, COMMANDS[cmd]);
40
+ const child = spawn(process.execPath, [script, ...rest], { stdio: "inherit" });
41
+ child.on("exit", (code) => process.exit(code ?? 1));
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@pavp/storywright",
3
+ "version": "1.0.0",
4
+ "description": "PM Skills pack for Claude Code — turn ambiguous inputs (prompts, screenshots, Figma links) into Jira-ready user stories.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "skills",
9
+ "product-management",
10
+ "user-stories",
11
+ "jira",
12
+ "agile",
13
+ "invest",
14
+ "ai-agent"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "pavp <pedrovillarreal@gmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/pavp/storywright.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/pavp/storywright/issues"
24
+ },
25
+ "homepage": "https://github.com/pavp/storywright#readme",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "bin": {
34
+ "storywright": "./bin/storywright.mjs"
35
+ },
36
+ "files": [
37
+ "bin/",
38
+ "scripts/",
39
+ "skills/",
40
+ ".claude-plugin/",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "validate": "node scripts/validate-skills.mjs",
46
+ "test": "node --test tests/*.test.mjs",
47
+ "postinstall": "node scripts/postinstall-hint.mjs",
48
+ "prepare": "husky"
49
+ },
50
+ "devDependencies": {
51
+ "@commitlint/cli": "^19.5.0",
52
+ "@commitlint/config-conventional": "^19.5.0",
53
+ "@semantic-release/changelog": "^6.0.3",
54
+ "@semantic-release/git": "^10.0.1",
55
+ "@semantic-release/github": "^11.0.0",
56
+ "@semantic-release/npm": "^12.0.1",
57
+ "@semantic-release/release-notes-generator": "^14.0.1",
58
+ "conventional-changelog-conventionalcommits": "^8.0.0",
59
+ "husky": "^9.1.6",
60
+ "semantic-release": "^24.1.2"
61
+ }
62
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { cp, mkdir, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { SKILLS_DIR, pathExists } from "./lib/skills.mjs";
6
+
7
+ const target = join(homedir(), ".claude", "skills", "storywright");
8
+
9
+ async function main() {
10
+ if (await pathExists(target)) {
11
+ await rm(target, { recursive: true, force: true });
12
+ console.log(`• Removed existing ${target}`);
13
+ }
14
+ await mkdir(target, { recursive: true });
15
+ await cp(SKILLS_DIR, target, { recursive: true });
16
+ console.log(`✓ Installed skills to ${target}`);
17
+ console.log(` Restart Claude Code (or reload) for skills to be picked up.`);
18
+ }
19
+
20
+ main().catch((err) => {
21
+ console.error("✗ Install failed:", err);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,94 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname } from "node:path";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ export const REPO_ROOT = join(__dirname, "..", "..");
8
+ export const SKILLS_DIR = join(REPO_ROOT, "skills");
9
+
10
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
11
+
12
+ export function parseFrontmatter(raw) {
13
+ const match = raw.match(FRONTMATTER_RE);
14
+ if (!match) return { frontmatter: null, body: raw };
15
+ const fmText = match[1];
16
+ const body = match[2];
17
+ const fm = parseSimpleYaml(fmText);
18
+ return { frontmatter: fm, body };
19
+ }
20
+
21
+ function parseSimpleYaml(text) {
22
+ const out = {};
23
+ let currentList = null;
24
+ let currentKey = null;
25
+ for (const rawLine of text.split(/\r?\n/)) {
26
+ const line = rawLine.replace(/\s+$/, "");
27
+ if (!line.trim()) continue;
28
+ if (line.startsWith(" - ") || line.startsWith("- ")) {
29
+ const value = line.replace(/^\s*-\s*/, "").trim();
30
+ if (currentList) currentList.push(stripQuotes(value));
31
+ continue;
32
+ }
33
+ const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
34
+ if (!m) continue;
35
+ const key = m[1];
36
+ const rest = m[2];
37
+ if (rest === "" || rest == null) {
38
+ out[key] = [];
39
+ currentList = out[key];
40
+ currentKey = key;
41
+ } else {
42
+ out[key] = stripQuotes(rest);
43
+ currentList = null;
44
+ currentKey = key;
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function stripQuotes(v) {
51
+ if (typeof v !== "string") return v;
52
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
53
+ return v.slice(1, -1);
54
+ }
55
+ return v;
56
+ }
57
+
58
+ export async function findSkillFiles(root = SKILLS_DIR) {
59
+ const out = [];
60
+ async function walk(dir) {
61
+ const entries = await readdir(dir, { withFileTypes: true });
62
+ for (const e of entries) {
63
+ const full = join(dir, e.name);
64
+ if (e.isDirectory()) {
65
+ await walk(full);
66
+ } else if (e.isFile() && e.name === "SKILL.md") {
67
+ out.push(full);
68
+ }
69
+ }
70
+ }
71
+ await walk(root);
72
+ return out;
73
+ }
74
+
75
+ export async function loadSkill(skillFilePath) {
76
+ const raw = await readFile(skillFilePath, "utf8");
77
+ const { frontmatter, body } = parseFrontmatter(raw);
78
+ return {
79
+ path: skillFilePath,
80
+ relPath: relative(REPO_ROOT, skillFilePath),
81
+ raw,
82
+ frontmatter: frontmatter ?? {},
83
+ body,
84
+ };
85
+ }
86
+
87
+ export async function pathExists(p) {
88
+ try {
89
+ await stat(p);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { join, relative } from "node:path";
4
+ import { findSkillFiles, loadSkill, pathExists, SKILLS_DIR } from "./lib/skills.mjs";
5
+
6
+ const installed = join(homedir(), ".claude", "skills", "storywright");
7
+
8
+ async function main() {
9
+ const repoFiles = await findSkillFiles(SKILLS_DIR);
10
+ const installedExists = await pathExists(installed);
11
+
12
+ const tops = [];
13
+ const components = [];
14
+ for (const f of repoFiles) {
15
+ const skill = await loadSkill(f);
16
+ const entry = {
17
+ name: skill.frontmatter.name ?? "(unnamed)",
18
+ desc: skill.frontmatter.description ?? "",
19
+ path: relative(SKILLS_DIR, f),
20
+ };
21
+ if (skill.relPath.includes("_components/")) components.push(entry);
22
+ else tops.push(entry);
23
+ }
24
+
25
+ console.log(`Repo skills (${repoFiles.length} total)`);
26
+ console.log(`Installed at ~/.claude/skills/storywright/: ${installedExists ? "YES" : "NO"}\n`);
27
+
28
+ console.log(`Top-level skills (${tops.length}):`);
29
+ for (const s of tops) console.log(` • ${s.name.padEnd(22)} — ${s.desc.slice(0, 80)}`);
30
+
31
+ console.log(`\nComponent skills (${components.length}):`);
32
+ for (const s of components) console.log(` · ${s.name.padEnd(28)} — ${s.desc.slice(0, 70)}`);
33
+ }
34
+
35
+ main().catch((err) => {
36
+ console.error("✗ List failed:", err);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // Skip noise in CI and in nested installs.
3
+ if (process.env.CI || process.env.npm_config_global !== "true") process.exit(0);
4
+
5
+ console.log(`
6
+ ┌─────────────────────────────────────────────────────────────┐
7
+ │ storywright installed │
8
+ │ Run: storywright install │
9
+ │ to copy the skills into ~/.claude/skills/storywright/ │
10
+ │ Then restart Claude Code to pick them up. │
11
+ └─────────────────────────────────────────────────────────────┘
12
+ `);
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { pathExists } from "./lib/skills.mjs";
6
+
7
+ const target = join(homedir(), ".claude", "skills", "storywright");
8
+
9
+ async function main() {
10
+ if (!(await pathExists(target))) {
11
+ console.log(`• Nothing to uninstall (${target} does not exist)`);
12
+ return;
13
+ }
14
+ await rm(target, { recursive: true, force: true });
15
+ console.log(`✓ Removed ${target}`);
16
+ }
17
+
18
+ main().catch((err) => {
19
+ console.error("✗ Uninstall failed:", err);
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import { join, dirname, basename } from "node:path";
3
+ import { findSkillFiles, loadSkill, pathExists, SKILLS_DIR, REPO_ROOT } from "./lib/skills.mjs";
4
+
5
+ const REQUIRED_FM = ["name", "description", "trigger", "intent", "version"];
6
+ const REQUIRED_SECTIONS = ["## Purpose", "## Application"];
7
+ const KEBAB_RE = /^[a-z][a-z0-9-]*$/;
8
+
9
+ async function main() {
10
+ const files = await findSkillFiles();
11
+ if (files.length === 0) {
12
+ console.error("✗ No SKILL.md files found under skills/");
13
+ process.exit(1);
14
+ }
15
+
16
+ const errors = [];
17
+ const skillNames = new Set();
18
+ const componentPaths = new Set();
19
+
20
+ for (const f of files) {
21
+ const skill = await loadSkill(f);
22
+ const rel = skill.relPath;
23
+ const fm = skill.frontmatter;
24
+
25
+ for (const key of REQUIRED_FM) {
26
+ if (!fm[key] || (Array.isArray(fm[key]) && fm[key].length === 0)) {
27
+ errors.push(`${rel}: missing required frontmatter '${key}'`);
28
+ }
29
+ }
30
+
31
+ if (fm.name) {
32
+ if (!KEBAB_RE.test(fm.name)) {
33
+ errors.push(`${rel}: name '${fm.name}' must be kebab-case`);
34
+ }
35
+ if (fm.name.length > 64) {
36
+ errors.push(`${rel}: name length ${fm.name.length} > 64`);
37
+ }
38
+ if (skillNames.has(fm.name)) {
39
+ errors.push(`${rel}: duplicate skill name '${fm.name}'`);
40
+ } else {
41
+ skillNames.add(fm.name);
42
+ }
43
+ }
44
+
45
+ if (fm.description && fm.description.length > 200) {
46
+ errors.push(`${rel}: description length ${fm.description.length} > 200`);
47
+ }
48
+
49
+ for (const section of REQUIRED_SECTIONS) {
50
+ if (!skill.body.includes(section)) {
51
+ errors.push(`${rel}: missing required section '${section}'`);
52
+ }
53
+ }
54
+
55
+ if (rel.includes("skills/_components/")) {
56
+ componentPaths.add(`_components/${basename(dirname(f))}`);
57
+ }
58
+ }
59
+
60
+ for (const f of files) {
61
+ const skill = await loadSkill(f);
62
+ const composes = Array.isArray(skill.frontmatter.composes) ? skill.frontmatter.composes : [];
63
+ for (const dep of composes) {
64
+ if (!componentPaths.has(dep)) {
65
+ errors.push(`${skill.relPath}: composes references missing component '${dep}'`);
66
+ }
67
+ }
68
+ }
69
+
70
+ if (errors.length) {
71
+ console.error(`✗ Validation failed (${errors.length} error${errors.length === 1 ? "" : "s"}):\n`);
72
+ for (const e of errors) console.error(` - ${e}`);
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log(`✓ ${files.length} skills validated (${componentPaths.size} components, ${files.length - componentPaths.size} top-level)`);
77
+ }
78
+
79
+ main().catch((err) => {
80
+ console.error("✗ Validator crashed:", err);
81
+ process.exit(2);
82
+ });
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { REPO_ROOT, SKILLS_DIR, pathExists } from "./lib/skills.mjs";
6
+
7
+ const skillName = process.argv[2];
8
+ if (!skillName) {
9
+ console.error("Usage: storywright zip <skill-name>");
10
+ console.error("Example: storywright zip story-generate");
11
+ process.exit(1);
12
+ }
13
+
14
+ async function main() {
15
+ const skillDir = join(SKILLS_DIR, skillName);
16
+ if (!(await pathExists(skillDir))) {
17
+ const compDir = join(SKILLS_DIR, "_components", skillName);
18
+ if (!(await pathExists(compDir))) {
19
+ console.error(`✗ Skill not found: ${skillName}`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ const sourceDir = (await pathExists(skillDir)) ? skillDir : join(SKILLS_DIR, "_components", skillName);
24
+ const outDir = join(REPO_ROOT, "dist");
25
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
26
+
27
+ const outFile = join(outDir, `${skillName}.zip`);
28
+ const result = spawnSync("zip", ["-r", outFile, "."], { cwd: sourceDir, stdio: "inherit" });
29
+
30
+ if (result.status !== 0) {
31
+ console.error(`✗ zip exited with status ${result.status}`);
32
+ process.exit(result.status ?? 1);
33
+ }
34
+ console.log(`✓ Wrote ${outFile}`);
35
+ }
36
+
37
+ main().catch((err) => {
38
+ console.error("✗ Zip failed:", err);
39
+ process.exit(1);
40
+ });
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: acceptance-criteria
3
+ description: Write acceptance criteria for a user story in Given/When/Then style. Covers happy path, edge cases, and explicit non-goals. Returns only the AC block, never a full story.
4
+ trigger: "internal use by story-* skills"
5
+ intent: Component skill that produces a complete, testable acceptance-criteria block. Pure renderer — does not ask the user questions; relies on context already gathered upstream.
6
+ version: 1.0.0
7
+ inputs:
8
+ - story-context
9
+ - edge-cases-block
10
+ outputs:
11
+ - acceptance-criteria-block
12
+ ---
13
+
14
+ ## Purpose
15
+
16
+ Generate ACs that are **independently testable** and map directly to QA cases. Use Given/When/Then phrasing. Include negative cases. Pair with `[[edge-cases]]` so each edge case has at least one matching AC.
17
+
18
+ ## When to use
19
+
20
+ Invoked by `story-generate` and `story-refine` after the body of the story is drafted.
21
+
22
+ ## Inputs & interpretation
23
+
24
+ - **story-context** — title, user role, expected behavior summary
25
+ - **edge-cases-block** — list of edge cases that must each be covered
26
+
27
+ ## Application (step-by-step)
28
+
29
+ 1. Start with the happy path: one AC for the primary success scenario.
30
+ 2. Add one AC per failure mode (auth failure, network error, validation error, permission denied).
31
+ 3. Add one AC per explicit edge case from `[[edge-cases]]`.
32
+ 4. Add one negative AC: "When [precondition not met], Then [no-op or error UX]."
33
+ 5. Each AC follows the pattern:
34
+ ```
35
+ **AC-N: <short title>**
36
+ - Given <precondition>
37
+ - When <action>
38
+ - Then <observable outcome>
39
+ - And <secondary observable outcome> (optional)
40
+ ```
41
+ 6. Number ACs `AC-1`, `AC-2`, …. Stable numbering — never renumber when adding new ones in iterations.
42
+ 7. Emit only the AC block. Do NOT include explanations, headers above `## Criterios de Aceptación`, or surrounding prose.
43
+
44
+ ## Examples
45
+
46
+ ### Good
47
+
48
+ ```
49
+ **AC-1: Successful Google login**
50
+ - Given the user is on the login screen
51
+ - When they tap "Continue with Google" and authorize a valid account
52
+ - Then they are redirected to the dashboard within 3 seconds
53
+
54
+ **AC-2: Account linking — same email exists with password**
55
+ - Given an account with email user@x.com already exists with email/password
56
+ - When the user logs in with Google using user@x.com
57
+ - Then a "Link your account" confirmation screen is shown
58
+ - And the user must enter their existing password to merge
59
+
60
+ **AC-3: Cancellation in OAuth consent**
61
+ - Given the user has tapped "Continue with Google"
62
+ - When they cancel the Google consent screen
63
+ - Then they are returned to the login screen with no error toast
64
+ ```
65
+
66
+ ### Bad
67
+
68
+ ```
69
+ - It should work with Google
70
+ - Handle errors properly
71
+ - The user can log in
72
+ ```
73
+
74
+ (no preconditions, no observables, untestable)
75
+
76
+ ## Splitting signal
77
+
78
+ **Multiple When/Then pairs in one AC ⇒ story needs splitting.** Each `When` and each `Then` should be singular. If you find yourself writing AC-1 with three `When`s, that's not a story with rich ACs — it's three stories collapsed into one. Stop and hand off to `[[story-split]]`.
79
+
80
+ Multiple `Given`s are fine — preconditions stack.
81
+
82
+ ## Common Pitfalls
83
+
84
+ - Vague verbs (`work properly`, `handle correctly`, `improved performance`, `faster`). Use observable, measurable outcomes ("loads in <2s", "success toast shown").
85
+ - Compound ACs (multiple `When`s or `Then`s). Split.
86
+ - "So that" restates "I want to" — the value statement disappears. Dig until you find the real motivation ("so I don't lose work if the tab crashes" — not "so I can save").
87
+ - Technical tasks disguised as stories ("As a developer, I want to refactor X"). If there's no observable user outcome, it's an eng task, not a story.
88
+ - Forgetting non-goals — write at least one "Then it does NOT…" or mark as scope boundary.
89
+ - Renumbering when adding ACs mid-iteration. Append, never renumber.
90
+
91
+ ## References
92
+
93
+ - [[edge-cases]]
94
+ - [[definition-of-done]]
95
+ - [[business-rules]]
96
+
97
+ <claude-specific>
98
+ Use extended thinking to enumerate failure modes before drafting. Cache the Given/When/Then template.
99
+ </claude-specific>