@pavp/storywright 1.1.0 → 1.3.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.
@@ -21,5 +21,11 @@
21
21
  "skills/_components/risks-and-dependencies",
22
22
  "skills/_components/jira-wiki-formatter"
23
23
  ],
24
+ "commands": [
25
+ "commands/story-generate.md",
26
+ "commands/story-refine.md",
27
+ "commands/story-split.md",
28
+ "commands/story-from-figma.md"
29
+ ],
24
30
  "tags": ["product-management", "user-stories", "jira", "agile", "invest"]
25
31
  }
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Generate user stories from a Figma file or frame URL (uses MCP Figma when available)
3
+ argument-hint: <Figma URL> [+ optional text prompt or screenshots]
4
+ ---
5
+
6
+ Invoke the `story-from-figma` skill from the storywright pack to analyze:
7
+
8
+ $ARGUMENTS
9
+
10
+ Follow the skill's procedure:
11
+
12
+ 1. Phase 0 — verify MCP Figma server is available. If not, ask the user to either install one, paste PNG exports, or describe the flows.
13
+ 2. Phase 0.5 — detect companion text / screenshots. Use text as canonical for `User Story / Scope / Business Goal`; Figma as canonical for `Components / States / Flows`. Surface conflicts as BLOCKING clarifications. Never silently pick a winner.
14
+ 3. Phase 1 — inventory pages and frames; group by prototype links into flows.
15
+ 4. Phase 2 — per flow: identify goal, entry point, states, components. Score per-inference confidence (HIGH/MEDIUM/LOW). Mark MEDIUM/LOW with `> ⚠️ Assumed:` blockquotes.
16
+ 5. Phase 3 — for any flow that fails INVEST (Small), hand off to `/story-split` before generating.
17
+ 6. Phase 4 — invoke `story-generate` per flow. Emit dual outputs + `flow-summary.md` mapping stories → frames for traceability.
18
+
19
+ Output in the input language (text language wins if mixed inputs).
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Transform an ambiguous prompt, screenshot, or Figma link into a Jira-ready user story
3
+ argument-hint: <prompt | paste story | attach image | figma URL>
4
+ ---
5
+
6
+ Invoke the `story-generate` skill from the storywright pack to handle the following input:
7
+
8
+ $ARGUMENTS
9
+
10
+ Follow the skill's full procedure:
11
+
12
+ 1. Detect input types present (text / image / figma-link / mixed).
13
+ 2. If multi-source, apply the source-priority matrix and surface conflicts as BLOCKING clarifications BEFORE drafting.
14
+ 3. Run gap check via `clarification-questions` component. Ask at most 3 critical questions if anything is blocking.
15
+ 4. Fill the CORE sections (Title, Summary, User Story, Acceptance Criteria, Definition of Done).
16
+ 5. Fill optional sections only if they have real content (drop empty ones).
17
+ 6. Run INVEST self-check. If NOT A STORY / NEEDS REFINEMENT / RUN A SPIKE / SPLIT RECOMMENDED — STOP and hand off accordingly.
18
+ 7. Render dual outputs: `story.jira-wiki.md` (Jira wiki markup) and `story.standard.md` (CommonMark).
19
+ 8. If clarifications remain, emit `clarifications.md` and mark the story DRAFT.
20
+
21
+ Output in the input language (preserve es/en).
@@ -0,0 +1,19 @@
1
+ ---
2
+ description: Audit an existing user story and fill gaps in place
3
+ argument-hint: <paste existing story>
4
+ ---
5
+
6
+ Invoke the `story-refine` skill from the storywright pack to audit and improve this story:
7
+
8
+ $ARGUMENTS
9
+
10
+ Follow the skill's procedure:
11
+
12
+ 1. Parse the existing story into the section taxonomy. Mark each section: present / missing / weak.
13
+ 2. Gap-check weak sections via `clarification-questions`. Ask only BLOCKING questions.
14
+ 3. Detect language and preserve it in output.
15
+ 4. Fill missing/weak sections via component skills. Preserve original wording where good.
16
+ 5. Append a "Refinement log" at the end listing what changed.
17
+ 6. Run INVEST. If verdict is `SPLIT RECOMMENDED`, stop and recommend `/story-split` instead.
18
+ 7. Render dual outputs via `jira-wiki-formatter`.
19
+ 8. Emit `clarifications.md` if assumptions remain unresolved.
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Split an oversize story into an epic + child stories using INVEST + Humanizing Work patterns
3
+ argument-hint: <paste oversize story>
4
+ ---
5
+
6
+ Invoke the `story-split` skill from the storywright pack to split this story:
7
+
8
+ $ARGUMENTS
9
+
10
+ Follow the skill's procedure:
11
+
12
+ 1. Run `invest-checklist` and apply the pre-split gates:
13
+ - If Valuable FAILS → NOT A STORY. Don't split. Stop.
14
+ - If Testable / Negotiable FAILS → fix in place, don't split. Stop.
15
+ - If Estimable fails on unknowns → recommend a spike. Stop.
16
+ - If Independent / Estimable / Small fails → continue.
17
+ 2. Apply the 9-pattern catalog in order. Name the first pattern that fits + the core complexity (meta-pattern).
18
+ 3. Calibrate to Cynefin domain (Obvious/Complicated vs Complex vs Chaotic).
19
+ 4. Draft a split plan as a Markdown table with rationale, pattern, and proposed children.
20
+ 5. Run the strategic evaluation: does the split reveal low-value work? Are children equal-sized?
21
+ 6. STOP and ask the user to approve / adjust / cancel. Never auto-split.
22
+ 7. After approval: write `epic.md` + one stub per child + `split-plan.md` decision trail.
23
+ 8. Coherence check + recursive re-split for children still >1 sprint.
24
+ 9. Save artifacts under `./stories/<epic-slug>/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pavp/storywright",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "PM Skills pack for Claude Code — turn ambiguous inputs (prompts, screenshots, Figma links) into Jira-ready user stories.",
5
5
  "keywords": [
6
6
  "claude",
@@ -38,6 +38,7 @@
38
38
  "bin/",
39
39
  "scripts/",
40
40
  "skills/",
41
+ "commands/",
41
42
  ".claude-plugin/",
42
43
  "README.md",
43
44
  "LICENSE"
@@ -1,20 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from "node:os";
3
- import { cp, mkdir, rm } from "node:fs/promises";
3
+ import { cp, mkdir, readdir, rm, unlink } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
- import { SKILLS_DIR, pathExists } from "./lib/skills.mjs";
5
+ import { REPO_ROOT, SKILLS_DIR, pathExists } from "./lib/skills.mjs";
6
6
 
7
- const target = join(homedir(), ".claude", "skills", "storywright");
7
+ const skillsTarget = join(homedir(), ".claude", "skills", "storywright");
8
+ const commandsDir = join(homedir(), ".claude", "commands");
9
+ const commandsSource = join(REPO_ROOT, "commands");
10
+ const COMMAND_PREFIX = "storywright-";
8
11
 
9
- async function main() {
10
- if (await pathExists(target)) {
11
- await rm(target, { recursive: true, force: true });
12
- console.log(`• Removed existing ${target}`);
12
+ async function installSkills() {
13
+ if (await pathExists(skillsTarget)) {
14
+ await rm(skillsTarget, { recursive: true, force: true });
15
+ console.log(`• Removed existing ${skillsTarget}`);
16
+ }
17
+ await mkdir(skillsTarget, { recursive: true });
18
+ await cp(SKILLS_DIR, skillsTarget, { recursive: true });
19
+ console.log(`✓ Installed skills to ${skillsTarget}`);
20
+ }
21
+
22
+ async function installCommands() {
23
+ if (!(await pathExists(commandsSource))) {
24
+ console.log("• No commands/ folder in source; skipping slash command install.");
25
+ return;
26
+ }
27
+ await mkdir(commandsDir, { recursive: true });
28
+
29
+ // Remove old storywright-* commands so renames don't leave orphans.
30
+ const existing = await readdir(commandsDir).catch(() => []);
31
+ for (const f of existing) {
32
+ if (f.startsWith(COMMAND_PREFIX)) await unlink(join(commandsDir, f));
13
33
  }
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.`);
34
+
35
+ const sources = await readdir(commandsSource);
36
+ for (const f of sources) {
37
+ if (!f.endsWith(".md")) continue;
38
+ await cp(join(commandsSource, f), join(commandsDir, `${COMMAND_PREFIX}${f}`));
39
+ }
40
+ console.log(`✓ Installed slash commands to ${commandsDir} (prefix: ${COMMAND_PREFIX})`);
41
+ }
42
+
43
+ async function main() {
44
+ await installSkills();
45
+ await installCommands();
46
+ console.log(` Restart Claude Code for changes to be picked up.`);
47
+ console.log(` After restart, try: /storywright-story-generate <your prompt>`);
18
48
  }
