
Jira Safe
- 110 installs
- 10 repo stars
- Updated December 11, 2025
- 01000001-01001110/agent-jira-skills
Orchestrate Next-Gen Jira stories, approvals, and custom workflow transitions while keeping agent task completion in sync with board status.
About
Jira Workflow Orchestration is an agent skill for solo builders and small teams who run work in Atlassian Jira alongside coding agents. It walks through creating user stories, epics, and tasks with explicit user approval first, then transitioning issues through a Next-Gen custom workflow instead of assuming default Jira columns. The skill stresses querying available transitions via GET /rest/api/3/issue/{key}/transitions before every move, which prevents failed API calls when board workflows differ per project. It covers sprint planning, backlog refinement, and linking completed agent tasks to Done states so the board reflects reality. Configuration distinguishes Next-Gen epic linking via parent from Classic customfield_10014. For indie shipping velocity, this keeps PM hygiene inside the same session as implementation rather than as a separate admin chore.
- End-to-end Jira REST workflow for user stories, epics, and tasks with approval gates before create
- Next-Gen (team-managed) aware: parent field for epics, query transitions before every status change
- Documented custom states: To Do, In Review, Progressing, Out Review, Done
- Syncs Claude Code task completion with Jira status in real time
- Requires JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL, JIRA_PROJECT_KEY, JIRA_BOARD_ID
Jira Safe by the numbers
- 110 all-time installs (skills.sh)
- Ranked #1,327 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/01000001-01001110/agent-jira-skills --skill jira-safeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 110 |
|---|---|
| repo stars | ★ 10 |
| Security audit | 3 / 3 scanners passed |
| Last updated | December 11, 2025 |
| Repository | 01000001-01001110/agent-jira-skills ↗ |
What it does
Orchestrate Next-Gen Jira stories, approvals, and custom workflow transitions while keeping agent task completion in sync with board status.
Files
Jira Workflow Orchestration Skill
Complete workflow management for Jira: building stories (SAFe), getting approvals, and transitioning items through the development lifecycle (To Do → Progressing → Done).
IMPORTANT: This project uses Next-Gen (Team-managed) Jira with custom workflow states. The actual states are:
To Do(backlog)In ReviewProgressing(active work)Out ReviewDone
Always query available transitions first: GET /rest/api/3/issue/{key}/transitions
When to Use
- Creating new user stories, epics, or tasks for the project
- Getting user approval before creating Jira items
- Moving stories through workflow states as work progresses
- Syncing Claude Code task completion with Jira status
- Managing sprint planning and backlog refinement
- Tracking development progress in real-time
Prerequisites
Environment Variables:
JIRA_EMAIL=your.email@domain.com
JIRA_API_TOKEN=your_api_token
JIRA_BASE_URL=https://your-org.atlassian.net
JIRA_PROJECT_KEY=SCRUM
JIRA_BOARD_ID=1Project Configuration:
- Must know if project is Next-Gen (Team-managed) or Classic (Company-managed)
- Next-Gen: Use
parentfield for Epic links - Classic: Use
customfield_10014for Epic links
---
Core Workflow Pattern
The Approval-Create-Track Loop
1. PLAN: Analyze task requirements
↓
2. PROPOSE: Present story to user for approval
↓
3. APPROVE: User confirms or modifies
↓
4. CREATE: Issue created in Jira backlog
↓
5. START: Transition to "Progressing" when work begins
↓
6. COMPLETE: Transition to "Done" when work verified
↓
7. SYNC: Update Jira with implementation details---
Phase 1: Story Building (SAFe Format)
Building a Story Proposal
When user requests work, build a SAFe-compliant story proposal:
function buildStoryProposal(task) {
return {
summary: `As a ${task.persona}, I want ${task.goal}, so that ${task.benefit}`,
description: {
userStory: `As a **${task.persona}**, I want **${task.goal}**, so that **${task.benefit}**.`,
acceptanceCriteria: task.scenarios.map(s => ({
name: s.name,
given: s.given,
when: s.when,
then: s.then
})),
definitionOfDone: [
'Code reviewed and approved',
'Unit tests written and passing',
'Integration tests passing',
'Documentation updated',
'Deployed to staging',
'Validated in production'
],
technicalNotes: task.technicalNotes || []
},
category: task.category, // authentication, ui, api, database, etc.
estimatedComplexity: task.complexity || 'medium', // small, medium, large
subtasks: task.subtasks || []
};
}Presenting for Approval
CRITICAL: Always get user approval before creating Jira items.
Use this prompt pattern:
## Proposed Jira Story
**Summary:** As a [persona], I want [goal], so that [benefit]
**Category:** [category]
**Complexity:** [small/medium/large]
### Acceptance Criteria
**Scenario 1: [Name]**
- **GIVEN** [precondition]
- **WHEN** [action]
- **THEN** [expected result]
### Subtasks (if any)
1. [Subtask 1]
2. [Subtask 2]
3. [Subtask 3]
---
**Do you want me to create this in Jira?**
Options:
1. **Yes, create as-is** - I'll create the story now
2. **Modify** - Tell me what to change
3. **Skip** - Don't create in Jira, just do the work---
Phase 2: Issue Creation
Create Story in Jira
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const PROJECT_KEY = process.env.JIRA_PROJECT_KEY;
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
const headers = {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
async function createStory(proposal, epicKey = null) {
const body = {
fields: {
project: { key: PROJECT_KEY },
issuetype: { name: 'Story' },
summary: proposal.summary,
description: buildADF(proposal.description),
labels: [proposal.category.toLowerCase().replace(/\s+/g, '-')]
}
};
// Link to Epic (Next-Gen project)
if (epicKey) {
body.fields.parent = { key: epicKey };
}
const response = await fetch(`${JIRA_BASE_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to create story: ${error}`);
}
const issue = await response.json();
console.log(`Created: ${issue.key} - ${proposal.summary}`);
// Create subtasks if any
if (proposal.subtasks?.length > 0) {
for (const subtask of proposal.subtasks) {
await createSubtask(issue.key, subtask);
await delay(100); // Rate limiting
}
}
return issue;
}
async function createSubtask(parentKey, summary) {
const body = {
fields: {
project: { key: PROJECT_KEY },
issuetype: { name: 'Subtask' }, // Note: 'Subtask' for Next-Gen
parent: { key: parentKey },
summary: summary
}
};
const response = await fetch(`${JIRA_BASE_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to create subtask: ${error}`);
}
return response.json();
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Build Atlassian Document Format (ADF)
function buildADF(content) {
const sections = [];
// User Story Section
sections.push({
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'User Story' }]
});
sections.push({
type: 'paragraph',
content: [{ type: 'text', text: content.userStory }]
});
// Acceptance Criteria Section
sections.push({
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Acceptance Criteria' }]
});
for (const scenario of content.acceptanceCriteria) {
sections.push({
type: 'heading',
attrs: { level: 3 },
content: [{ type: 'text', text: `Scenario: ${scenario.name}` }]
});
sections.push({
type: 'bulletList',
content: [
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: `GIVEN ${scenario.given}`, marks: [{ type: 'strong' }] }] }] },
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: `WHEN ${scenario.when}`, marks: [{ type: 'strong' }] }] }] },
{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: `THEN ${scenario.then}`, marks: [{ type: 'strong' }] }] }] }
]
});
}
// Definition of Done Section
sections.push({
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Definition of Done' }]
});
sections.push({
type: 'bulletList',
content: content.definitionOfDone.map(item => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: `[ ] ${item}` }] }]
}))
});
// Technical Notes (if any)
if (content.technicalNotes?.length > 0) {
sections.push({
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Technical Notes' }]
});
sections.push({
type: 'bulletList',
content: content.technicalNotes.map(note => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: note }] }]
}))
});
}
return { type: 'doc', version: 1, content: sections };
}---
Phase 3: Workflow Transitions
Get Available Transitions
async function getTransitions(issueKey) {
const response = await fetch(
`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/transitions`,
{ headers }
);
if (!response.ok) {
throw new Error(`Failed to get transitions: ${response.status}`);
}
const data = await response.json();
return data.transitions;
}Transition Issue to State
async function transitionTo(issueKey, targetState) {
// Get available transitions
const transitions = await getTransitions(issueKey);
// Find the transition to target state
const transition = transitions.find(t =>
t.to.name.toLowerCase() === targetState.toLowerCase() ||
t.name.toLowerCase() === targetState.toLowerCase()
);
if (!transition) {
console.log(`Available transitions for ${issueKey}:`);
transitions.forEach(t => console.log(` - ${t.name} → ${t.to.name}`));
throw new Error(`No transition to "${targetState}" found`);
}
// Execute the transition
const response = await fetch(
`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/transitions`,
{
method: 'POST',
headers,
body: JSON.stringify({ transition: { id: transition.id } })
}
);
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to transition: ${error}`);
}
console.log(`${issueKey} transitioned to ${targetState}`);
return true;
}Common Workflow Operations
// Start work on a story (To Do → Progressing)
async function startWork(issueKey) {
await transitionTo(issueKey, 'Progressing');
console.log(`Started: ${issueKey}`);
}
// Complete a story (Progressing → Done)
async function completeWork(issueKey) {
await transitionTo(issueKey, 'Done');
console.log(`Completed: ${issueKey}`);
}
// Move back to backlog (any state → To Do)
async function moveToBacklog(issueKey) {
await transitionTo(issueKey, 'To Do');
console.log(`Moved to backlog: ${issueKey}`);
}
// Reopen a completed issue (Done → To Do)
async function reopenWork(issueKey) {
await transitionTo(issueKey, 'To Do');
console.log(`Reopened: ${issueKey}`);
}---
Phase 4: Add Comments and Updates
Add Work Log Comment
async function addComment(issueKey, comment) {
const body = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: comment }]
}
]
}
};
const response = await fetch(
`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/comment`,
{
method: 'POST',
headers,
body: JSON.stringify(body)
}
);
if (!response.ok) {
throw new Error(`Failed to add comment: ${response.status}`);
}
console.log(`Comment added to ${issueKey}`);
return response.json();
}Add Implementation Details Comment
async function addImplementationDetails(issueKey, details) {
const content = [
{ type: 'heading', attrs: { level: 3 }, content: [{ type: 'text', text: 'Implementation Details' }] },
{ type: 'paragraph', content: [{ type: 'text', text: `Completed: ${new Date().toISOString()}` }] }
];
if (details.files?.length > 0) {
content.push(
{ type: 'heading', attrs: { level: 4 }, content: [{ type: 'text', text: 'Files Modified' }] },
{
type: 'bulletList',
content: details.files.map(f => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: f }] }]
}))
}
);
}
if (details.commits?.length > 0) {
content.push(
{ type: 'heading', attrs: { level: 4 }, content: [{ type: 'text', text: 'Commits' }] },
{
type: 'bulletList',
content: details.commits.map(c => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: c }] }]
}))
}
);
}
if (details.notes) {
content.push(
{ type: 'heading', attrs: { level: 4 }, content: [{ type: 'text', text: 'Notes' }] },
{ type: 'paragraph', content: [{ type: 'text', text: details.notes }] }
);
}
const body = { body: { type: 'doc', version: 1, content } };
const response = await fetch(
`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/comment`,
{
method: 'POST',
headers,
body: JSON.stringify(body)
}
);
return response.json();
}---
Complete Workflow Example
Full Cycle: Propose → Approve → Create → Work → Complete
async function fullWorkflowCycle(task) {
// 1. Build proposal
const proposal = buildStoryProposal(task);
// 2. Present for approval (use AskUserQuestion tool)
const approved = await presentForApproval(proposal);
if (!approved) {
console.log('Story creation skipped by user');
return null;
}
// 3. Create in Jira
const issue = await createStory(proposal, task.epicKey);
console.log(`Created: ${issue.key}`);
// 4. Start work (transition to In Progress)
await startWork(issue.key);
// 5. Do the actual work (your implementation here)
const result = await doTheWork(task);
// 6. Add implementation details
await addImplementationDetails(issue.key, {
files: result.modifiedFiles,
commits: result.commits,
notes: result.notes
});
// 7. Complete the work
await completeWork(issue.key);
return issue;
}---
Integration with Claude Code Orchestration
Sync with TodoWrite
When working on Jira stories, sync with TodoWrite:
TodoWrite todos:
[
{ "content": "SCRUM-55: Create signup API", "status": "in_progress", "activeForm": "Working on SCRUM-55" },
{ "content": "SCRUM-56: Create login API", "status": "pending", "activeForm": "Waiting for SCRUM-55" },
{ "content": "SCRUM-57: Create logout API", "status": "pending", "activeForm": "Waiting for SCRUM-56" }
]
As each task completes:
1. Mark TodoWrite item as completed
2. Transition Jira issue to Done
3. Add implementation comment to Jira
4. Move to next taskAuto-Transition Pattern
// When starting a task
async function startTask(issueKey) {
// 1. Transition Jira to Progressing
await startWork(issueKey);
// 2. Update TodoWrite (in Claude Code)
// TodoWrite: Mark as in_progress
return issueKey;
}
// When completing a task
async function completeTask(issueKey, details) {
// 1. Add implementation comment
await addImplementationDetails(issueKey, details);
// 2. Transition Jira to Done
await completeWork(issueKey);
// 3. Update TodoWrite (in Claude Code)
// TodoWrite: Mark as completed
return issueKey;
}---
Quick Reference
Status Transitions (SCRUM Project - Next-Gen)
| From | To | Transition Name | Typical Use |
|---|---|---|---|
| To Do | Progressing | "Progressing" | Starting work |
| To Do | In Review | "In Review" | Needs review first |
| Progressing | Done | "Done" | Work complete |
| Progressing | To Do | "To Do" | Blocked/deprioritized |
| Done | To Do | "To Do" | Reopening |
Available States: To Do, In Review, Progressing, Out Review, Done
Note: Always query transitions first - they vary by issue type and current state.
API Endpoints
| Action | Method | Endpoint |
|---|---|---|
| Create Issue | POST | /rest/api/3/issue |
| Get Issue | GET | /rest/api/3/issue/{key} |
| Update Issue | PUT | /rest/api/3/issue/{key} |
| Delete Issue | DELETE | /rest/api/3/issue/{key} |
| Get Transitions | GET | /rest/api/3/issue/{key}/transitions |
| Do Transition | POST | /rest/api/3/issue/{key}/transitions |
| Add Comment | POST | /rest/api/3/issue/{key}/comment |
| Search | GET | /rest/api/3/search/jql?jql=... |
Rate Limiting
- Max 10 requests/second
- Add 100ms delay between bulk operations
- Batch operations where possible
---
Error Handling
async function safeJiraOperation(operation, issueKey) {
try {
return await operation();
} catch (error) {
console.error(`Jira operation failed for ${issueKey}: ${error.message}`);
// Common error patterns
if (error.message.includes('404')) {
console.log('Issue not found - may have been deleted');
}
if (error.message.includes('401')) {
console.log('Authentication failed - check API token');
}
if (error.message.includes('403')) {
console.log('Permission denied - check project access');
}
if (error.message.includes('400')) {
console.log('Bad request - check field names and values');
}
throw error;
}
}---
Executable Scripts
Ready-to-run scripts are available in both Node.js and Python:
Using the Cross-Platform Runner
# From the .claude/skills/jira directory
node scripts/run.js workflow demo SCRUM-100 # Demo full workflow
node scripts/run.js test # Test authentication
# Force specific runtime
node scripts/run.js --python workflow demo SCRUM-100
node scripts/run.js --node workflow demo SCRUM-100Direct Script Execution
# Node.js
node scripts/jira-workflow-demo.mjs demo SCRUM-100
node scripts/jira-workflow-demo.mjs start SCRUM-100
node scripts/jira-workflow-demo.mjs complete SCRUM-100
node scripts/jira-workflow-demo.mjs reopen SCRUM-100
node scripts/jira-workflow-demo.mjs status SCRUM-100
# Python (recommended on Windows)
python scripts/jira-workflow-demo.py demo SCRUM-100
python scripts/jira-workflow-demo.py start SCRUM-100
python scripts/jira-workflow-demo.py complete SCRUM-100
python scripts/jira-workflow-demo.py reopen SCRUM-100
python scripts/jira-workflow-demo.py status SCRUM-100Available Scripts
| Script | Node.js | Python | Purpose |
|---|---|---|---|
| Workflow Demo | jira-workflow-demo.mjs | jira-workflow-demo.py | Full To Do → Progressing → Done demo |
| Add Subtasks | jira-add-subtasks.mjs | jira-add-subtasks.py | Create subtasks under a story |
| Create Story | jira-create-one.mjs | jira-create-one.py | Create single story |
| Bulk Create | jira-bulk-create.mjs | jira-bulk-create.py | Create from git commits |
---
References
// Add subtasks to a Story
// Following jira-safe skill patterns for Next-Gen project
// Usage: node jira-add-subtasks.mjs SCRUM-148 "Subtask 1" "Subtask 2" "Subtask 3"
// node jira-add-subtasks.mjs demo (creates test story with subtasks)
// Load from environment variables (set by run.js or manually)
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const PROJECT_KEY = process.env.JIRA_PROJECT_KEY || 'SCRUM';
// Validate required env vars
if (!JIRA_EMAIL || !JIRA_API_TOKEN || !JIRA_BASE_URL) {
console.error('Error: Missing required environment variables.');
console.error('Required: JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL');
console.error('Set these in .claude/skills/jira/.env or export them manually.');
process.exit(1);
}
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
const headers = {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// ==================== SKILL PATTERNS ====================
// From .claude/skills/jira/jira-safe/SKILL.md
// Create Subtask (Next-Gen pattern)
// NOTE: Next-Gen uses 'Subtask' (no hyphen), NOT 'Sub-task'
async function createSubtask(parentKey, summary) {
const url = `${JIRA_BASE_URL}/rest/api/3/issue`;
const body = {
fields: {
project: { key: PROJECT_KEY },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', Classic: 'Sub-task'
parent: { key: parentKey },
summary: summary
}
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status}: ${error.substring(0, 200)}`);
}
return response.json();
}
// Create Story (Next-Gen pattern)
async function createStory(summary, epicKey = null) {
const url = `${JIRA_BASE_URL}/rest/api/3/issue`;
const fields = {
project: { key: PROJECT_KEY },
issuetype: { name: 'Story' },
summary: summary
};
// Next-Gen: Link to parent Epic using 'parent' field (not customfield_10014)
if (epicKey) {
fields.parent = { key: epicKey };
}
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ fields })
});
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status}: ${error.substring(0, 200)}`);
}
return response.json();
}
// Verify issue exists
async function verifyIssue(issueKey) {
const url = `${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}?fields=summary,issuetype`;
const response = await fetch(url, { headers });
if (!response.ok) {
return null;
}
return response.json();
}
// Demo mode - creates a story with subtasks
async function runDemo() {
console.log('========================================');
console.log(' ADD SUBTASKS DEMO');
console.log(' (Following jira-safe skill patterns)');
console.log('========================================\n');
// Create a demo story
console.log('Creating demo Story...');
const story = await createStory('[Demo] Test story with subtasks');
console.log(`+ Story created: ${story.key}\n`);
// Define demo subtasks
const demoSubtasks = [
'Subtask 1: Research and planning',
'Subtask 2: Implementation',
'Subtask 3: Testing',
'Subtask 4: Documentation',
'Subtask 5: Review and merge'
];
console.log(`Adding ${demoSubtasks.length} subtasks to ${story.key}...`);
const results = { created: 0, failed: 0 };
for (const summary of demoSubtasks) {
try {
const subtask = await createSubtask(story.key, summary);
console.log(` + ${subtask.key}: ${summary}`);
results.created++;
} catch (error) {
console.log(` - FAILED: ${summary} (${error.message})`);
results.failed++;
}
await new Promise(r => setTimeout(r, 100)); // Rate limiting
}
console.log('\n========================================');
console.log(' SUMMARY');
console.log('========================================');
console.log(`Story: ${story.key}`);
console.log(`Subtasks created: ${results.created}`);
console.log(`Subtasks failed: ${results.failed}`);
console.log(`\nView: ${JIRA_BASE_URL}/browse/${story.key}`);
console.log('========================================');
}
// Main function - add subtasks to specified story
async function addSubtasksToStory(storyKey, subtaskSummaries) {
console.log('========================================');
console.log(' ADD SUBTASKS TO STORY');
console.log(' (Following jira-safe skill patterns)');
console.log('========================================\n');
// Verify story exists
console.log(`Verifying ${storyKey}...`);
const story = await verifyIssue(storyKey);
if (!story) {
console.error(`ERROR: Issue ${storyKey} not found or not accessible.`);
process.exit(1);
}
console.log(`Found: ${story.key} [${story.fields.issuetype.name}]`);
console.log(`Summary: ${story.fields.summary}\n`);
const results = { created: 0, failed: 0 };
console.log(`Adding ${subtaskSummaries.length} subtasks...`);
for (const summary of subtaskSummaries) {
try {
const subtask = await createSubtask(storyKey, summary);
console.log(` + ${subtask.key}: ${summary}`);
results.created++;
} catch (error) {
console.log(` - FAILED: ${summary} (${error.message})`);
results.failed++;
}
await new Promise(r => setTimeout(r, 100)); // Rate limiting
}
console.log('\n========================================');
console.log(' SUMMARY');
console.log('========================================');
console.log(`Story: ${storyKey}`);
console.log(`Subtasks created: ${results.created}`);
console.log(`Subtasks failed: ${results.failed}`);
console.log(`\nView: ${JIRA_BASE_URL}/browse/${storyKey}`);
console.log('========================================');
}
// Parse args and run
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage:');
console.log(' node jira-add-subtasks.mjs demo');
console.log(' Creates a test story with 5 subtasks');
console.log('');
console.log(' node jira-add-subtasks.mjs SCRUM-148 "Task 1" "Task 2" "Task 3"');
console.log(' Adds subtasks to an existing story');
process.exit(0);
}
if (args[0] === 'demo') {
runDemo().catch(console.error);
} else {
const storyKey = args[0];
const subtasks = args.slice(1);
if (subtasks.length === 0) {
console.error('ERROR: No subtask summaries provided.');
console.log('Usage: node jira-add-subtasks.mjs SCRUM-148 "Task 1" "Task 2"');
process.exit(1);
}
addSubtasksToStory(storyKey, subtasks).catch(console.error);
}
#!/usr/bin/env python3
"""
Jira Add Subtasks (Python)
Add subtasks to an existing story.
Following jira-safe skill patterns for Next-Gen projects.
Usage:
python jira-add-subtasks.py demo
Creates a test story with 5 subtasks
python jira-add-subtasks.py SCRUM-148 "Task 1" "Task 2" "Task 3"
Adds subtasks to an existing story
"""
import base64
import json
import os
import sys
import time
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError
# Load .env file from jira root (two levels up from scripts/)
def load_env():
env_path = Path(__file__).parent.parent.parent / '.env'
if env_path.exists():
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
load_env()
# Configuration from environment variables
JIRA_EMAIL = os.environ.get('JIRA_EMAIL')
JIRA_API_TOKEN = os.environ.get('JIRA_API_TOKEN')
JIRA_BASE_URL = os.environ.get('JIRA_BASE_URL')
PROJECT_KEY = os.environ.get('JIRA_PROJECT_KEY', 'SCRUM')
# Validate required env vars
if not all([JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL]):
print('Error: Missing required environment variables.', file=sys.stderr)
print('Required: JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL', file=sys.stderr)
print('Set these in .claude/skills/jira/.env or export them manually.', file=sys.stderr)
sys.exit(1)
# Build auth header
auth_string = f'{JIRA_EMAIL}:{JIRA_API_TOKEN}'
auth_bytes = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')
HEADERS = {
'Authorization': f'Basic {auth_bytes}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
def make_request(method, path, data=None):
"""Make HTTP request to Jira API."""
url = f'{JIRA_BASE_URL}/rest/api/3{path}'
body = json.dumps(data).encode('utf-8') if data else None
req = Request(url, data=body, headers=HEADERS, method=method)
try:
with urlopen(req) as response:
status = response.status
if status == 204:
return None
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
error_body = e.read().decode('utf-8')
raise Exception(f'{e.code}: {error_body[:200]}')
def create_story(summary):
"""Create a Story."""
fields = {
'project': {'key': PROJECT_KEY},
'issuetype': {'name': 'Story'},
'summary': summary
}
return make_request('POST', '/issue', {'fields': fields})
def create_subtask(parent_key, summary):
"""Create a Subtask under a parent Story.
NOTE: Next-Gen uses 'Subtask' (no hyphen), NOT 'Sub-task'
"""
fields = {
'project': {'key': PROJECT_KEY},
'issuetype': {'name': 'Subtask'}, # Next-Gen: 'Subtask', Classic: 'Sub-task'
'parent': {'key': parent_key},
'summary': summary
}
return make_request('POST', '/issue', {'fields': fields})
def verify_issue(issue_key):
"""Verify an issue exists."""
try:
return make_request('GET', f'/issue/{issue_key}?fields=summary,issuetype')
except:
return None
def run_demo():
"""Demo mode - creates a story with subtasks."""
print('=' * 40)
print(' ADD SUBTASKS DEMO (PYTHON)')
print(' (Following jira-safe skill patterns)')
print('=' * 40 + '\n')
# Create demo story
print('Creating demo Story...')
story = create_story('[Demo-Python] Test story with subtasks')
print(f'+ Story created: {story["key"]}\n')
# Demo subtasks
demo_subtasks = [
'Subtask 1: Research and planning',
'Subtask 2: Implementation',
'Subtask 3: Testing',
'Subtask 4: Documentation',
'Subtask 5: Review and merge'
]
print(f'Adding {len(demo_subtasks)} subtasks to {story["key"]}...')
created = 0
failed = 0
for summary in demo_subtasks:
try:
subtask = create_subtask(story['key'], summary)
print(f' + {subtask["key"]}: {summary}')
created += 1
except Exception as e:
print(f' - FAILED: {summary} ({e})')
failed += 1
time.sleep(0.1) # Rate limiting
print('\n' + '=' * 40)
print(' SUMMARY')
print('=' * 40)
print(f'Story: {story["key"]}')
print(f'Subtasks created: {created}')
print(f'Subtasks failed: {failed}')
print(f'\nView: {JIRA_BASE_URL}/browse/{story["key"]}')
print('=' * 40)
def add_subtasks_to_story(story_key, subtask_summaries):
"""Add subtasks to an existing story."""
print('=' * 40)
print(' ADD SUBTASKS TO STORY (PYTHON)')
print(' (Following jira-safe skill patterns)')
print('=' * 40 + '\n')
# Verify story exists
print(f'Verifying {story_key}...')
story = verify_issue(story_key)
if not story:
print(f'ERROR: Issue {story_key} not found or not accessible.')
sys.exit(1)
print(f'Found: {story["key"]} [{story["fields"]["issuetype"]["name"]}]')
print(f'Summary: {story["fields"]["summary"]}\n')
created = 0
failed = 0
print(f'Adding {len(subtask_summaries)} subtasks...')
for summary in subtask_summaries:
try:
subtask = create_subtask(story_key, summary)
print(f' + {subtask["key"]}: {summary}')
created += 1
except Exception as e:
print(f' - FAILED: {summary} ({e})')
failed += 1
time.sleep(0.1) # Rate limiting
print('\n' + '=' * 40)
print(' SUMMARY')
print('=' * 40)
print(f'Story: {story_key}')
print(f'Subtasks created: {created}')
print(f'Subtasks failed: {failed}')
print(f'\nView: {JIRA_BASE_URL}/browse/{story_key}')
print('=' * 40)
def main():
args = sys.argv[1:]
if not args:
print('Usage:')
print(' python jira-add-subtasks.py demo')
print(' Creates a test story with 5 subtasks')
print('')
print(' python jira-add-subtasks.py SCRUM-148 "Task 1" "Task 2" "Task 3"')
print(' Adds subtasks to an existing story')
sys.exit(0)
if args[0] == 'demo':
run_demo()
else:
story_key = args[0]
subtasks = args[1:]
if not subtasks:
print('ERROR: No subtask summaries provided.')
print('Usage: python jira-add-subtasks.py SCRUM-148 "Task 1" "Task 2"')
sys.exit(1)
add_subtasks_to_story(story_key, subtasks)
if __name__ == '__main__':
main()
// Create MVP Epic and Stories using SAFe methodology
// Based on Tustle Marketing Copilot PRD
// Load from environment variables (set by run.js or manually)
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const PROJECT_KEY = process.env.JIRA_PROJECT_KEY || 'SCRUM';
// Validate required env vars
if (!JIRA_EMAIL || !JIRA_API_TOKEN || !JIRA_BASE_URL) {
console.error('Error: Missing required environment variables.');
console.error('Required: JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL');
console.error('Set these in .claude/skills/jira/.env or export them manually.');
process.exit(1);
}
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
const headers = {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// ====================
// HELPER FUNCTIONS
// ====================
async function jiraRequest(path, options = {}) {
const url = `${JIRA_BASE_URL}/rest/api/3${path}`;
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status}: ${error.substring(0, 300)}`);
}
if (response.status === 204) return null;
return response.json();
}
// Build Atlassian Document Format (ADF) from plain text with sections
function buildADF(sections) {
const content = [];
for (const section of sections) {
if (section.heading) {
content.push({
type: 'heading',
attrs: { level: section.level || 2 },
content: [{ type: 'text', text: section.heading }]
});
}
if (section.paragraph) {
content.push({
type: 'paragraph',
content: [{ type: 'text', text: section.paragraph }]
});
}
if (section.bullets) {
content.push({
type: 'bulletList',
content: section.bullets.map(bullet => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: bullet }] }]
}))
});
}
}
return { type: 'doc', version: 1, content };
}
async function createIssue(fields) {
return jiraRequest('/issue', {
method: 'POST',
body: JSON.stringify({ fields })
});
}
// ====================
// MVP EPIC DEFINITION
// ====================
const mvpEpic = {
project: { key: PROJECT_KEY },
issuetype: { name: 'Epic' },
summary: 'Tustle MVP - 24/7 Marketing Copilot with Brand Memory',
// Note: Epic Name field (customfield_10011) not available in this project
description: buildADF([
{ heading: 'Business Outcome' },
{ paragraph: 'Launch a ChatGPT-style marketing assistant that remembers brand voice, maintains conversation history, and generates on-brand content instantly. Enable small businesses and solo marketers to produce professional marketing content 24/7 without expensive agencies.' },
{ heading: 'Success Metrics' },
{ bullets: [
'User Acquisition: 1,000 registered users in first 90 days',
'Conversion: 5% trial-to-paid conversion rate',
'Engagement: Average 3+ chat sessions per user per week',
'Retention: 60% month-over-month user retention',
'Revenue: $5,000 MRR by end of Q1'
]},
{ heading: 'Scope' },
{ paragraph: 'IN SCOPE: Authentication, brand questionnaire, AI chat with brand context, thread management, subscription billing (Stripe), usage limits, landing page, basic analytics.' },
{ paragraph: 'OUT OF SCOPE: Team collaboration, search/export, message regeneration, templates library, third-party integrations, mobile app.' },
{ heading: 'Target Users' },
{ bullets: [
'Solo Marketing Manager: 1-person marketing team needing to maintain brand voice across channels',
'Small Business Owner: Time-poor entrepreneurs who need marketing help',
'Freelance Marketer: Consultants managing multiple client brands'
]},
{ heading: 'Tech Stack' },
{ bullets: [
'Frontend: Next.js 14 (App Router), React 19, Tailwind CSS v4',
'Backend: Next.js API Routes, Edge Runtime',
'Database: PostgreSQL (Neon) with Drizzle ORM',
'AI: OpenAI GPT-5-nano-2025-08-07',
'Payments: Stripe (subscriptions)',
'Hosting: Vercel, Cloudflare (DNS/WAF)'
]}
])
};
// ====================
// USER STORIES (SAFe Format)
// ====================
const stories = [
// ==================== AUTHENTICATION ====================
{
category: 'Authentication',
summary: 'As a new user, I want to create an account with email and password, so that I can access the marketing copilot',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a new user, I want to create an account with email and password, so that I can access the marketing copilot and start generating content.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Successful Registration', level: 3 },
{ bullets: [
'GIVEN I am on the signup page',
'WHEN I enter a valid email, password (8+ chars, 1 uppercase, 1 number), and accept terms',
'THEN my account is created, I am logged in, and redirected to onboarding'
]},
{ heading: 'Scenario 2: Duplicate Email', level: 3 },
{ bullets: [
'GIVEN an account with my email already exists',
'WHEN I try to register with the same email',
'THEN I see an error "Email already registered" with login link'
]},
{ heading: 'Scenario 3: Invalid Password', level: 3 },
{ bullets: [
'GIVEN I am on the signup page',
'WHEN I enter a password that does not meet requirements',
'THEN I see specific validation errors (min length, uppercase, number)'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Signup API endpoint (/api/auth/signup) implemented',
'[ ] Password hashed with bcrypt (cost factor 12)',
'[ ] JWT token generated and set in HTTP-only cookie',
'[ ] Email validation with proper error messages',
'[ ] Unit tests for signup flow',
'[ ] Integration tests with database'
]}
]),
subtasks: [
'Create signup API route (/api/auth/signup)',
'Implement password hashing with bcrypt',
'Create JWT token generation utility',
'Build signup form component with validation',
'Add error handling and user feedback',
'Write unit tests for signup'
]
},
{
category: 'Authentication',
summary: 'As a returning user, I want to log in with my credentials, so that I can access my saved brands and conversations',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a returning user, I want to log in with my email and password, so that I can access my saved brands, conversation history, and continue where I left off.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Successful Login', level: 3 },
{ bullets: [
'GIVEN I have an existing account',
'WHEN I enter correct email and password',
'THEN I am logged in and redirected to dashboard/chat'
]},
{ heading: 'Scenario 2: Invalid Credentials', level: 3 },
{ bullets: [
'GIVEN I am on the login page',
'WHEN I enter incorrect email or password',
'THEN I see "Invalid email or password" (no specific field indication for security)'
]},
{ heading: 'Scenario 3: Rate Limiting', level: 3 },
{ bullets: [
'GIVEN I have failed login 10 times in an hour',
'WHEN I try to login again',
'THEN I am temporarily blocked with countdown timer'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Login API endpoint (/api/auth/login) implemented',
'[ ] Password verification with bcrypt',
'[ ] JWT refresh token logic',
'[ ] Rate limiting (10 attempts/hour/email)',
'[ ] Remember me functionality (30-day vs 7-day token)',
'[ ] Unit and integration tests'
]}
]),
subtasks: [
'Create login API route (/api/auth/login)',
'Implement password verification',
'Add rate limiting middleware',
'Build login form component',
'Implement "Remember me" functionality',
'Write tests for login flow'
]
},
{
category: 'Authentication',
summary: 'As a user, I want to log out and have my session terminated, so that my account remains secure on shared devices',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want to log out from my account, so that my session is terminated and my account is protected on shared or public devices.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Successful Logout', level: 3 },
{ bullets: [
'GIVEN I am logged in',
'WHEN I click the logout button',
'THEN my session cookie is cleared and I am redirected to login page'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Logout API endpoint (/api/auth/logout) implemented',
'[ ] Cookie properly cleared',
'[ ] Client-side state reset',
'[ ] Redirect to login page'
]}
]),
subtasks: []
},
// ==================== BRAND ONBOARDING ====================
{
category: 'Brand Onboarding',
summary: 'As a new user, I want to complete a brand questionnaire, so that the AI understands my brand voice and can generate on-brand content',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a new user, I want to complete a guided brand questionnaire during onboarding, so that the AI understands my brand voice, messaging rules, and can generate content that sounds like me.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Complete Questionnaire', level: 3 },
{ bullets: [
'GIVEN I am a new user after signup',
'WHEN I complete all 6 steps of the questionnaire',
'THEN my brand profile is saved and I can start chatting'
]},
{ heading: 'Scenario 2: Save Progress', level: 3 },
{ bullets: [
'GIVEN I am on step 3 of questionnaire',
'WHEN I close the browser and return later',
'THEN I can continue from step 3 (draft saved)'
]},
{ heading: 'Questionnaire Steps' },
{ bullets: [
'Step 1: Company Basics (name, industry, tagline, website)',
'Step 2: Target Audience (demographics, pain points, goals)',
'Step 3: Brand Voice (personality sliders, tone descriptors)',
'Step 4: Messaging Rules (banned phrases, required phrases)',
'Step 5: Products/Services (offerings, key benefits)',
'Step 6: Channels & Formatting (platforms, content types)'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] 6-step questionnaire UI implemented',
'[ ] Brand profile stored in database',
'[ ] Progress saved between sessions',
'[ ] Validation on each step',
'[ ] Skip option for optional fields',
'[ ] Review & edit before submit'
]}
]),
subtasks: [
'Create brand questionnaire page layout',
'Build Step 1: Company Basics component',
'Build Step 2: Target Audience component',
'Build Step 3: Brand Voice sliders component',
'Build Step 4: Messaging Rules component',
'Build Step 5: Products/Services component',
'Build Step 6: Channels component',
'Create API routes for brand profile CRUD',
'Implement progress persistence',
'Add validation schemas with Zod'
]
},
{
category: 'Brand Onboarding',
summary: 'As a user, I want to edit my brand profile at any time, so that I can update it as my brand evolves',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want to edit my brand profile at any time from settings, so that I can update my brand voice, messaging rules, and other details as my brand evolves.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Edit Brand Profile', level: 3 },
{ bullets: [
'GIVEN I have a saved brand profile',
'WHEN I go to brand settings and make changes',
'THEN my profile is updated and AI uses new context'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Brand settings page accessible from dashboard',
'[ ] All questionnaire fields editable',
'[ ] Changes reflected in AI context immediately',
'[ ] Change history audit log'
]}
]),
subtasks: [
'Create brand settings page',
'Implement edit mode for questionnaire',
'Add save/cancel functionality',
'Update AI context builder to use latest profile'
]
},
// ==================== AI CHAT ====================
{
category: 'AI Chat',
summary: 'As a user, I want to chat with an AI that understands my brand, so that I can generate on-brand marketing content',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want to chat with an AI assistant that has full context of my brand voice, messaging rules, and products, so that every response is on-brand and ready to use.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Start New Chat', level: 3 },
{ bullets: [
'GIVEN I have a brand profile saved',
'WHEN I start a new chat and send a message',
'THEN AI responds using my brand voice with streaming tokens'
]},
{ heading: 'Scenario 2: Brand Context Applied', level: 3 },
{ bullets: [
'GIVEN my brand has specific tone and banned phrases',
'WHEN I ask AI to write content',
'THEN response matches my brand tone and avoids banned phrases'
]},
{ heading: 'Technical Requirements' },
{ bullets: [
'Edge Runtime for SSE streaming',
'OpenAI GPT-5-nano model',
'Brand context in system prompt',
'Last 20 messages as conversation history',
'Token-by-token streaming display'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Chat API route with SSE streaming (/api/chat)',
'[ ] Brand context builder utility',
'[ ] Chat UI with message bubbles',
'[ ] Streaming token display',
'[ ] Error handling for API failures',
'[ ] Loading states and indicators'
]}
]),
subtasks: [
'Create chat API route with Edge Runtime',
'Build brand context builder (lib/ai/context-builder.ts)',
'Implement OpenAI streaming integration',
'Build chat UI component',
'Add message bubble components',
'Implement streaming token display',
'Add typing indicator',
'Handle error states'
]
},
{
category: 'AI Chat',
summary: 'As a user, I want my chat messages saved to threads, so that I can find and continue past conversations',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want my chat messages automatically saved to threads, so that I can find past conversations, continue where I left off, and reference previous content.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Auto-save Messages', level: 3 },
{ bullets: [
'GIVEN I am chatting with the AI',
'WHEN I send a message and receive a response',
'THEN both messages are saved to the current thread'
]},
{ heading: 'Scenario 2: Load Thread History', level: 3 },
{ bullets: [
'GIVEN I have previous threads',
'WHEN I click on a thread in the sidebar',
'THEN all messages from that thread are loaded'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Messages saved to database after each exchange',
'[ ] Thread sidebar showing all threads',
'[ ] Click to load thread history',
'[ ] Thread title auto-generated from first message',
'[ ] Threads sorted by last activity'
]}
]),
subtasks: [
'Create threads table and API routes',
'Create messages table and API routes',
'Build thread sidebar component',
'Implement thread loading on click',
'Add auto-title generation for threads',
'Sort threads by last activity'
]
},
// ==================== THREAD MANAGEMENT ====================
{
category: 'Thread Management',
summary: 'As a user, I want to create, rename, and delete conversation threads, so that I can organize my marketing work',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want to create new threads, rename existing ones, and delete threads I no longer need, so that I can keep my marketing work organized.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Create New Thread', level: 3 },
{ bullets: [
'GIVEN I am on the chat page',
'WHEN I click "New Thread" button',
'THEN a new empty thread is created and selected'
]},
{ heading: 'Scenario 2: Rename Thread', level: 3 },
{ bullets: [
'GIVEN I have an existing thread',
'WHEN I click edit on thread name',
'THEN I can enter a new name and save it'
]},
{ heading: 'Scenario 3: Delete Thread', level: 3 },
{ bullets: [
'GIVEN I have a thread with messages',
'WHEN I click delete and confirm',
'THEN the thread and all messages are permanently deleted'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] New thread creation API and UI',
'[ ] Thread rename functionality',
'[ ] Thread delete with confirmation modal',
'[ ] Cascade delete of messages'
]}
]),
subtasks: [
'Create new thread API endpoint',
'Add "New Thread" button to sidebar',
'Implement thread rename inline edit',
'Create delete confirmation modal',
'Implement cascade delete for messages'
]
},
// ==================== SUBSCRIPTION & BILLING ====================
{
category: 'Billing',
summary: 'As a trial user, I want to upgrade to a paid plan, so that I can get more messages and features',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a trial user who has found value in the product, I want to upgrade to a paid subscription plan, so that I can get more messages per month and additional features.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: View Plans', level: 3 },
{ bullets: [
'GIVEN I am a trial user',
'WHEN I click "Upgrade" or visit billing page',
'THEN I see Pro ($9.99/mo, 100 msg) and Unlimited ($39.99/mo) plans'
]},
{ heading: 'Scenario 2: Checkout Flow', level: 3 },
{ bullets: [
'GIVEN I am viewing plans',
'WHEN I click "Subscribe" on a plan',
'THEN I am redirected to Stripe Checkout'
]},
{ heading: 'Scenario 3: Successful Payment', level: 3 },
{ bullets: [
'GIVEN I complete Stripe checkout',
'WHEN payment succeeds',
'THEN my plan is upgraded and I return to dashboard with confirmation'
]},
{ heading: 'Subscription Tiers' },
{ bullets: [
'Trial: $0, 50 messages lifetime, 1 brand',
'Pro: $9.99/month, 100 messages/month, 10 brands',
'Unlimited: $39.99/month, unlimited messages, unlimited brands'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Stripe Checkout integration',
'[ ] Billing page with plan comparison',
'[ ] Webhook handler for checkout.session.completed',
'[ ] Subscription status stored in database',
'[ ] Plan upgrade reflected in UI immediately'
]}
]),
subtasks: [
'Create billing page with plan cards',
'Integrate Stripe Checkout',
'Create checkout API route',
'Implement Stripe webhooks handler',
'Store subscription in database',
'Add upgrade confirmation UI'
]
},
{
category: 'Billing',
summary: 'As a paid user, I want to manage my subscription, so that I can upgrade, downgrade, or cancel as needed',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a paid subscriber, I want to manage my subscription through a billing portal, so that I can upgrade, downgrade, update payment method, or cancel.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Access Billing Portal', level: 3 },
{ bullets: [
'GIVEN I am a paid subscriber',
'WHEN I click "Manage Subscription" in settings',
'THEN I am redirected to Stripe Customer Portal'
]},
{ heading: 'Scenario 2: Cancel Subscription', level: 3 },
{ bullets: [
'GIVEN I cancel through portal',
'WHEN cancellation is processed',
'THEN I keep access until period end, then revert to trial limits'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Stripe Customer Portal integration',
'[ ] Webhook handlers for subscription.updated, subscription.deleted',
'[ ] Grace period handling for cancellations',
'[ ] UI reflects current plan and status'
]}
]),
subtasks: [
'Create customer portal API route',
'Add subscription webhooks (updated, deleted)',
'Implement grace period logic',
'Show subscription status in UI'
]
},
// ==================== USAGE TRACKING ====================
{
category: 'Usage',
summary: 'As a user, I want to see my remaining messages, so that I know when to upgrade or pace my usage',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a user, I want to see my remaining message count clearly displayed, so that I can track my usage and know when I need to upgrade my plan.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: View Usage', level: 3 },
{ bullets: [
'GIVEN I am logged in',
'WHEN I look at the dashboard or chat page',
'THEN I see "X messages remaining" or "X/100 used this month"'
]},
{ heading: 'Scenario 2: Usage Warning', level: 3 },
{ bullets: [
'GIVEN I have used 80% of my messages',
'WHEN I view the app',
'THEN I see a warning banner suggesting upgrade'
]},
{ heading: 'Scenario 3: Limit Reached', level: 3 },
{ bullets: [
'GIVEN I have used all my messages',
'WHEN I try to send another message',
'THEN I see upgrade prompt instead of sending'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Usage counter component',
'[ ] Usage API endpoint',
'[ ] 80% warning banner',
'[ ] 100% limit enforcement with upgrade prompt',
'[ ] Monthly reset for paid plans'
]}
]),
subtasks: [
'Create usage tracking API',
'Build usage counter component',
'Implement warning banner at 80%',
'Add limit enforcement in chat API',
'Create upgrade prompt modal'
]
},
// ==================== LANDING PAGE ====================
{
category: 'Marketing',
summary: 'As a visitor, I want to see a compelling landing page, so that I understand the product value and am motivated to sign up',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a website visitor, I want to see a clear, compelling landing page that explains what Tustle does and its benefits, so that I understand the value and am motivated to start a free trial.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Landing Page Content', level: 3 },
{ bullets: [
'GIVEN I visit tustle.ai',
'WHEN the page loads',
'THEN I see hero section, features, pricing, and CTA'
]},
{ heading: 'Landing Page Sections' },
{ bullets: [
'Hero: Headline, subheadline, CTA button, product screenshot',
'Problem: Pain points of manual content creation',
'Solution: How Tustle solves it with brand memory',
'Features: Key capabilities with icons',
'Pricing: Plan comparison table',
'Testimonials: Social proof (for launch)',
'CTA: Final call-to-action with signup'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Responsive landing page implemented',
'[ ] All sections with compelling copy',
'[ ] Pricing table with plan features',
'[ ] Mobile-optimized design',
'[ ] Fast page load (<3s)',
'[ ] SEO meta tags'
]}
]),
subtasks: [
'Create landing page layout',
'Build hero section component',
'Build features section component',
'Build pricing table component',
'Add CTA sections',
'Implement responsive design',
'Add SEO meta tags'
]
},
// ==================== DATABASE & INFRASTRUCTURE ====================
{
category: 'Infrastructure',
summary: 'As a developer, I want the database schema deployed, so that the application can store and retrieve data',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a developer, I want the complete database schema deployed to Neon PostgreSQL, so that all application data can be properly stored, queried, and maintained.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Schema Migration', level: 3 },
{ bullets: [
'GIVEN I have the schema definition',
'WHEN I run migrations',
'THEN all tables, indexes, and constraints are created'
]},
{ heading: 'Database Tables' },
{ bullets: [
'users: Authentication and profile',
'brands: Brand ownership and metadata',
'brand_profiles: Questionnaire responses',
'threads: Conversation containers',
'messages: Chat history',
'subscriptions: Stripe subscription state',
'usage_periods: Monthly usage tracking',
'audit_logs: Security audit trail'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Drizzle schema defined for all tables',
'[ ] Migrations generated and tested',
'[ ] Neon database provisioned',
'[ ] Connection pooling configured',
'[ ] Indexes optimized for queries'
]}
]),
subtasks: [
'Create Drizzle schema file (lib/db/schema.ts)',
'Generate initial migration',
'Set up Neon project and branch',
'Configure connection pooling',
'Add database indexes',
'Create seed script for testing'
]
},
{
category: 'Infrastructure',
summary: 'As a developer, I want CI/CD and deployment configured, so that code changes deploy automatically',
description: buildADF([
{ heading: 'User Story' },
{ paragraph: 'As a developer, I want continuous integration and deployment configured, so that code pushed to GitHub automatically deploys to Vercel production.' },
{ heading: 'Acceptance Criteria' },
{ heading: 'Scenario 1: Auto Deploy', level: 3 },
{ bullets: [
'GIVEN code is pushed to main branch',
'WHEN GitHub receives the push',
'THEN Vercel automatically builds and deploys'
]},
{ heading: 'Scenario 2: Preview Deployments', level: 3 },
{ bullets: [
'GIVEN a PR is opened',
'WHEN GitHub triggers Vercel',
'THEN a preview deployment is created for testing'
]},
{ heading: 'Definition of Done' },
{ bullets: [
'[ ] Vercel project connected to GitHub',
'[ ] Environment variables configured',
'[ ] Production domain configured',
'[ ] Preview deployments working',
'[ ] Build notifications set up'
]}
]),
subtasks: [
'Connect Vercel to GitHub repo',
'Configure environment variables in Vercel',
'Set up production domain',
'Test preview deployments',
'Add build status badge to README'
]
}
];
// ====================
// EXECUTION
// ====================
async function main() {
console.log('========================================');
console.log(' CREATE MVP EPIC AND STORIES (SAFe)');
console.log('========================================\n');
const results = {
epic: null,
stories: [],
subtasks: [],
failed: []
};
// 1. Create MVP Epic
console.log('Creating MVP Epic...');
try {
results.epic = await createIssue(mvpEpic);
console.log(`\nEpic Created: ${results.epic.key}`);
console.log(`URL: ${JIRA_BASE_URL}/browse/${results.epic.key}\n`);
} catch (error) {
console.error(`Failed to create Epic: ${error.message}`);
return;
}
await new Promise(r => setTimeout(r, 200));
// 2. For Next-Gen projects, use parent field to link Stories to Epic
console.log('This is a Next-Gen project - using parent field for Epic linking\n');
// 3. Create Stories under Epic
console.log('Creating Stories...\n');
for (let i = 0; i < stories.length; i++) {
const story = stories[i];
const storyNumber = i + 1;
try {
const storyFields = {
project: { key: PROJECT_KEY },
issuetype: { name: 'Story' },
summary: story.summary,
description: story.description,
parent: { key: results.epic.key }, // Next-Gen: use parent field
labels: [story.category.toLowerCase().replace(/\s+/g, '-')]
};
const createdStory = await createIssue(storyFields);
results.stories.push({ key: createdStory.key, summary: story.summary, category: story.category });
console.log(`[${storyNumber}/${stories.length}] Story Created: ${createdStory.key}`);
console.log(` ${story.summary.substring(0, 60)}...`);
// Create Subtasks if any
if (story.subtasks && story.subtasks.length > 0) {
for (const subtaskSummary of story.subtasks) {
try {
const subtaskFields = {
project: { key: PROJECT_KEY },
issuetype: { name: 'Subtask' },
parent: { key: createdStory.key },
summary: subtaskSummary
};
const createdSubtask = await createIssue(subtaskFields);
results.subtasks.push({ key: createdSubtask.key, parent: createdStory.key });
console.log(` -> Subtask: ${createdSubtask.key} - ${subtaskSummary.substring(0, 40)}...`);
await new Promise(r => setTimeout(r, 100));
} catch (subtaskError) {
console.log(` -> Failed subtask: ${subtaskError.message}`);
results.failed.push({ type: 'subtask', summary: subtaskSummary, error: subtaskError.message });
}
}
}
console.log('');
await new Promise(r => setTimeout(r, 150));
} catch (storyError) {
console.log(`[${storyNumber}/${stories.length}] FAILED: ${story.summary.substring(0, 50)}...`);
console.log(` Error: ${storyError.message}\n`);
results.failed.push({ type: 'story', summary: story.summary, error: storyError.message });
}
}
// Summary
console.log('\n========================================');
console.log(' SUMMARY');
console.log('========================================');
console.log(`Epic: ${results.epic.key}`);
console.log(`Stories: ${results.stories.length}/${stories.length}`);
console.log(`Subtasks: ${results.subtasks.length}`);
if (results.failed.length > 0) {
console.log(`\nFailed: ${results.failed.length}`);
results.failed.forEach(f => console.log(` - ${f.type}: ${f.summary.substring(0, 40)}...`));
}
// Group by category
console.log('\n--- By Category ---');
const byCategory = {};
for (const s of results.stories) {
if (!byCategory[s.category]) byCategory[s.category] = [];
byCategory[s.category].push(s.key);
}
for (const [cat, keys] of Object.entries(byCategory)) {
console.log(`${cat}: ${keys.join(', ')}`);
}
console.log('\n========================================');
console.log(' LINKS');
console.log('========================================');
console.log(`Epic: ${JIRA_BASE_URL}/browse/${results.epic.key}`);
console.log(`Board: ${JIRA_BASE_URL}/jira/software/projects/${PROJECT_KEY}/boards/1`);
console.log(`Backlog: ${JIRA_BASE_URL}/jira/software/projects/${PROJECT_KEY}/boards/1/backlog`);
console.log('========================================\n');
return results;
}
main().catch(console.error);
#!/usr/bin/env python3
"""
Create MVP Epic and Stories using SAFe methodology
Based on Tustle Marketing Copilot PRD
Following jira-safe skill patterns for Next-Gen projects.
Usage:
python create-mvp.py
Creates complete MVP Epic with Stories and Subtasks
"""
import base64
import json
import os
import sys
import time
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError
# Load .env file from jira root (two levels up from scripts/)
def load_env():
env_path = Path(__file__).parent.parent.parent / '.env'
if env_path.exists():
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
load_env()
# Configuration from environment variables
JIRA_EMAIL = os.environ.get('JIRA_EMAIL')
JIRA_API_TOKEN = os.environ.get('JIRA_API_TOKEN')
JIRA_BASE_URL = os.environ.get('JIRA_BASE_URL')
PROJECT_KEY = os.environ.get('JIRA_PROJECT_KEY', 'SCRUM')
# Validate required env vars
if not all([JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL]):
print('Error: Missing required environment variables.', file=sys.stderr)
print('Required: JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL', file=sys.stderr)
print('Set these in .claude/skills/jira/.env or export them manually.', file=sys.stderr)
sys.exit(1)
# Build auth header
auth_string = f'{JIRA_EMAIL}:{JIRA_API_TOKEN}'
auth_bytes = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')
HEADERS = {
'Authorization': f'Basic {auth_bytes}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# ====================
# HELPER FUNCTIONS
# ====================
def jira_request(path, method='GET', data=None):
"""Make HTTP request to Jira API."""
url = f'{JIRA_BASE_URL}/rest/api/3{path}'
body = json.dumps(data).encode('utf-8') if data else None
req = Request(url, data=body, headers=HEADERS, method=method)
try:
with urlopen(req) as response:
status = response.status
if status == 204:
return None
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
error_body = e.read().decode('utf-8')
raise Exception(f'{e.code}: {error_body[:300]}')
def build_adf(sections):
"""Build Atlassian Document Format (ADF) from sections.
Each section can have:
- heading: str (heading text)
- level: int (heading level, default 2)
- paragraph: str (paragraph text)
- bullets: list[str] (bullet points)
"""
content = []
for section in sections:
if 'heading' in section:
content.append({
'type': 'heading',
'attrs': {'level': section.get('level', 2)},
'content': [{'type': 'text', 'text': section['heading']}]
})
if 'paragraph' in section:
content.append({
'type': 'paragraph',
'content': [{'type': 'text', 'text': section['paragraph']}]
})
if 'bullets' in section:
content.append({
'type': 'bulletList',
'content': [
{
'type': 'listItem',
'content': [{'type': 'paragraph', 'content': [{'type': 'text', 'text': bullet}]}]
}
for bullet in section['bullets']
]
})
return {'type': 'doc', 'version': 1, 'content': content}
def create_issue(fields):
"""Create a Jira issue."""
return jira_request('/issue', method='POST', data={'fields': fields})
# ====================
# MVP EPIC DEFINITION
# ====================
MVP_EPIC = {
'project': {'key': PROJECT_KEY},
'issuetype': {'name': 'Epic'},
'summary': 'Tustle MVP - 24/7 Marketing Copilot with Brand Memory',
'description': build_adf([
{'heading': 'Business Outcome'},
{'paragraph': 'Launch a ChatGPT-style marketing assistant that remembers brand voice, maintains conversation history, and generates on-brand content instantly. Enable small businesses and solo marketers to produce professional marketing content 24/7 without expensive agencies.'},
{'heading': 'Success Metrics'},
{'bullets': [
'User Acquisition: 1,000 registered users in first 90 days',
'Conversion: 5% trial-to-paid conversion rate',
'Engagement: Average 3+ chat sessions per user per week',
'Retention: 60% month-over-month user retention',
'Revenue: $5,000 MRR by end of Q1'
]},
{'heading': 'Scope'},
{'paragraph': 'IN SCOPE: Authentication, brand questionnaire, AI chat with brand context, thread management, subscription billing (Stripe), usage limits, landing page, basic analytics.'},
{'paragraph': 'OUT OF SCOPE: Team collaboration, search/export, message regeneration, templates library, third-party integrations, mobile app.'},
{'heading': 'Target Users'},
{'bullets': [
'Solo Marketing Manager: 1-person marketing team needing to maintain brand voice across channels',
'Small Business Owner: Time-poor entrepreneurs who need marketing help',
'Freelance Marketer: Consultants managing multiple client brands'
]},
{'heading': 'Tech Stack'},
{'bullets': [
'Frontend: Next.js 14 (App Router), React 19, Tailwind CSS v4',
'Backend: Next.js API Routes, Edge Runtime',
'Database: PostgreSQL (Neon) with Drizzle ORM',
'AI: OpenAI GPT-5-nano-2025-08-07',
'Payments: Stripe (subscriptions)',
'Hosting: Vercel, Cloudflare (DNS/WAF)'
]}
])
}
# ====================
# USER STORIES (SAFe Format)
# ====================
STORIES = [
# ==================== AUTHENTICATION ====================
{
'category': 'Authentication',
'summary': 'As a new user, I want to create an account with email and password, so that I can access the marketing copilot',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a new user, I want to create an account with email and password, so that I can access the marketing copilot and start generating content.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Successful Registration', 'level': 3},
{'bullets': [
'GIVEN I am on the signup page',
'WHEN I enter a valid email, password (8+ chars, 1 uppercase, 1 number), and accept terms',
'THEN my account is created, I am logged in, and redirected to onboarding'
]},
{'heading': 'Scenario 2: Duplicate Email', 'level': 3},
{'bullets': [
'GIVEN an account with my email already exists',
'WHEN I try to register with the same email',
'THEN I see an error "Email already registered" with login link'
]},
{'heading': 'Scenario 3: Invalid Password', 'level': 3},
{'bullets': [
'GIVEN I am on the signup page',
'WHEN I enter a password that does not meet requirements',
'THEN I see specific validation errors (min length, uppercase, number)'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Signup API endpoint (/api/auth/signup) implemented',
'[ ] Password hashed with bcrypt (cost factor 12)',
'[ ] JWT token generated and set in HTTP-only cookie',
'[ ] Email validation with proper error messages',
'[ ] Unit tests for signup flow',
'[ ] Integration tests with database'
]}
]),
'subtasks': [
'Create signup API route (/api/auth/signup)',
'Implement password hashing with bcrypt',
'Create JWT token generation utility',
'Build signup form component with validation',
'Add error handling and user feedback',
'Write unit tests for signup'
]
},
{
'category': 'Authentication',
'summary': 'As a returning user, I want to log in with my credentials, so that I can access my saved brands and conversations',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a returning user, I want to log in with my email and password, so that I can access my saved brands, conversation history, and continue where I left off.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Successful Login', 'level': 3},
{'bullets': [
'GIVEN I have an existing account',
'WHEN I enter correct email and password',
'THEN I am logged in and redirected to dashboard/chat'
]},
{'heading': 'Scenario 2: Invalid Credentials', 'level': 3},
{'bullets': [
'GIVEN I am on the login page',
'WHEN I enter incorrect email or password',
'THEN I see "Invalid email or password" (no specific field indication for security)'
]},
{'heading': 'Scenario 3: Rate Limiting', 'level': 3},
{'bullets': [
'GIVEN I have failed login 10 times in an hour',
'WHEN I try to login again',
'THEN I am temporarily blocked with countdown timer'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Login API endpoint (/api/auth/login) implemented',
'[ ] Password verification with bcrypt',
'[ ] JWT refresh token logic',
'[ ] Rate limiting (10 attempts/hour/email)',
'[ ] Remember me functionality (30-day vs 7-day token)',
'[ ] Unit and integration tests'
]}
]),
'subtasks': [
'Create login API route (/api/auth/login)',
'Implement password verification',
'Add rate limiting middleware',
'Build login form component',
'Implement "Remember me" functionality',
'Write tests for login flow'
]
},
{
'category': 'Authentication',
'summary': 'As a user, I want to log out and have my session terminated, so that my account remains secure on shared devices',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want to log out from my account, so that my session is terminated and my account is protected on shared or public devices.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Successful Logout', 'level': 3},
{'bullets': [
'GIVEN I am logged in',
'WHEN I click the logout button',
'THEN my session cookie is cleared and I am redirected to login page'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Logout API endpoint (/api/auth/logout) implemented',
'[ ] Cookie properly cleared',
'[ ] Client-side state reset',
'[ ] Redirect to login page'
]}
]),
'subtasks': []
},
# ==================== BRAND ONBOARDING ====================
{
'category': 'Brand Onboarding',
'summary': 'As a new user, I want to complete a brand questionnaire, so that the AI understands my brand voice and can generate on-brand content',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a new user, I want to complete a guided brand questionnaire during onboarding, so that the AI understands my brand voice, messaging rules, and can generate content that sounds like me.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Complete Questionnaire', 'level': 3},
{'bullets': [
'GIVEN I am a new user after signup',
'WHEN I complete all 6 steps of the questionnaire',
'THEN my brand profile is saved and I can start chatting'
]},
{'heading': 'Scenario 2: Save Progress', 'level': 3},
{'bullets': [
'GIVEN I am on step 3 of questionnaire',
'WHEN I close the browser and return later',
'THEN I can continue from step 3 (draft saved)'
]},
{'heading': 'Questionnaire Steps'},
{'bullets': [
'Step 1: Company Basics (name, industry, tagline, website)',
'Step 2: Target Audience (demographics, pain points, goals)',
'Step 3: Brand Voice (personality sliders, tone descriptors)',
'Step 4: Messaging Rules (banned phrases, required phrases)',
'Step 5: Products/Services (offerings, key benefits)',
'Step 6: Channels & Formatting (platforms, content types)'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] 6-step questionnaire UI implemented',
'[ ] Brand profile stored in database',
'[ ] Progress saved between sessions',
'[ ] Validation on each step',
'[ ] Skip option for optional fields',
'[ ] Review & edit before submit'
]}
]),
'subtasks': [
'Create brand questionnaire page layout',
'Build Step 1: Company Basics component',
'Build Step 2: Target Audience component',
'Build Step 3: Brand Voice sliders component',
'Build Step 4: Messaging Rules component',
'Build Step 5: Products/Services component',
'Build Step 6: Channels component',
'Create API routes for brand profile CRUD',
'Implement progress persistence',
'Add validation schemas with Zod'
]
},
{
'category': 'Brand Onboarding',
'summary': 'As a user, I want to edit my brand profile at any time, so that I can update it as my brand evolves',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want to edit my brand profile at any time from settings, so that I can update my brand voice, messaging rules, and other details as my brand evolves.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Edit Brand Profile', 'level': 3},
{'bullets': [
'GIVEN I have a saved brand profile',
'WHEN I go to brand settings and make changes',
'THEN my profile is updated and AI uses new context'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Brand settings page accessible from dashboard',
'[ ] All questionnaire fields editable',
'[ ] Changes reflected in AI context immediately',
'[ ] Change history audit log'
]}
]),
'subtasks': [
'Create brand settings page',
'Implement edit mode for questionnaire',
'Add save/cancel functionality',
'Update AI context builder to use latest profile'
]
},
# ==================== AI CHAT ====================
{
'category': 'AI Chat',
'summary': 'As a user, I want to chat with an AI that understands my brand, so that I can generate on-brand marketing content',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want to chat with an AI assistant that has full context of my brand voice, messaging rules, and products, so that every response is on-brand and ready to use.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Start New Chat', 'level': 3},
{'bullets': [
'GIVEN I have a brand profile saved',
'WHEN I start a new chat and send a message',
'THEN AI responds using my brand voice with streaming tokens'
]},
{'heading': 'Scenario 2: Brand Context Applied', 'level': 3},
{'bullets': [
'GIVEN my brand has specific tone and banned phrases',
'WHEN I ask AI to write content',
'THEN response matches my brand tone and avoids banned phrases'
]},
{'heading': 'Technical Requirements'},
{'bullets': [
'Edge Runtime for SSE streaming',
'OpenAI GPT-5-nano model',
'Brand context in system prompt',
'Last 20 messages as conversation history',
'Token-by-token streaming display'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Chat API route with SSE streaming (/api/chat)',
'[ ] Brand context builder utility',
'[ ] Chat UI with message bubbles',
'[ ] Streaming token display',
'[ ] Error handling for API failures',
'[ ] Loading states and indicators'
]}
]),
'subtasks': [
'Create chat API route with Edge Runtime',
'Build brand context builder (lib/ai/context-builder.ts)',
'Implement OpenAI streaming integration',
'Build chat UI component',
'Add message bubble components',
'Implement streaming token display',
'Add typing indicator',
'Handle error states'
]
},
{
'category': 'AI Chat',
'summary': 'As a user, I want my chat messages saved to threads, so that I can find and continue past conversations',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want my chat messages automatically saved to threads, so that I can find past conversations, continue where I left off, and reference previous content.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Auto-save Messages', 'level': 3},
{'bullets': [
'GIVEN I am chatting with the AI',
'WHEN I send a message and receive a response',
'THEN both messages are saved to the current thread'
]},
{'heading': 'Scenario 2: Load Thread History', 'level': 3},
{'bullets': [
'GIVEN I have previous threads',
'WHEN I click on a thread in the sidebar',
'THEN all messages from that thread are loaded'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Messages saved to database after each exchange',
'[ ] Thread sidebar showing all threads',
'[ ] Click to load thread history',
'[ ] Thread title auto-generated from first message',
'[ ] Threads sorted by last activity'
]}
]),
'subtasks': [
'Create threads table and API routes',
'Create messages table and API routes',
'Build thread sidebar component',
'Implement thread loading on click',
'Add auto-title generation for threads',
'Sort threads by last activity'
]
},
# ==================== THREAD MANAGEMENT ====================
{
'category': 'Thread Management',
'summary': 'As a user, I want to create, rename, and delete conversation threads, so that I can organize my marketing work',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want to create new threads, rename existing ones, and delete threads I no longer need, so that I can keep my marketing work organized.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Create New Thread', 'level': 3},
{'bullets': [
'GIVEN I am on the chat page',
'WHEN I click "New Thread" button',
'THEN a new empty thread is created and selected'
]},
{'heading': 'Scenario 2: Rename Thread', 'level': 3},
{'bullets': [
'GIVEN I have an existing thread',
'WHEN I click edit on thread name',
'THEN I can enter a new name and save it'
]},
{'heading': 'Scenario 3: Delete Thread', 'level': 3},
{'bullets': [
'GIVEN I have a thread with messages',
'WHEN I click delete and confirm',
'THEN the thread and all messages are permanently deleted'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] New thread creation API and UI',
'[ ] Thread rename functionality',
'[ ] Thread delete with confirmation modal',
'[ ] Cascade delete of messages'
]}
]),
'subtasks': [
'Create new thread API endpoint',
'Add "New Thread" button to sidebar',
'Implement thread rename inline edit',
'Create delete confirmation modal',
'Implement cascade delete for messages'
]
},
# ==================== SUBSCRIPTION & BILLING ====================
{
'category': 'Billing',
'summary': 'As a trial user, I want to upgrade to a paid plan, so that I can get more messages and features',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a trial user who has found value in the product, I want to upgrade to a paid subscription plan, so that I can get more messages per month and additional features.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: View Plans', 'level': 3},
{'bullets': [
'GIVEN I am a trial user',
'WHEN I click "Upgrade" or visit billing page',
'THEN I see Pro ($9.99/mo, 100 msg) and Unlimited ($39.99/mo) plans'
]},
{'heading': 'Scenario 2: Checkout Flow', 'level': 3},
{'bullets': [
'GIVEN I am viewing plans',
'WHEN I click "Subscribe" on a plan',
'THEN I am redirected to Stripe Checkout'
]},
{'heading': 'Scenario 3: Successful Payment', 'level': 3},
{'bullets': [
'GIVEN I complete Stripe checkout',
'WHEN payment succeeds',
'THEN my plan is upgraded and I return to dashboard with confirmation'
]},
{'heading': 'Subscription Tiers'},
{'bullets': [
'Trial: $0, 50 messages lifetime, 1 brand',
'Pro: $9.99/month, 100 messages/month, 10 brands',
'Unlimited: $39.99/month, unlimited messages, unlimited brands'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Stripe Checkout integration',
'[ ] Billing page with plan comparison',
'[ ] Webhook handler for checkout.session.completed',
'[ ] Subscription status stored in database',
'[ ] Plan upgrade reflected in UI immediately'
]}
]),
'subtasks': [
'Create billing page with plan cards',
'Integrate Stripe Checkout',
'Create checkout API route',
'Implement Stripe webhooks handler',
'Store subscription in database',
'Add upgrade confirmation UI'
]
},
{
'category': 'Billing',
'summary': 'As a paid user, I want to manage my subscription, so that I can upgrade, downgrade, or cancel as needed',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a paid subscriber, I want to manage my subscription through a billing portal, so that I can upgrade, downgrade, update payment method, or cancel.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Access Billing Portal', 'level': 3},
{'bullets': [
'GIVEN I am a paid subscriber',
'WHEN I click "Manage Subscription" in settings',
'THEN I am redirected to Stripe Customer Portal'
]},
{'heading': 'Scenario 2: Cancel Subscription', 'level': 3},
{'bullets': [
'GIVEN I cancel through portal',
'WHEN cancellation is processed',
'THEN I keep access until period end, then revert to trial limits'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Stripe Customer Portal integration',
'[ ] Webhook handlers for subscription.updated, subscription.deleted',
'[ ] Grace period handling for cancellations',
'[ ] UI reflects current plan and status'
]}
]),
'subtasks': [
'Create customer portal API route',
'Add subscription webhooks (updated, deleted)',
'Implement grace period logic',
'Show subscription status in UI'
]
},
# ==================== USAGE TRACKING ====================
{
'category': 'Usage',
'summary': 'As a user, I want to see my remaining messages, so that I know when to upgrade or pace my usage',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a user, I want to see my remaining message count clearly displayed, so that I can track my usage and know when I need to upgrade my plan.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: View Usage', 'level': 3},
{'bullets': [
'GIVEN I am logged in',
'WHEN I look at the dashboard or chat page',
'THEN I see "X messages remaining" or "X/100 used this month"'
]},
{'heading': 'Scenario 2: Usage Warning', 'level': 3},
{'bullets': [
'GIVEN I have used 80% of my messages',
'WHEN I view the app',
'THEN I see a warning banner suggesting upgrade'
]},
{'heading': 'Scenario 3: Limit Reached', 'level': 3},
{'bullets': [
'GIVEN I have used all my messages',
'WHEN I try to send another message',
'THEN I see upgrade prompt instead of sending'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Usage counter component',
'[ ] Usage API endpoint',
'[ ] 80% warning banner',
'[ ] 100% limit enforcement with upgrade prompt',
'[ ] Monthly reset for paid plans'
]}
]),
'subtasks': [
'Create usage tracking API',
'Build usage counter component',
'Implement warning banner at 80%',
'Add limit enforcement in chat API',
'Create upgrade prompt modal'
]
},
# ==================== LANDING PAGE ====================
{
'category': 'Marketing',
'summary': 'As a visitor, I want to see a compelling landing page, so that I understand the product value and am motivated to sign up',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a website visitor, I want to see a clear, compelling landing page that explains what Tustle does and its benefits, so that I understand the value and am motivated to start a free trial.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Landing Page Content', 'level': 3},
{'bullets': [
'GIVEN I visit tustle.ai',
'WHEN the page loads',
'THEN I see hero section, features, pricing, and CTA'
]},
{'heading': 'Landing Page Sections'},
{'bullets': [
'Hero: Headline, subheadline, CTA button, product screenshot',
'Problem: Pain points of manual content creation',
'Solution: How Tustle solves it with brand memory',
'Features: Key capabilities with icons',
'Pricing: Plan comparison table',
'Testimonials: Social proof (for launch)',
'CTA: Final call-to-action with signup'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Responsive landing page implemented',
'[ ] All sections with compelling copy',
'[ ] Pricing table with plan features',
'[ ] Mobile-optimized design',
'[ ] Fast page load (<3s)',
'[ ] SEO meta tags'
]}
]),
'subtasks': [
'Create landing page layout',
'Build hero section component',
'Build features section component',
'Build pricing table component',
'Add CTA sections',
'Implement responsive design',
'Add SEO meta tags'
]
},
# ==================== DATABASE & INFRASTRUCTURE ====================
{
'category': 'Infrastructure',
'summary': 'As a developer, I want the database schema deployed, so that the application can store and retrieve data',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a developer, I want the complete database schema deployed to Neon PostgreSQL, so that all application data can be properly stored, queried, and maintained.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Schema Migration', 'level': 3},
{'bullets': [
'GIVEN I have the schema definition',
'WHEN I run migrations',
'THEN all tables, indexes, and constraints are created'
]},
{'heading': 'Database Tables'},
{'bullets': [
'users: Authentication and profile',
'brands: Brand ownership and metadata',
'brand_profiles: Questionnaire responses',
'threads: Conversation containers',
'messages: Chat history',
'subscriptions: Stripe subscription state',
'usage_periods: Monthly usage tracking',
'audit_logs: Security audit trail'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Drizzle schema defined for all tables',
'[ ] Migrations generated and tested',
'[ ] Neon database provisioned',
'[ ] Connection pooling configured',
'[ ] Indexes optimized for queries'
]}
]),
'subtasks': [
'Create Drizzle schema file (lib/db/schema.ts)',
'Generate initial migration',
'Set up Neon project and branch',
'Configure connection pooling',
'Add database indexes',
'Create seed script for testing'
]
},
{
'category': 'Infrastructure',
'summary': 'As a developer, I want CI/CD and deployment configured, so that code changes deploy automatically',
'description': build_adf([
{'heading': 'User Story'},
{'paragraph': 'As a developer, I want continuous integration and deployment configured, so that code pushed to GitHub automatically deploys to Vercel production.'},
{'heading': 'Acceptance Criteria'},
{'heading': 'Scenario 1: Auto Deploy', 'level': 3},
{'bullets': [
'GIVEN code is pushed to main branch',
'WHEN GitHub receives the push',
'THEN Vercel automatically builds and deploys'
]},
{'heading': 'Scenario 2: Preview Deployments', 'level': 3},
{'bullets': [
'GIVEN a PR is opened',
'WHEN GitHub triggers Vercel',
'THEN a preview deployment is created for testing'
]},
{'heading': 'Definition of Done'},
{'bullets': [
'[ ] Vercel project connected to GitHub',
'[ ] Environment variables configured',
'[ ] Production domain configured',
'[ ] Preview deployments working',
'[ ] Build notifications set up'
]}
]),
'subtasks': [
'Connect Vercel to GitHub repo',
'Configure environment variables in Vercel',
'Set up production domain',
'Test preview deployments',
'Add build status badge to README'
]
}
]
# ====================
# EXECUTION
# ====================
def main():
print('=' * 40)
print(' CREATE MVP EPIC AND STORIES (PYTHON)')
print(' (SAFe Methodology)')
print('=' * 40 + '\n')
results = {
'epic': None,
'stories': [],
'subtasks': [],
'failed': []
}
# 1. Create MVP Epic
print('Creating MVP Epic...')
try:
results['epic'] = create_issue(MVP_EPIC)
print(f"\nEpic Created: {results['epic']['key']}")
print(f"URL: {JIRA_BASE_URL}/browse/{results['epic']['key']}\n")
except Exception as e:
print(f'Failed to create Epic: {e}')
return
time.sleep(0.2)
# 2. For Next-Gen projects, use parent field to link Stories to Epic
print('This is a Next-Gen project - using parent field for Epic linking\n')
# 3. Create Stories under Epic
print('Creating Stories...\n')
for i, story in enumerate(STORIES):
story_number = i + 1
try:
story_fields = {
'project': {'key': PROJECT_KEY},
'issuetype': {'name': 'Story'},
'summary': story['summary'],
'description': story['description'],
'parent': {'key': results['epic']['key']}, # Next-Gen: use parent field
'labels': [story['category'].lower().replace(' ', '-')]
}
created_story = create_issue(story_fields)
results['stories'].append({
'key': created_story['key'],
'summary': story['summary'],
'category': story['category']
})
print(f"[{story_number}/{len(STORIES)}] Story Created: {created_story['key']}")
print(f" {story['summary'][:60]}...")
# Create Subtasks if any
if story.get('subtasks'):
for subtask_summary in story['subtasks']:
try:
subtask_fields = {
'project': {'key': PROJECT_KEY},
'issuetype': {'name': 'Subtask'},
'parent': {'key': created_story['key']},
'summary': subtask_summary
}
created_subtask = create_issue(subtask_fields)
results['subtasks'].append({
'key': created_subtask['key'],
'parent': created_story['key']
})
print(f" -> Subtask: {created_subtask['key']} - {subtask_summary[:40]}...")
time.sleep(0.1)
except Exception as subtask_error:
print(f" -> Failed subtask: {subtask_error}")
results['failed'].append({
'type': 'subtask',
'summary': subtask_summary,
'error': str(subtask_error)
})
print('')
time.sleep(0.15)
except Exception as story_error:
print(f"[{story_number}/{len(STORIES)}] FAILED: {story['summary'][:50]}...")
print(f" Error: {story_error}\n")
results['failed'].append({
'type': 'story',
'summary': story['summary'],
'error': str(story_error)
})
# Summary
print('\n' + '=' * 40)
print(' SUMMARY')
print('=' * 40)
print(f"Epic: {results['epic']['key']}")
print(f"Stories: {len(results['stories'])}/{len(STORIES)}")
print(f"Subtasks: {len(results['subtasks'])}")
if results['failed']:
print(f"\nFailed: {len(results['failed'])}")
for f in results['failed']:
print(f" - {f['type']}: {f['summary'][:40]}...")
# Group by category
print('\n--- By Category ---')
by_category = {}
for s in results['stories']:
cat = s['category']
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(s['key'])
for cat, keys in by_category.items():
print(f"{cat}: {', '.join(keys)}")
print('\n' + '=' * 40)
print(' LINKS')
print('=' * 40)
print(f"Epic: {JIRA_BASE_URL}/browse/{results['epic']['key']}")
print(f"Board: {JIRA_BASE_URL}/jira/software/projects/{PROJECT_KEY}/boards/1")
print(f"Backlog: {JIRA_BASE_URL}/jira/software/projects/{PROJECT_KEY}/boards/1/backlog")
print('=' * 40 + '\n')
return results
if __name__ == '__main__':
main()
// Create Two-Level Hierarchy Epics and Stories
// Following jira-safe skill patterns for Next-Gen project
// Source: docs/epics/EPIC-001, EPIC-002, EPIC-003
// Load from environment variables (set by run.js or manually)
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const PROJECT_KEY = process.env.JIRA_PROJECT_KEY || 'SCRUM';
// Validate required env vars
if (!JIRA_EMAIL || !JIRA_API_TOKEN || !JIRA_BASE_URL) {
console.error('Error: Missing required environment variables.');
console.error('Required: JIRA_EMAIL, JIRA_API_TOKEN, JIRA_BASE_URL');
console.error('Set these in .claude/skills/jira/.env or export them manually.');
process.exit(1);
}
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
const headers = {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// ==================== SKILL PATTERNS ====================
// From .claude/skills/jira/jira-safe/SKILL.md
// ADF Helper (Atlassian Document Format)
function buildADF(content) {
return { type: 'doc', version: 1, content };
}
function heading(level, text) {
return {
type: 'heading',
attrs: { level },
content: [{ type: 'text', text }]
};
}
function paragraph(text) {
return {
type: 'paragraph',
content: [{ type: 'text', text }]
};
}
function bulletList(items) {
return {
type: 'bulletList',
content: items.map(item => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: item }] }]
}))
};
}
// Create Issue (from skill pattern)
async function createIssue(fields) {
const response = await fetch(`${JIRA_BASE_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields })
});
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status}: ${error.substring(0, 200)}`);
}
return response.json();
}
function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
// ==================== EPIC & STORY DEFINITIONS ====================
// From docs/epics/EPIC-001-database-migration.md, EPIC-002-backend-api.md, EPIC-003-frontend-ui.md
const epics = [
{
id: 'EPIC-001',
summary: 'EPIC-001: Database Migration - Two-Level Hierarchy',
description: buildADF([
heading(2, 'Business Outcome'),
paragraph('Transform the current single-level database structure (users → brands) into a two-level hierarchy (users → clients → brands) to support agencies managing multiple client companies.'),
heading(2, 'Success Criteria'),
bulletList([
'clients table created with proper schema',
'brands.clientId column added and populated',
'100% of existing brands mapped to clients',
'All foreign key constraints enforced',
'Zero data loss during migration'
]),
heading(2, 'Dependencies'),
paragraph('None - this is the foundational epic')
]),
labels: ['two-level-hierarchy', 'database'],
stories: [
{
id: 'US-001.1',
summary: 'US-001.1: As a database administrator, I want to create a clients table, so that users can manage multiple client companies',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a database administrator, I want to create a new clients table with proper schema, indexes, and constraints, so that users can manage multiple client companies, each with their own brands.'),
heading(2, 'Acceptance Criteria'),
heading(3, 'Scenario: Table Creation'),
bulletList([
'GIVEN the database schema needs updating',
'WHEN the migration runs',
'THEN clients table exists with id, user_id, name, is_default, created_at, updated_at'
]),
heading(2, 'Definition of Done'),
bulletList([
'[ ] Migration file created and tested',
'[ ] Rollback SQL documented',
'[ ] TypeScript types auto-generated',
'[ ] PR merged to master'
])
]),
labels: ['database', 'epic-001'],
subtasks: [
'Define clients table schema in Drizzle ORM',
'Add user_id foreign key with CASCADE delete',
'Create index on user_id for performance',
'Add unique partial index for default client per user',
'Generate migration file',
'Create validation queries',
'Test on Neon branch'
]
},
{
id: 'US-001.2',
summary: 'US-001.2: As a database administrator, I want to add clientId column to brands, so that brands are associated with clients',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a database administrator, I want to add a nullable client_id foreign key column to the brands table, so that each brand can be associated with a client company.'),
heading(2, 'Acceptance Criteria'),
bulletList([
'GIVEN the brands table exists',
'WHEN the migration runs',
'THEN client_id column is added as nullable UUID'
]),
heading(2, 'Definition of Done'),
bulletList([
'[ ] Column added as NULLABLE',
'[ ] Index created on client_id',
'[ ] FK references clients.id with CASCADE',
'[ ] PR merged to master'
])
]),
labels: ['database', 'epic-001'],
subtasks: [
'Add clientId field to brands schema',
'Configure as nullable UUID',
'Add foreign key reference to clients.id',
'Set onDelete: cascade',
'Create index on client_id',
'Generate migration file',
'Test on Neon branch'
]
},
{
id: 'US-001.3',
summary: 'US-001.3: As a database administrator, I want to migrate existing brands to clients, so that all data follows two-level hierarchy',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a database administrator, I want to create a client for each existing brand and link them, so that existing data is migrated to the new two-level hierarchy.'),
heading(2, 'Acceptance Criteria'),
bulletList([
'GIVEN existing brands without clients',
'WHEN migration runs',
'THEN one client created per brand with matching user_id',
'AND each brand.client_id points to its new client'
])
]),
labels: ['database', 'epic-001'],
subtasks: [
'Create data migration script',
'For each brand, create client with same user_id',
'Update brand.client_id to new client.id',
'Verify 0 brands with NULL client_id',
'Verify brand_count = client_count'
]
},
{
id: 'US-001.4',
summary: 'US-001.4: As a database administrator, I want to enforce NOT NULL on clientId, so that data integrity is guaranteed',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a database administrator, I want to make client_id NOT NULL and add final constraints, so that data integrity is enforced at the database level.'),
heading(2, 'Acceptance Criteria'),
bulletList([
'GIVEN all brands have client_id populated',
'WHEN constraint added',
'THEN client_id column is NOT NULL'
])
]),
labels: ['database', 'epic-001'],
subtasks: [
'Verify 0 NULL client_id values',
'Alter column to NOT NULL',
'Add unique partial index for default brand per client',
'Update drizzle schema',
'Generate final migration'
]
}
]
},
{
id: 'EPIC-002',
summary: 'EPIC-002: Backend API - Two-Level Hierarchy',
description: buildADF([
heading(2, 'Business Outcome'),
paragraph('Implement comprehensive backend API support for the two-level hierarchy including client CRUD, updated brand APIs, limit enforcement, and ownership verification.'),
heading(2, 'Success Criteria'),
bulletList([
'Client CRUD endpoints implemented (/api/clients/*)',
'Brand API requires clientId parameter',
'Two-level limit enforcement working',
'All DELETE/PUT have ownership checks'
]),
heading(2, 'Dependencies'),
paragraph('EPIC-001 (Database Migration) must be complete')
]),
labels: ['two-level-hierarchy', 'api'],
stories: [
{
id: 'US-002.1',
summary: 'US-002.1: As a frontend developer, I want client CRUD API endpoints, so that users can manage client companies',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a frontend developer, I want complete CRUD API endpoints for client management, so that users can create, read, update, and delete their client companies.'),
heading(2, 'Endpoints'),
bulletList([
'GET /api/clients - List all clients',
'POST /api/clients - Create with limit check',
'GET /api/clients/[id] - Get single client',
'PUT /api/clients/[id] - Update with ownership check',
'DELETE /api/clients/[id] - Delete with cascade'
])
]),
labels: ['api', 'epic-002'],
subtasks: [
'Create app/api/clients/route.ts (GET, POST)',
'Implement GET - list clients for user',
'Implement POST - create with limit check',
'Create app/api/clients/[id]/route.ts',
'Implement ownership verification helper',
'Add Zod validation schemas',
'Create unit tests'
]
},
{
id: 'US-002.2',
summary: 'US-002.2: As a frontend developer, I want brand API updated for client hierarchy, so that brands are scoped to clients',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a frontend developer, I want brand API updated to require clientId parameter, so that brands are properly scoped to client companies.'),
heading(2, 'Changes'),
bulletList([
'POST /api/brands requires clientId',
'GET /api/clients/[id]/brands returns brands for client',
'PUT/DELETE have ownership verification'
])
]),
labels: ['api', 'epic-002'],
subtasks: [
'Update POST /api/brands to require clientId',
'Create GET /api/clients/[id]/brands endpoint',
'Add ownership verification to DELETE',
'Add ownership verification to PUT',
'Add tests for cross-client access prevention'
]
},
{
id: 'US-002.3',
summary: 'US-002.3: As a billing system, I want two-level limit enforcement, so that subscription tiers are enforced',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a billing system, I want two-level limit enforcement, so that users cannot exceed their client or brand limits.'),
heading(2, 'Limits by Tier'),
bulletList([
'Trial: 1 client, 2 brands per client',
'Pro: 10 clients, 10 brands per client',
'Unlimited: No limits'
])
]),
labels: ['api', 'epic-002'],
subtasks: [
'Create lib/subscription/limits.ts',
'Define limit constants by tier',
'Implement getClientCount(userId)',
'Implement getBrandCount(clientId)',
'Add atomic limit checks'
]
},
{
id: 'US-002.4',
summary: 'US-002.4: As a security system, I want two-level ownership verification, so that users can only access their data',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a security system, I want two-level ownership verification on all endpoints, so that users cannot access other users data.'),
heading(2, 'Requirements'),
bulletList([
'verifyClientOwnership(userId, clientId) helper',
'verifyBrandOwnership(userId, brandId) helper',
'Applied to all client and brand endpoints'
])
]),
labels: ['api', 'security', 'epic-002'],
subtasks: [
'Create lib/auth/ownership.ts',
'Implement verifyClientOwnership',
'Implement verifyBrandOwnership',
'Apply to all endpoints',
'Create security test suite'
]
},
{
id: 'US-002.5',
summary: 'US-002.5: As an AI system, I want context builder updated, so that client info is included in AI prompts',
description: buildADF([
heading(2, 'User Story'),
paragraph('As an AI system, I want brand context builder to include client information, so that AI responses are aware of the client company context.')
]),
labels: ['api', 'ai', 'epic-002'],
subtasks: [
'Update lib/ai/context-builder.ts',
'Add client name to context',
'Test context generation',
'Verify backward compatibility'
]
}
]
},
{
id: 'EPIC-003',
summary: 'EPIC-003: Frontend UI - Two-Level Hierarchy',
description: buildADF([
heading(2, 'Business Outcome'),
paragraph('Implement complete frontend UI support for the two-level hierarchy including client management pages, two-level selector, updated onboarding, and subscription limit displays.'),
heading(2, 'Success Criteria'),
bulletList([
'Client management page at /dashboard/clients',
'Two-level selector in dashboard header',
'Onboarding Step 0 creates client',
'Responsive design (mobile/tablet/desktop)'
]),
heading(2, 'Dependencies'),
paragraph('EPIC-002 (Backend API) must be complete')
]),
labels: ['two-level-hierarchy', 'frontend'],
stories: [
{
id: 'US-003.1',
summary: 'US-003.1: As a user, I want a client management page, so that I can create and manage client companies',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a user, I want to manage my client companies from a dedicated page, so that I can create, view, edit, and delete clients easily.'),
heading(2, 'Features'),
bulletList([
'Page at /dashboard/clients',
'List all clients in card format',
'Show brand count per client',
'Create/Edit/Delete functionality'
])
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Create app/dashboard/clients/page.tsx',
'Add to dashboard navigation',
'Implement client list component',
'Create New Client modal',
'Add delete confirmation dialog',
'Handle loading/error/empty states'
]
},
{
id: 'US-003.2',
summary: 'US-003.2: As a user, I want a two-level selector, so that I can switch between clients and brands',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a user, I want to use a two-level selector to switch clients and brands, so that I can quickly navigate between different client companies.'),
heading(2, 'Features'),
bulletList([
'Client dropdown (top level)',
'Brand dropdown (filtered by client)',
'Selection persists across navigation'
])
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Create client-brand-selector.tsx',
'Implement client dropdown',
'Implement brand dropdown filtered by client',
'Persist selection to localStorage',
'Sync with URL params'
]
},
{
id: 'US-003.3',
summary: 'US-003.3: As a new user, I want onboarding to create a client first, so that brands are properly organized',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a new user, I want to create a client company during onboarding, so that my first brand is properly associated with a client.'),
heading(2, 'Flow'),
bulletList([
'Step 0: What is your company name? (creates client)',
'Steps 1-6: Existing brand questionnaire'
])
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Add Step 0 to onboarding flow',
'Create client name input form',
'Call POST /api/clients on Step 0',
'Pass clientId to brand creation'
]
},
{
id: 'US-003.4',
summary: 'US-003.4: As a user, I want a client profile page, so that I can view and edit client details',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a user, I want to view and edit client details on a dedicated page.')
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Create app/dashboard/clients/[id]/page.tsx',
'Display client details',
'Add edit functionality',
'Show brands list'
]
},
{
id: 'US-003.5',
summary: 'US-003.5: As a user, I want brand management scoped to client, so that brands are organized by client',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a user, I want to see brands filtered by selected client, so that I only see relevant brands.')
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Update brands page to filter by client',
'Pass clientId to brand creation',
'Show default brand badge per client'
]
},
{
id: 'US-003.6',
summary: 'US-003.6: As a user, I want to see subscription limits, so that I know when to upgrade',
description: buildADF([
heading(2, 'User Story'),
paragraph('As a user, I want to see my current usage vs subscription limits, so that I know when I am approaching limits.')
]),
labels: ['frontend', 'epic-003'],
subtasks: [
'Create usage-display.tsx component',
'Show client count vs limit',
'Show brand count vs limit',
'Add Upgrade prompt'
]
}
]
}
];
// ==================== MAIN ====================
async function main() {
console.log('========================================');
console.log(' CREATE TWO-LEVEL HIERARCHY BACKLOG');
console.log(' Following jira-safe skill patterns');
console.log('========================================\n');
const results = {
epics: { created: 0, failed: 0, keys: [] },
stories: { created: 0, failed: 0, keys: [] },
subtasks: { created: 0, failed: 0 }
};
for (const epic of epics) {
console.log(`\n--- ${epic.id} ---`);
// Create Epic (Next-Gen: no customfield_10011)
try {
const epicIssue = await createIssue({
project: { key: PROJECT_KEY },
issuetype: { name: 'Epic' },
summary: epic.summary,
description: epic.description,
labels: epic.labels
});
console.log(`+ Epic: ${epicIssue.key}`);
results.epics.created++;
results.epics.keys.push({ key: epicIssue.key, id: epic.id });
await delay(150);
// Create Stories under Epic (Next-Gen: use parent field)
for (const story of epic.stories) {
try {
const storyIssue = await createIssue({
project: { key: PROJECT_KEY },
issuetype: { name: 'Story' },
summary: story.summary,
description: story.description,
parent: { key: epicIssue.key }, // Next-Gen pattern from skill
labels: story.labels
});
console.log(` + Story: ${storyIssue.key} (${story.id})`);
results.stories.created++;
results.stories.keys.push({ key: storyIssue.key, id: story.id, epicKey: epicIssue.key });
await delay(100);
// Create Subtasks (Next-Gen: use 'Subtask' not 'Sub-task')
if (story.subtasks && story.subtasks.length > 0) {
for (const subtaskSummary of story.subtasks) {
try {
const subtaskIssue = await createIssue({
project: { key: PROJECT_KEY },
issuetype: { name: 'Subtask' }, // Next-Gen pattern from skill
summary: subtaskSummary,
parent: { key: storyIssue.key }
});
console.log(` + Subtask: ${subtaskIssue.key}`);
results.subtasks.created++;
} catch (err) {
console.log(` - FAILED: ${err.message.substring(0, 50)}`);
results.subtasks.failed++;
}
await delay(50);
}
}
} catch (err) {
console.log(` - FAILED ${story.id}: ${err.message.substring(0, 80)}`);
results.stories.failed++;
}
}
} catch (err) {
console.log(`- FAILED ${epic.id}: ${err.message.substring(0, 80)}`);
results.epics.failed++;
}
}
console.log('\n========================================');
console.log(' SUMMARY');
console.log('========================================');
console.log(`Epics: ${results.epics.created} created, ${results.epics.failed} failed`);
console.log(`Stories: ${results.stories.created} created, ${results.stories.failed} failed`);
console.log(`Subtasks: ${results.subtasks.created} created, ${results.subtasks.failed} failed`);
console.log('\n--- Created Issues ---');
for (const epic of results.epics.keys) {
console.log(`${epic.key}: ${epic.id}`);
for (const story of results.stories.keys.filter(s => s.epicKey === epic.key)) {
console.log(` └─ ${story.key}: ${story.id}`);
}
}
console.log(`\n========================================`);
console.log(`View: ${JIRA_BASE_URL}/jira/software/projects/${PROJECT_KEY}/boards/1/backlog`);
console.log('========================================');
}
main().catch(console.error);
Related skills
FAQ
Is Jira Safe safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.