This demo shows GraphStack handling a real feature request end-to-end —
no manual role switching, no copy-pasting prompts.
The project is a Node.js auth service (demo/). The user wants to add rate limiting.
# 1. Install GraphStack into the demo project
bash install.sh demo/
# 2. Graphify has already run (see demo/graphify-out/GRAPH_REPORT.md)
# In real usage: open Cursor in demo/ and run /graphify .
# 3. In Cursor chat, paste ONE prompt to start everything:The only prompt the user ever types:
Read orchestrator/ORCHESTRATOR.md.
I want to add rate limiting to the login endpoint — max 5 attempts per email per 15 minutes.
GraphStack ready.
Graph loaded: 6 nodes, 3 clusters, last updated 2026-05-04.
Knowledge gaps noted: no rate limiting, in-memory sessions, logout TODO.
Switching to Architect for scoping.
[ARCHITECT MODE]
Reading graph... login.ts is entry point. utils/crypto.ts is god node (blast radius: full auth).
Objective: Rate limit login attempts — 5 per email per 15 min.
Blast radius: login.ts (direct), api/types.ts (new error code needed)
Risk nodes: utils/crypto.ts — NOT in scope, do not touch.
Brief: [written to handoff/BRIEF.md]
Board: [creating board/todo/rate-limit-login.json]
→ Confirm to build, or adjust scope.
User says: "looks good"
$ python -m graphstack board new rate-limit-login "Rate limit login — 5 attempts / 15 min"
✅ Task 'rate-limit-login' created in todo/
[ARCHITECT → BUILDER]
Brief locked. Board task created. Switching to Builder.
[BUILDER MODE]
$ python -m graphstack board claim rate-limit-login builder
✅ Task 'rate-limit-login' claimed by builder
Graph check: rateLimit.ts doesn't exist yet — no dependencies to verify.
Graph check: login.ts imports → [crypto, session, users, types] — rateLimit will be a new import.
Graph check: types.ts consumers → [login.ts, users.ts] — adding enum value is safe.
Implementing criterion 1 & 2: new src/auth/rateLimit.ts
// src/auth/rateLimit.ts
interface RateLimitEntry {
attempts: number;
resetAt: Date;
}
const store = new Map<string, RateLimitEntry>();
const MAX_ATTEMPTS = 5;
const WINDOW_MS = 15 * 60 * 1000;
export function checkRateLimit(email: string): boolean {
const entry = store.get(email);
if (!entry || entry.resetAt < new Date()) {
return true; // allowed
}
return entry.attempts < MAX_ATTEMPTS;
}
export function recordFailedAttempt(email: string): void {
const entry = store.get(email);
const now = new Date();
if (!entry || entry.resetAt < now) {
store.set(email, { attempts: 1, resetAt: new Date(now.getTime() + WINDOW_MS) });
} else {
entry.attempts += 1;
}
}
export function resetRateLimit(email: string): void {
store.delete(email);
}✓ Criterion 1 & 2 complete.
Implementing criterion 3 & 4: update src/auth/login.ts
// src/auth/login.ts (updated)
import { hashPassword, comparePassword } from "../utils/crypto";
import { createSession } from "./session";
import { findUserByEmail } from "../api/users";
import { checkRateLimit, recordFailedAttempt } from "./rateLimit"; // new
import type { LoginResult, Credentials } from "../api/types";
export async function login(credentials: Credentials): Promise<LoginResult> {
// Rate limit check — before any expensive operations
if (!checkRateLimit(credentials.email)) {
return { success: false, error: "RATE_LIMITED" };
}
const user = await findUserByEmail(credentials.email);
if (!user) {
recordFailedAttempt(credentials.email); // count failed attempts
return { success: false, error: "USER_NOT_FOUND" };
}
const valid = await comparePassword(credentials.password, user.passwordHash);
if (!valid) {
recordFailedAttempt(credentials.email); // count failed attempts
return { success: false, error: "INVALID_PASSWORD" };
}
// Success — do NOT record as failed attempt (criterion 3)
const session = await createSession(user.id);
return { success: true, token: session.token, userId: user.id };
}✓ Criterion 3 & 4 complete.
Updating src/api/types.ts — adding RATE_LIMITED error code.
error?: "USER_NOT_FOUND" | "INVALID_PASSWORD" | "SESSION_ERROR" | "RATE_LIMITED";✓ All 4 criteria implemented.
[BUILDER → REVIEWER]
Implementation complete. Switching to Reviewer.
[REVIEWER MODE]
Checking 4 criteria + graph neighbors...
Graph: rateLimit.ts neighbors → login.ts only (correct, expected)
Graph: login.ts neighbors → crypto, session, users, types (unchanged)
God node utils/crypto.ts → NOT touched (brief compliance ✓)
✓ Criterion 1: RATE_LIMITED returned after 5 failures — logic correct
✓ Criterion 2: TTL resets at resetAt — 15min window correct
✓ Criterion 3: recordFailedAttempt not called on success — correct
✓ Criterion 4: keyed by email — correct
Side effects:
✓ types.ts change: additive only, existing consumers unaffected
✓ login.ts: rate check is first operation (correct — no unnecessary DB calls)
Tests: missing for rateLimit.ts — flagging for QA.
Verdict: APPROVED (with test gap noted)
[REVIEWER → QA]
[QA MODE]
Path: login(credentials) → checkRateLimit → [recordFailedAttempt | createSession]
✓ Happy path (valid credentials, <5 attempts): returns token
✓ Wrong password ×5: 6th attempt returns RATE_LIMITED
✓ Wrong password ×5, wait 15min: counter resets, login allowed
✓ Wrong password ×4, correct password: success (criterion 3)
✗ Edge case: email not found — recordFailedAttempt called correctly?
→ Verified: yes, USER_NOT_FOUND path calls recordFailedAttempt ✓
Integration: rateLimit.ts ↔ login.ts edge — clean, no circular deps
Flaky: in-memory Map — resets on server restart (known gap, noted in GRAPH_REPORT)
Not a test failure — known limitation of this cycle.
Verdict: PASS
[QA → SHIP]
[SHIP MODE]
Checklist: 5/5 passed
$ python -m graphstack board complete rate-limit-login
✅ Task 'rate-limit-login' marked complete
$ python -m graphstack board status
📋 GraphStack GNAP Board
════════════════════════════════════════
TASK ID STATUS ASSIGNED TITLE
──────────────────────────────────────────────────────────────
rate-limit-login done builder Rate limit login...
Todo: 0 | In Progress: 0 | Done: 1
⚠ Recommend: /graphify --update (new module added)
Commit message:
────────────────────────────────────────
feat: add email-based login rate limiting (#1)
What:
- src/auth/rateLimit.ts — new module, 5 attempts / 15min window
- src/auth/login.ts — rate limit check before auth logic
- src/api/types.ts — RATE_LIMITED error code
Why:
Prevent brute-force attacks on login endpoint.
Tested:
- Happy path ✅
- 5 failed attempts → blocked ✅
- TTL reset after 15min ✅
- Success does not count as failure ✅
Graph updated: recommended (run /graphify --update)
────────────────────────────────────────
[SHIP → IDLE]
Done. Run /graphify --update to keep graph current. What's next?
| Metric | Without GraphStack | With GraphStack |
|---|---|---|
| Manual role switches | 6 | 0 |
| Copy-pasted prompts | 6 | 1 |
| Files read unnecessarily | ~8 | 3 (only in-scope) |
| God node accidentally touched | Possible | Prevented by graph |
| Out-of-scope changes | Likely | 0 |
| Session resumable? | No | Yes (STATE.md) |
# Clone and install
git clone https://github.com/MertCapkin/graphstack
cd graphstack
bash install.sh demo/
# Open demo/ in Cursor
# In Cursor chat, type:
# "Read orchestrator/ORCHESTRATOR.md. I want to add [your feature]."