Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"skills": [
"./skills/spec-driven/writing-specs",
"./skills/spec-driven/writing-tasks",
"./skills/spec-driven/writing-flows",
"./skills/spec-driven/implement-with-test",
"./skills/spec-driven/test-commit-push-pr-clean",
"./skills/agents/create-team",
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ archived/ # retired skills/plugins (not shipped)
```

### Categories
- **spec-driven** — writing-specs, writing-tasks, implement-with-test, test-commit-push-pr-clean
- **spec-driven** — writing-specs, writing-tasks, writing-flows, implement-with-test, test-commit-push-pr-clean
- **agents** — create-team, split-work
- **browser** — browser-walkthrough, computer-use-test, ui-prototype-preview
- **productivity** — brain-storm, session-resume, llm-wiki
Expand Down
9 changes: 7 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# claude-plugins — Spec/Task Authoring Language

The vocabulary used by the authoring plugins (`writing-specs`, `writing-tasks`) for how generated documents are organized inside a target project. This is a glossary, not a spec.
The vocabulary used by the authoring plugins (`writing-specs`, `writing-tasks`, `writing-flows`) for how generated documents are organized inside a target project. This is a glossary, not a spec.

## Language

Expand All @@ -12,6 +12,10 @@ _Avoid_: requirement doc, ticket
An executable, dependency-aware decomposition of a Spec, tracked as a file with frontmatter. One Task references exactly one Spec.
_Avoid_: issue, todo

**Flow**:
A single document capturing how one scenario behaves at runtime — actors/trigger (use case), a sequence diagram, and the step-by-step user flow with branches — in one fixed template. One Flow = one scenario.
_Avoid_: use case doc, diagram file, user journey

**Topic**:
The coherent feature/domain a Spec or Task belongs to (e.g. `auth`, `news`, `race`). The top-level organizing axis under `docs/specs/` and `docs/tasks/`, inferred from the conversation that produced the document.
_Avoid_: phase, feature-folder, module
Expand All @@ -25,7 +29,8 @@ _Avoid_: dependency (that is the frontmatter graph; this is the WHY it can't hol

## Relationships

- A **Topic** groups one or more **Specs**
- A **Topic** groups one or more **Specs**, and zero or more **Flows**
- A **Flow** documents exactly one scenario; it may mention Specs in prose but carries no structural reference (no frontmatter link)
- A **Spec** is decomposed into one or more **Tasks**
- A **Task** references exactly one **Spec**, and zero or more other **Tasks** via `depends_on`

Expand Down
53 changes: 53 additions & 0 deletions evals/writing-flows/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"skill_name": "writing-flows",
"evals": [
{
"id": 0,
"eval_name": "checkout-sequence-from-code",
"prompt": "결제(체크아웃)가 어떤 순서로 동작하는지 한눈에 보고 싶어. 플로우 문서로 정리해줘.",
"expected_output": "flows/<topic>/01-*.md 가 생성되고, 4섹션(Context/Diagram/Steps & Branches/Source)을 갖추며, Diagram은 mermaid sequenceDiagram 하나로 Buyer→checkout→inventory→PG→order→notification 체인을 그린다. 재고 부족(409), 결제 거절(402, 예약 해제) 분기가 Steps에 나타나고 Source에 실제 탐색한 파일 경로가 나열된다.",
"files": ["shop-app"],
"expectations": [
"A flow file is created under flows/<topic>/ with a 2-digit NN prefix and lowercase-hyphen name (not flows/phase-N/)",
"The file contains exactly the four template sections: Context, Diagram, Steps & Branches, Source",
"The Diagram section contains exactly one mermaid block of type sequenceDiagram or flowchart (not both, not other types)",
"The diagram reflects the real code chain including the payment gateway (PG) call",
"Steps & Branches includes the out-of-stock branch and the payment-declined branch (with stock reservation release)",
"The Source section lists real file paths from the project (e.g. src/routes/checkout.ts) without line numbers",
"A final report is printed with Action, File, and the basis (code files explored)",
"No flows/README.md or flows/INDEX.md is created"
]
},
{
"id": 1,
"eval_name": "signup-userflow-branching",
"prompt": "회원가입 유저 플로우 그려줘. 이메일 인증까지 포함해서 어떤 케이스에서 어떻게 분기되는지 보고 싶어.",
"expected_output": "flows/<topic>/ 아래 회원가입 Flow가 생성된다. 분기(잘못된 입력 400, 기존 미인증 유저 재발송, 이메일 중복 409, 토큰 만료 → /signup/expired)가 Diagram 또는 Steps에 반영된다. 사용자 분기 중심이므로 flowchart 선택이 자연스럽지만 sequenceDiagram도 허용.",
"files": ["shop-app"],
"expectations": [
"A flow file is created under flows/<topic>/ following the NN-name convention",
"The file contains exactly the four template sections: Context, Diagram, Steps & Branches, Source",
"The Diagram section contains exactly one mermaid block (sequenceDiagram or flowchart)",
"The verification-token-expired branch (redirect to re-request) appears in the document",
"The duplicate-email and unverified-resend branches both appear in the document",
"The Source section lists src/routes/signup.ts (file path only, no line numbers)",
"The signup scenario is one file — it is not merged with checkout or other scenarios"
]
},
{
"id": 2,
"eval_name": "design-stage-flow-from-spec",
"prompt": "추천인 리워드 기능 플로우 작성해줘. 아직 구현 전이고 specs/referral/01-referral-reward.md 스펙 기반으로.",
"expected_output": "코드가 없으므로 스펙을 입력으로 설계 Flow를 생성한다. Source 섹션에 스펙 경로 또는 design 표기가 들어가고, 7일 지연 지급/환불 취소/셀프 추천 거절 분기가 Steps에 반영된다. 리포트에 코드가 아닌 스펙 기반임이 명시된다.",
"files": ["design-stage"],
"expectations": [
"A flow file is created under flows/<topic>/ following the NN-name convention",
"The file contains exactly the four template sections: Context, Diagram, Steps & Branches, Source",
"The Source section references the spec path or marks the flow as design-stage (no fabricated code paths)",
"The 7-day delayed reward and refund-cancellation branch appear in the document",
"The self-referral rejection branch appears in the document",
"The final report states the flow was derived from the spec, not code"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Referral Reward

## Purpose

Reward existing users with credit when a friend they invited completes a first purchase, to drive organic growth.

## Requirements

- Each user has a unique referral code shareable as a link
- When an invited friend signs up via the link and completes their first purchase, both sides receive 5,000 KRW credit
- Credit is granted at most once per invited friend; self-referral is rejected
- Reward grant is delayed 7 days after the purchase to absorb refunds — a refund within the window cancels the reward

## Approach

Referral attribution is stored at signup time by resolving the code in the invite link. Reward granting runs as a scheduled job that scans purchases older than 7 days with pending referral rewards, skipping refunded orders. Credits are issued through the existing wallet service.

## Verification

- Invited signup followed by first purchase creates a pending reward for both users
- Refund within 7 days cancels the pending reward
- Self-referral and second purchases grant nothing
30 changes: 30 additions & 0 deletions evals/writing-flows/files/shop-app/src/routes/checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Router } from "express";
import { reserveStock } from "../services/inventory";
import { requestPayment } from "../services/payment";
import { confirmOrder } from "../services/order";
import { sendOrderNotification } from "../services/notification";

const router = Router();

// POST /checkout — buyer clicks "결제하기" in the cart screen
router.post("/checkout", async (req, res) => {
const { cartId, paymentMethod } = req.body;

const reservation = await reserveStock(cartId);
if (!reservation.ok) {
return res.status(409).json({ error: "OUT_OF_STOCK", items: reservation.missing });
}

const payment = await requestPayment(cartId, paymentMethod);
if (payment.status === "DECLINED") {
await reservation.release();
return res.status(402).json({ error: "PAYMENT_DECLINED", retryUrl: "/checkout/retry" });
}

const order = await confirmOrder(cartId, payment.transactionId);
await sendOrderNotification(order.id);

return res.status(201).json({ orderId: order.id });
});

export default router;
44 changes: 44 additions & 0 deletions evals/writing-flows/files/shop-app/src/routes/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Router } from "express";
import { db } from "../lib/db";
import { sendVerificationEmail } from "../services/email";

const router = Router();

// POST /signup — from the signup screen
router.post("/signup", async (req, res) => {
const { email, password } = req.body;

if (!isValidEmail(email) || password.length < 8) {
return res.status(400).json({ error: "INVALID_INPUT" });
}

const existing = await db.findUserByEmail(email);
if (existing) {
// Existing but unverified → resend verification instead of erroring
if (!existing.verified) {
await sendVerificationEmail(existing.id, email);
return res.status(200).json({ status: "VERIFICATION_RESENT" });
}
return res.status(409).json({ error: "EMAIL_TAKEN", loginUrl: "/login" });
}

const user = await db.createUser(email, password, { verified: false });
await sendVerificationEmail(user.id, email);
return res.status(201).json({ status: "VERIFICATION_SENT" });
});

// GET /signup/verify?token= — link in the verification email
router.get("/signup/verify", async (req, res) => {
const claim = await db.consumeVerificationToken(String(req.query.token));
if (!claim) {
return res.redirect("/signup/expired"); // expired or already used → re-request screen
}
await db.markVerified(claim.userId);
return res.redirect("/welcome");
});

function isValidEmail(email: string) {
return /^[^@]+@[^@]+\.[^@]+$/.test(email);
}

export default router;
14 changes: 14 additions & 0 deletions evals/writing-flows/files/shop-app/src/services/inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { db } from "../lib/db";

export async function reserveStock(cartId: string) {
const items = await db.cartItems(cartId);
const missing = items.filter((i) => i.stock < i.quantity);
if (missing.length > 0) {
return { ok: false as const, missing: missing.map((i) => i.sku) };
}
const reservationId = await db.reserve(items);
return {
ok: true as const,
release: () => db.releaseReservation(reservationId),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { pushQueue } from "../lib/queue";

export async function sendOrderNotification(orderId: string) {
// Fire-and-forget: enqueue only, the worker delivers the push/email.
await pushQueue.enqueue({ type: "ORDER_CONFIRMED", orderId });
}
7 changes: 7 additions & 0 deletions evals/writing-flows/files/shop-app/src/services/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { db } from "../lib/db";

export async function confirmOrder(cartId: string, transactionId: string) {
const order = await db.createOrder(cartId, transactionId);
await db.clearCart(cartId);
return order;
}
11 changes: 11 additions & 0 deletions evals/writing-flows/files/shop-app/src/services/payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { pgClient } from "../lib/pg-client";

export async function requestPayment(cartId: string, method: string) {
// Calls the external PG (payment gateway). Times out after 10s → treated as DECLINED.
try {
const result = await pgClient.charge({ cartId, method, timeoutMs: 10_000 });
return { status: result.approved ? "APPROVED" : "DECLINED", transactionId: result.txId };
} catch (e) {
return { status: "DECLINED", transactionId: null };
}
}
60 changes: 60 additions & 0 deletions skills/spec-driven/writing-flows/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: writing-flows
description: "Writes single-scenario Flow documents under flows/<topic>/ — use case context, one Mermaid diagram (sequence or flowchart), step-by-step branches, and source file references — so how a service behaves end-to-end can be grasped at a glance. Use when user asks for a sequence diagram, user flow, use case doc, or mentions 시퀀스 다이어그램, 유저 플로우, 플로우 그려줘, 동작 흐름, 플로우 작성. Also trigger on intent like 어떻게 동작하는지 한눈에 보고 싶다, 동작 순서 정리해줘 — but not on simple code questions about a single function."
allowed-tools: Read, Write, Glob, Grep, Bash, AskUserQuestion
context: fork
agent: general-purpose
---

# Writing Flows

Document how one scenario behaves at runtime as a **Flow** file under `flows/`. A Flow makes the order of operations and the branching visible at a glance — the thing specs, tasks, and ADRs can't show.

## Hard Rules

- **Template-driven.** Every Flow follows [templates/flow-template.md](templates/flow-template.md) exactly — four sections, no freestyle.
- **One Flow = one scenario.** A request spanning multiple scenarios becomes multiple Flow files.
- **Code is the source of truth.** When code exists, read it before drawing. Never draw from assumption what can be verified by exploration.
- **Never auto-modify existing Flows.** Propose the diff, wait for confirmation.
- **No derived index files.** Never create `flows/README.md` or `flows/INDEX.md` — the directory listing is the index.

## Directory Rules

- Flows live in `flows/<topic>/NN-name.md` at the project root. Topic = coherent feature/domain (`auth`, `checkout`), the same axis as ADR-0001 — never phase numbers.
- Match the topic against existing `flows/*/` directories before creating a new one.
- `NN` is a 2-digit index within the topic, starting `01`. Filenames are lowercase-with-hyphens.

## Phase 1 — Search

1. `Glob flows/**/*.md` and check whether a Flow for this scenario already exists.
- Exact scenario match → ask via `AskUserQuestion`: update, create new, or cancel.
- Existing Flow whose `## Source` files no longer exist (verify with `Glob`) → report as outdated; propose an update, never apply silently.
2. Locate the scenario in code: entry point (route/handler/command), the calls it makes, and where behavior branches. Follow the chain far enough to know every actor and every branch — guessing here produces a wrong diagram.
3. No relevant code (design-stage scenario)? Fall back to specs (`Glob specs/**/*.md`) or conversation context, and say so in the report.

## Phase 2 — Write

Read [templates/flow-template.md](templates/flow-template.md) and fill it strictly:

- **Context** — 2-3 sentences: the scenario, its trigger, the actors involved.
- **Diagram** — exactly one Mermaid diagram. `sequenceDiagram` when the story is interaction between components/services; `flowchart TD` when it is user-facing branching between screens/states. Never both, never other types.
- **Steps & Branches** — numbered happy path; attach branches and edge cases to the step where they diverge (`— 실패 시 → ...`).
- **Source** — the files the Flow was derived from, file paths only (no line numbers — they go stale). For design-stage Flows, list the spec or write `(design — no code yet)`.

Diagram and Steps must agree with each other and with the code. On update, preserve sections the user didn't ask to change.

## Phase 3 — Report

End with a short report: Action (Created/Updated), File (relative path), Scenario (H1), Basis (code files explored, or spec/conversation), and any outdated Flows found in Phase 1. One actionable next step (e.g. a neighboring scenario worth documenting).

## Anti-patterns

**WRONG:** drawing the diagram from the user's description while the code says otherwise.
**RIGHT:** explore the code first; if it contradicts the user's description, surface the difference and ask which one the Flow should capture.

**WRONG:** one giant Flow covering signup, login, and password reset.
**RIGHT:** three Flow files in `flows/auth/`, each one scenario.

## Boundaries

This skill only writes Flow files. Requirements belong to `writing-specs`, work breakdown to `writing-tasks`. It never modifies source code.
22 changes: 22 additions & 0 deletions skills/spec-driven/writing-flows/templates/flow-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# {Scenario Title} Flow

## Context

{2-3 sentences: what scenario this is, what triggers it, which actors are involved.}

## Diagram

```mermaid
{exactly one diagram: sequenceDiagram (component interaction) or flowchart TD (user-facing branching)}
```

## Steps & Branches

1. {step} — {branch/edge case if it diverges here → outcome}
2. {step}
3. {step} — {branch → outcome}

## Source

- {path/to/file.ts}
- {path/to/other-file.ts}
Loading