19
49
 
20
50
  main().catch((err) => {
@@ -1,18 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from "node:os";
3
- import { rm } from "node:fs/promises";
3
+ import { readdir, rm, unlink } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { pathExists } from "./lib/skills.mjs";
6
6
 
7
- const target = join(homedir(), ".claude", "skills", "storywright");
7
+ const skillsTarget = join(homedir(), ".claude", "skills", "storywright");
8
+ const commandsDir = join(homedir(), ".claude", "commands");
9
+ const COMMAND_PREFIX = "storywright-";
8
10
 
9
11
  async function main() {
10
- if (!(await pathExists(target))) {
11
- console.log(`• Nothing to uninstall (${target} does not exist)`);
12
- return;
12
+ let removed = 0;
13
+
14
+ if (await pathExists(skillsTarget)) {
15
+ await rm(skillsTarget, { recursive: true, force: true });
16
+ console.log(`✓ Removed ${skillsTarget}`);
17
+ removed++;
18
+ }
19
+
20
+ if (await pathExists(commandsDir)) {
21
+ const files = await readdir(commandsDir);
22
+ let cmdsRemoved = 0;
23
+ for (const f of files) {
24
+ if (f.startsWith(COMMAND_PREFIX)) {
25
+ await unlink(join(commandsDir, f));
26
+ cmdsRemoved++;
27
+ }
28
+ }
29
+ if (cmdsRemoved > 0) {
30
+ console.log(`✓ Removed ${cmdsRemoved} slash commands from ${commandsDir}`);
31
+ removed++;
32
+ }
13
33
  }
14
- await rm(target, { recursive: true, force: true });
15
- console.log(`✓ Removed ${target}`);
34
+
35
+ if (removed === 0) console.log("• Nothing to uninstall.");
16
36
  }
17
37
 
18
38
  main().catch((err) => {
@@ -37,6 +37,7 @@ Invoked by `story-generate`, `story-refine`, `story-split`, and `story-from-figm
37
37
  - **Data inputs** (auth scope, identifiers, format)
38
38
  - **Constraints** (platform, accessibility, locale, SLA)
39
39
  - **Out-of-scope assumptions**
40
+ - **Multi-source conflicts** (when text + image + Figma are all provided): explicit disagreement between sources MUST be surfaced. Examples: text says "Google only" but Figma shows multiple providers; text mentions 1 flow but Figma shows 3; image shows error state not mentioned in text. Never silently pick a winner.
40
41
  2. For each axis, mark one of: ANSWERED · INFERRABLE · BLOCKING.
41
42
  3. Drop `ANSWERED`. For `INFERRABLE`, do NOT ask — mark assumption in the story output with `> ⚠️ Assumed:`.
42
43
  4. For `BLOCKING`, draft questions. Limit to **3 questions max per round**. Prefer multiple-choice or yes/no.
@@ -47,6 +47,14 @@ A Figma file usually represents N screens / N flows. This skill maps that visual
47
47
  - Ask the user to either (a) install an MCP Figma server, (b) export the relevant frames as PNGs and drop them in chat, or (c) paste a textual description of the flows.
48
48
  - Continue under the chosen fallback. The rest of the skill works with screenshots via Claude vision.
49
49
 
50
+ ### Phase 0.5 — Detect companion inputs
51
+
52
+ Before extracting from Figma, check whether the user attached:
53
+ - **Accompanying text** (goal, story draft, constraints) — treat as canonical for `User Story / Scope / Business Goal`. Figma is canonical for `Components / States / Flows`.
54
+ - **Reference screenshots** — usually redundant when Figma is available; use only if Figma frames are missing states the screenshots show.
55
+
56
+ If text + Figma both describe the same flow but disagree (e.g., text says "single page form", Figma shows multi-step wizard), **surface the conflict** in clarifications and ask before drafting. Do NOT silently pick a winner. See `[[story-generate]]` "Mixed inputs" section for source priority.
57
+
50
58
  ### Phase 1 — Inventory
51
59
 
52
60
  1. List pages in the file (if MCP allows).
@@ -49,9 +49,37 @@ Use vision. Extract:
49
49
  ### Figma links
50
50
  If MCP Figma is available (see `[[story-from-figma]]`), use it to enumerate frames, components, navigation. If not, fall back to asking the user to drop screenshots.
51
51
 
52
+ ### Mixed inputs (text + image + Figma)
53
+
54
+ The skill is designed to **fuse multiple sources** in a single invocation. Common pairings:
55
+
56
+ - **Text + screenshot** — text states the goal, image shows the proposed UI. Use text for `User Story / Goal / Scope`, image for `Components / States / Edge cases / UX flow`.
57
+ - **Text + Figma link** — text gives intent, Figma gives implementation surface. Use text for `User Story / Business goal`, Figma for `Technical considerations / Edge cases / Components / Multi-screen flows`.
58
+ - **Text + image + Figma** — full triangulation. Highest fidelity; also highest chance of conflict.
59
+
60
+ **Source priority (when sources disagree):**
61
+
62
+ | Section | Primary | Secondary | Tertiary |
63
+ |---|---|---|---|
64
+ | User Story / Goal | Text | Figma (frame titles, callouts) | Image |
65
+ | Business Rules / Scope | Text | Figma | Image |
66
+ | UI Components / States | Figma | Image | Text |
67
+ | Edge Cases | Figma + Image (states shown) | Text | — |
68
+ | Technical Considerations | Figma (component naming, design system refs) | Text | Image |
69
+ | Acceptance Criteria | Triangulate all three | — | — |
70
+
71
+ **Conflict handling:**
72
+
73
+ 1. **Detect the conflict explicitly.** Example: text says "Google only" but Figma shows Google + Facebook buttons.
74
+ 2. **Do NOT silently pick a winner.** Surface the conflict in `clarifications.md` as a BLOCKING question: *"Text says X but design shows Y — which is canonical?"*
75
+ 3. **If the user is in-session, ask immediately** before drafting. If running batch, mark the story `DRAFT` and write both options in scope/out-of-scope with `> ⚠️ Conflict:` annotation.
76
+ 4. **Scope coverage check:** if Figma shows N flows but text describes 1, ask whether to (a) generate 1 story bounded to text, (b) generate N stories from Figma, or (c) generate 1 story + flag remaining flows as roadmap.
77
+
52
78
  ## Application (step-by-step)
53
79
 
54
- 1. **Detect input type** (text | image | figma-link | mixed). Branch accordingly.
80
+ 1. **Detect input types present** text, image, figma-link, or any combination. Branch accordingly:
81
+ - **Single source** → process as before.
82
+ - **Mixed sources** → run the "Mixed inputs" protocol above, including source-priority lookup and explicit conflict detection BEFORE drafting.
55
83
  2. **Intake gap check** — invoke `[[clarification-questions]]`. If it returns BLOCKING questions, **ask first** before drafting.
56
84
  3. **Detect language** of input (es | en | other). Output in the input language.
57
85
  4. **Draft skeleton** of the structured story (all 15 sections from the template).