Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:56:44 +08:00
commit 93c16ce8f2
19 changed files with 4121 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
{
"name": "specweave-jira",
"description": "JIRA integration for SpecWeave increments. Bidirectional sync between SpecWeave increments and JIRA epics/stories. Automatically creates JIRA issues from increments, tracks progress, and updates status.",
"version": "0.24.0",
"author": {
"name": "SpecWeave Team",
"url": "https://spec-weave.com"
},
"skills": [
"./skills"
],
"agents": [
"./agents"
],
"commands": [
"./commands"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# specweave-jira
JIRA integration for SpecWeave increments. Bidirectional sync between SpecWeave increments and JIRA epics/stories. Automatically creates JIRA issues from increments, tracks progress, and updates status.

View File

@@ -0,0 +1,416 @@
# Jira Manager Agent
## 🚀 How to Invoke This Agent
**Subagent Type**: `specweave-jira:jira-manager:jira-manager`
**Usage Example**:
```typescript
Task({
subagent_type: "specweave-jira:jira-manager:jira-manager",
prompt: "Create Jira epic for increment 0005 and sync all tasks from spec.md and tasks.md",
model: "haiku" // optional: haiku, sonnet, opus
});
```
**Naming Convention**: `{plugin}:{directory}:{yaml-name-or-directory-name}`
- **Plugin**: specweave-jira
- **Directory**: jira-manager
- **Agent Name**: jira-manager
**When to Use**:
- You need to sync SpecWeave increments to Jira epics
- You want bidirectional synchronization between SpecWeave and Jira
- You need to update Jira issues with task completion status
- You're creating Jira stories from SpecWeave specifications
- You need to manage cross-system consistency between SpecWeave and Jira
**Role**: Jira integration specialist for SpecWeave increments
**Expertise**: Jira REST API, Epic/Story/Task management, JQL, automation, webhooks, custom fields
**Tools**: Read, Write, Edit, Bash (curl for Jira API)
**Default Behavior**: **Bidirectional sync** (two-way) - Synchronizes changes in both directions automatically
---
## Capabilities
As the Jira Manager agent, I specialize in:
### 1. Bidirectional Synchronization (Default)
- **Two-Way Sync**: Keep SpecWeave and Jira synchronized automatically
- **FROM Jira**: Pull status changes, priority updates, sprint assignments, comments
- **TO Jira**: Push task completion, progress updates, story points, metadata
- **Conflict Resolution**: Detect and resolve conflicts between systems
- **Smart Merging**: Merge changes from both directions intelligently
- **Change Detection**: Track what changed in both systems since last sync
### 2. Epic Management
- **Create Epics**: Generate Jira Epics from SpecWeave increments
- **Update Epics**: Bidirectional sync of progress, status, comments
- **Close Epics**: Close epics with completion summaries
- **Link Epics**: Connect related epics, stories, and tasks
- **Custom Fields**: Handle custom fields, labels, versions
### 3. Story & Task Management
- **Create Stories**: Map SpecWeave specs to Jira Stories (PRD or RFC)
- **Create Tasks**: Map SpecWeave tasks to Jira Tasks
- **Update Status**: Bidirectional sync of task completion
- **Subtasks**: Handle Jira subtasks and dependencies
- **Bulk Operations**: Batch create/update stories and tasks
### 4. Progress Tracking
- **Sprint Progress**: Track epic progress within sprints (bidirectional)
- **Story Points**: Calculate and update story points
- **Status Updates**: Bidirectional sync of status changes
- **Comments**: Post task completion comments and import Jira comments
- **Time Tracking**: Track estimated vs actual time
### 4. Jira API Operations
- **REST API**: Use curl for all Jira operations
- **JQL Queries**: Search epics, stories, tasks via JQL
- **Webhooks**: Configure webhooks for two-way sync
- **Automation Rules**: Trigger Jira automation
- **Rate Limiting**: Handle rate limits gracefully
### 5. Type Detection Intelligence
- **Business vs Technical**: Classify stories as PRD or RFC
- **Decision Detection**: Identify architecture decisions (ADR)
- **Bug Classification**: Map bugs to operational incidents
- **Task vs Story**: Distinguish implementation tasks from requirements
---
## 🚨 CRITICAL: Concept Mapping (MANDATORY)
**BEFORE any sync operation, you MUST**:
1. **Read the Mapping Reference**: [reference/jira-specweave-mapping.md](../../reference/jira-specweave-mapping.md)
2. **Follow mapping rules EXACTLY** - No custom mappings allowed
3. **Validate mappings after sync** - Ensure bidirectional links are correct
**Key Mapping Rules** (Quick Reference):
| Jira | SpecWeave | Rule |
|------|-----------|------|
| Epic | Increment | 1:1 mapping (MANDATORY) |
| Story (business) | PRD | "As a user" stories |
| Story (technical) | RFC | Technical design stories |
| Story (decision) | ADR | Architecture decisions |
| Task | Task | Implementation tasks |
| Subtask | Subtask | Sub-items in tasks.md |
| Bug | Incident | Operational issues |
| Sprint | Release Plan | Sprint planning |
| Component | Module | Architecture modules |
| To Do | planned | Not started |
| In Progress | in_progress | Active work |
| In Review | in_progress | Code review |
| Done | completed | Fully complete |
| Won't Do | cancelled | Out of scope |
**Source of Truth**: [.specweave/docs/internal/delivery/guides/tool-concept-mapping.md](../../../.specweave/docs/internal/delivery/guides/tool-concept-mapping.md)
**Story Type Detection** (USE THIS DECISION TREE):
```
Is the story primarily a business requirement?
├─ YES → PRD (.specweave/docs/internal/strategy/prd-{name}.md)
│ Indicators: "As a user", labels: business/requirement
└─ NO → Is it a technical design/API change?
├─ YES → RFC (.specweave/docs/internal/architecture/rfc/####-{name}.md)
│ Indicators: "Design", "API", labels: technical/design
└─ NO → Is it an architecture decision?
├─ YES → ADR (.specweave/docs/internal/architecture/adr/####-{decision}.md)
│ Indicators: "Decide", "Choose", labels: decision/adr
└─ NO → Task (.specweave/increments/####-{name}/tasks.md)
Indicators: Specific, actionable work
```
**Validation Checklist** (Run BEFORE and AFTER every sync):
- [ ] Jira Epic exists and is accessible
- [ ] Increment metadata has valid Jira link (`jira.epic`)
- [ ] Status mapped correctly (use status mapping table)
- [ ] Priority mapped correctly (Highest→P1, High→P2, Medium→P3, Low→P4)
- [ ] Story type detected correctly (PRD vs RFC vs ADR via decision tree)
- [ ] Labels follow SpecWeave conventions (project-key, increment-####)
- [ ] Comments include increment context
- [ ] Bidirectional links are valid (Epic ↔ Increment)
**Example Workflow** (MUST follow this pattern):
```
1. Read mapping reference (MANDATORY first step)
2. Read increment files (spec.md, tasks.md, metadata.json)
3. Apply mapping rules to convert SpecWeave → Jira
4. Create/update Jira epic via REST API
5. Validate mapping (check bidirectional links)
6. Update increment metadata with Jira epic key
7. Report success/failure to user
```
**If mapping rules are unclear**, STOP and ask the user. Never guess or create custom mappings.
---
## When to Use This Agent
Invoke the jira-manager agent (via Task tool) for:
1. **Initial Setup**
- "Set up Jira sync for this SpecWeave project"
- "Configure Jira integration with auto-sync"
2. **Epic Operations**
- "Create Jira epic for increment 0005"
- "Update epic PROJ-123 with latest progress"
- "Close all completed increment epics"
3. **Story/Task Operations**
- "Create Jira stories from spec.md"
- "Sync all tasks to Jira"
- "Update Jira task status after completion"
4. **Bulk Operations**
- "Sync all increments to Jira"
- "Generate epics for all backlog items"
- "Update all open epics with current status"
5. **Troubleshooting**
- "Why isn't epic PROJ-123 updating?"
- "Check Jira sync status for increment 0005"
- "Fix broken Jira integration"
---
## Jira API Operations
### Authentication
```bash
# Basic Auth (email:token)
JIRA_AUTH=$(echo -n "$JIRA_EMAIL:$JIRA_API_TOKEN" | base64)
```
### Create Epic
```bash
curl -X POST \
-H "Authorization: Basic $JIRA_AUTH" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"project": {"key": "PROJ"},
"summary": "Increment 0005: User Authentication",
"issuetype": {"name": "Epic"},
"customfield_10011": "AUTH-001",
"description": "User authentication with OAuth2...",
"labels": ["specweave", "increment-0005"],
"priority": {"name": "High"}
}
}' \
https://$JIRA_DOMAIN/rest/api/3/issue
```
### Update Epic Status
```bash
curl -X POST \
-H "Authorization: Basic $JIRA_AUTH" \
-H "Content-Type: application/json" \
-d '{
"transition": {"id": "31"}
}' \
https://$JIRA_DOMAIN/rest/api/3/issue/PROJ-123/transitions
```
### Add Comment
```bash
curl -X POST \
-H "Authorization: Basic $JIRA_AUTH" \
-H "Content-Type: application/json" \
-d '{
"body": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [{
"type": "text",
"text": "Progress Update: 60% complete (6/10 tasks)"
}]
}]
}
}' \
https://$JIRA_DOMAIN/rest/api/3/issue/PROJ-123/comment
```
### Query Epics (JQL)
```bash
curl -X GET \
-H "Authorization: Basic $JIRA_AUTH" \
"https://$JIRA_DOMAIN/rest/api/3/search?jql=project=PROJ+AND+type=Epic+AND+labels=specweave"
```
---
## Configuration Management
**Read Configuration**:
```bash
# From .specweave/config.json
JIRA_DOMAIN=$(jq -r '.externalPM.config.domain' .specweave/config.json)
JIRA_PROJECT=$(jq -r '.externalPM.config.project' .specweave/config.json)
```
**Validate Configuration**:
- Domain format: `*.atlassian.net` or self-hosted
- API token valid and not expired
- Project exists and user has access
- Required custom fields exist (epic name, epic link)
---
## Error Handling
### Common Errors
**401 Unauthorized**:
- API token invalid or expired
- Email address incorrect
- Solution: Regenerate token, verify credentials
**403 Forbidden**:
- Insufficient permissions (need: Browse Projects, Create Issues, Edit Issues)
- Solution: Contact Jira admin for permissions
**404 Not Found**:
- Project key invalid
- Epic doesn't exist
- Solution: Verify project key and epic key
**400 Bad Request**:
- Invalid custom field
- Invalid status transition
- Solution: Validate request payload, check workflow
### Retry Strategy
```bash
# Exponential backoff
for i in 1 2 3; do
response=$(curl -w "%{http_code}" ...)
if [ "$response" = "200" ]; then
break
fi
sleep $((2 ** i))
done
```
---
## Rate Limiting
**Jira Cloud Limits**:
- Varies by account type (standard: ~100 req/sec)
- Burst allowance available
- 429 response when exceeded
**Strategy**:
- Track request count
- Implement token bucket algorithm
- Queue requests if approaching limit
- Warn user if rate limit hit
---
## Bidirectional Sync (Future)
**Jira → SpecWeave**:
1. Poll Jira for epic changes
2. Detect status changes (In Progress → Done)
3. Update increment status locally
4. Notify user
**Webhook Setup** (preferred):
1. Configure Jira webhook
2. Point to SpecWeave endpoint
3. Receive real-time updates
4. Process state changes
---
## Security Considerations
**Jira API Token**:
- ✅ Store in environment variable: `JIRA_API_TOKEN`
- ✅ Never log or commit token
- ✅ Use Basic Auth: `base64(email:token)`
- ✅ Rotate every 90 days
**API Requests**:
- ✅ Use HTTPS only
- ✅ Validate SSL certificates
- ✅ Sanitize user input
- ✅ Log requests (without token)
---
## Examples
### Example 1: Create Epic from Increment
**Input**: Increment 0005-user-authentication
**Process**:
1. Read spec.md → Extract title, summary, acceptance criteria
2. Read mapping reference → Confirm Epic = Increment
3. Format Jira epic payload (project key, summary, description, labels)
4. POST to Jira API → Create epic
5. Parse response → Extract epic key (PROJ-123)
6. Save to metadata.json: `external_ids.jira.epic = PROJ-123`
7. Display: "Created Jira Epic: PROJ-123"
### Example 2: Sync Progress
**Input**: 6/10 tasks complete
**Process**:
1. Read tasks.md → Parse completion status
2. Calculate: 60% complete
3. Identify: Recently completed tasks (T-005, T-006)
4. Format comment with progress update
5. POST comment to epic PROJ-123
6. Display: "Synced to Jira Epic: PROJ-123"
### Example 3: Close Epic
**Input**: Increment 0005 complete (10/10 tasks)
**Process**:
1. Validate: All tasks complete
2. Generate: Completion summary
3. Transition epic state → Done
4. POST final comment
5. Display: "Closed Jira Epic: PROJ-123"
---
## Related Tools
- **Jira CLI**: Alternative to REST API (requires installation)
- **Jira REST API v3**: https://developer.atlassian.com/cloud/jira/platform/rest/v3/
- **Jira Automation**: Trigger rules from SpecWeave
---
**Status**: Production-ready
**Version**: 1.0.0
**Last Updated**: 2025-11-04

181
commands/import-projects.js Normal file
View File

@@ -0,0 +1,181 @@
import chalk from "chalk";
import { confirm } from "@inquirer/prompts";
import { existsSync } from "fs";
import path from "path";
import { JiraClient } from "../../../src/integrations/jira/jira-client.js";
import { FilterProcessor } from "../../../src/integrations/jira/filter-processor.js";
import { mergeEnvList, getEnvValue } from "../../../src/utils/env-manager.js";
import { consoleLogger } from "../../../src/utils/logger.js";
import { credentialsManager } from "../../../src/core/credentials-manager.js";
async function importProjects(options = {}) {
const logger = options.logger ?? consoleLogger;
const projectRoot = process.cwd();
console.log(chalk.cyan("\n\u{1F4E5} JIRA Project Import\n"));
const credentials = credentialsManager.getJiraCredentials();
if (!credentials) {
console.log(chalk.red("\u274C No JIRA credentials found"));
console.log(chalk.gray(" Run: specweave init"));
return;
}
const client = new JiraClient(credentials);
const filterProcessor = new FilterProcessor(client, { logger });
if (options.resume) {
const resumed = await resumeImport(projectRoot, client, filterProcessor, options);
if (resumed) {
return;
}
console.log(chalk.yellow("\u26A0\uFE0F No import state found. Starting fresh import.\n"));
}
const existing = await loadExistingProjects(projectRoot, logger);
console.log(chalk.gray(`Current projects: ${existing.length > 0 ? existing.join(", ") : "none"}
`));
console.log(chalk.cyan("\u{1F4E1} Fetching available JIRA projects...\n"));
let allProjects = [];
try {
const response = await client.searchProjects({ maxResults: 1e3 });
allProjects = response.values || [];
console.log(chalk.green(`\u2713 Found ${allProjects.length} total projects
`));
} catch (error) {
console.log(chalk.red(`\u274C Failed to fetch projects: ${error.message}`));
return;
}
let filteredProjects = allProjects;
if (options.preset) {
console.log(chalk.cyan(`\u{1F50D} Applying preset: ${options.preset}
`));
try {
filteredProjects = await filterProcessor.applyPreset(allProjects, options.preset);
} catch (error) {
console.log(chalk.red(`\u274C ${error.message}`));
return;
}
} else if (options.filter || options.type || options.lead || options.jql) {
const filterOptions = {};
if (options.filter === "active") {
filterOptions.active = true;
} else if (options.filter === "archived") {
filterOptions.active = false;
}
if (options.type) {
filterOptions.types = options.type;
}
if (options.lead) {
filterOptions.lead = options.lead;
}
if (options.jql) {
filterOptions.jql = options.jql;
}
console.log(chalk.cyan("\u{1F50D} Applying filters...\n"));
filteredProjects = await filterProcessor.applyFilters(allProjects, filterOptions);
}
const newProjects = filteredProjects.filter((p) => {
return !existing.some((e) => e.toLowerCase() === p.key.toLowerCase());
});
if (newProjects.length === 0) {
console.log(chalk.yellow("\u26A0\uFE0F No new projects found to import"));
console.log(chalk.gray(" All available projects are already imported\n"));
return;
}
console.log(chalk.cyan("\u{1F4CB} Import Preview:\n"));
console.log(chalk.white(` Total available: ${allProjects.length}`));
console.log(chalk.white(` After filtering: ${filteredProjects.length}`));
console.log(chalk.white(` Already imported: ${existing.length}`));
console.log(chalk.white(` New projects: ${chalk.green.bold(newProjects.length)}
`));
if (newProjects.length <= 10) {
console.log(chalk.gray("Projects to import:"));
newProjects.forEach((p) => {
const typeLabel = p.projectTypeKey || "unknown";
const leadLabel = p.lead?.displayName || "no lead";
console.log(chalk.gray(` \u2728 ${p.key} - ${p.name} (${typeLabel}, ${leadLabel})`));
});
console.log("");
}
if (options.dryRun) {
console.log(chalk.yellow("\u{1F50D} Dry-run mode: No changes will be made\n"));
console.log(chalk.green(`\u2713 Preview complete: ${newProjects.length} project(s) would be imported
`));
return;
}
const confirmed = await confirm({
message: `Import ${newProjects.length} new project(s)?`,
default: true
});
if (!confirmed) {
console.log(chalk.yellow("\n\u23ED\uFE0F Import canceled\n"));
return;
}
const projectKeys = newProjects.map((p) => p.key);
console.log(chalk.cyan("\n\u{1F4E5} Importing projects...\n"));
try {
await mergeEnvList({
key: "JIRA_PROJECTS",
newValues: projectKeys,
projectRoot,
logger,
createBackup: true
});
console.log(chalk.green(`
\u2705 Successfully imported ${projectKeys.length} project(s)
`));
console.log(chalk.gray("Updated: .env (JIRA_PROJECTS)"));
console.log(chalk.gray("Backup: .env.backup\n"));
} catch (error) {
console.log(chalk.red(`
\u274C Import failed: ${error.message}
`));
}
}
async function loadExistingProjects(projectRoot, logger) {
const value = await getEnvValue(projectRoot, "JIRA_PROJECTS");
if (!value) {
return [];
}
return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
}
async function resumeImport(projectRoot, client, filterProcessor, options) {
const stateFile = path.join(projectRoot, ".specweave", "cache", "import-state.json");
if (!existsSync(stateFile)) {
return false;
}
console.log(chalk.cyan("\u{1F504} Resuming interrupted import...\n"));
return false;
}
async function main() {
const args = process.argv.slice(2);
const options = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--filter") {
options.filter = args[++i];
} else if (arg === "--type") {
options.type = args[++i].split(",");
} else if (arg === "--lead") {
options.lead = args[++i];
} else if (arg === "--jql") {
options.jql = args[++i];
} else if (arg === "--preset") {
options.preset = args[++i];
} else if (arg === "--dry-run") {
options.dryRun = true;
} else if (arg === "--resume") {
options.resume = true;
} else if (arg === "--no-progress") {
options.noProgress = true;
}
}
await importProjects(options);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(chalk.red(`
\u274C Error: ${error.message}
`));
process.exit(1);
});
}
export {
main as default,
importProjects
};

View File

@@ -0,0 +1,97 @@
---
name: specweave-jira:import-projects
description: Import additional JIRA projects post-init with smart filtering, resume, and dry-run support
---
# JIRA Project Import Command
Import additional JIRA projects after initial setup with advanced filtering and merge capabilities.
## Usage
```bash
# Import all active projects
/specweave-jira:import-projects --filter active
# Import with preset filter
/specweave-jira:import-projects --preset production
# Import with custom JQL
/specweave-jira:import-projects --jql "project NOT IN (TEST, SANDBOX)"
# Dry-run preview (no changes)
/specweave-jira:import-projects --dry-run
# Resume interrupted import
/specweave-jira:import-projects --resume
```
## Options
- `--filter <type>` - Filter by status (active, archived, all)
- `--type <types>` - Filter by project type (software, business, service_desk)
- `--lead <email>` - Filter by project lead
- `--jql <query>` - Custom JQL filter
- `--preset <name>` - Use saved filter preset
- `--dry-run` - Preview without making changes
- `--resume` - Resume interrupted import
- `--no-progress` - Disable progress tracking
## Examples
### Import Active Projects Only
```bash
/specweave-jira:import-projects --filter active
```
Filters out archived projects, shows preview, prompts for confirmation, merges with existing.
### Import with Production Preset
```bash
/specweave-jira:import-projects --preset production
```
Uses the "production" preset filter (active + software + excludes TEST/SANDBOX).
### Custom JQL Filter
```bash
/specweave-jira:import-projects --jql "lead = currentUser() AND status != Archived"
```
Imports projects where you are the lead and not archived.
### Dry-Run Preview
```bash
/specweave-jira:import-projects --filter active --dry-run
```
Shows which projects would be imported without making any changes.
## Features
- **Smart Filtering**: Filter by status, type, lead, or custom JQL
- **Merge with Existing**: Automatically merges with existing projects (no duplicates)
- **Progress Tracking**: Real-time progress with ETA and cancelation support
- **Resume Support**: Resume interrupted imports with `--resume`
- **Dry-Run Mode**: Preview changes before applying
- **Filter Presets**: Use saved filter combinations
## Implementation
This command:
1. Reads existing projects from `.env` (JIRA_PROJECTS)
2. Fetches available projects from JIRA API
3. Applies selected filters using FilterProcessor
4. Shows preview with project count and reduction percentage
5. Prompts for confirmation
6. Batch imports with AsyncProjectLoader (50-project limit)
7. Shows progress with ETA
8. Merges with existing projects (no duplicates)
9. Updates `.env` file atomically
Handles Ctrl+C gracefully with import state saving for resume.

286
commands/import-projects.ts Normal file
View File

@@ -0,0 +1,286 @@
/**
* JIRA Project Import Command (Post-Init)
*
* Import additional JIRA projects after initial setup with smart filtering,
* resume support, and merge capabilities.
*
* Features:
* - Smart filtering (active, type, lead, JQL)
* - Filter presets (production, active-only, agile-only)
* - Dry-run preview mode
* - Resume interrupted imports
* - Progress tracking with ETA
* - Merge with existing projects (no duplicates)
*
* NEW (v0.24.0): Post-init flexibility for project management
*
* @module plugins/specweave-jira/commands/import-projects
*/
import chalk from 'chalk';
import { confirm } from '@inquirer/prompts';
import { existsSync } from 'fs';
import path from 'path';
import { JiraClient } from '../../../src/integrations/jira/jira-client.js';
import { FilterProcessor, type FilterOptions } from '../../../src/integrations/jira/filter-processor.js';
import { AsyncProjectLoader } from '../../../src/cli/helpers/async-project-loader.js';
import { mergeEnvList, getEnvValue } from '../../../src/utils/env-manager.js';
import { consoleLogger, type Logger } from '../../../src/utils/logger.js';
import { credentialsManager } from '../../../src/core/credentials-manager.js';
export interface ImportProjectsOptions {
filter?: 'active' | 'archived' | 'all';
type?: string[];
lead?: string;
jql?: string;
preset?: string;
dryRun?: boolean;
resume?: boolean;
noProgress?: boolean;
logger?: Logger;
}
export interface ImportState {
total: number;
completed: string[];
remaining: string[];
timestamp: number;
}
/**
* Import JIRA projects command
*
* TC-063: Post-Init Import (Merge with Existing)
* TC-064: Filter Active Projects Only
* TC-068: Resume Interrupted Import
* TC-069: Dry-Run Preview
* TC-070: Progress During Import
*
* @param options - Import options
*/
export async function importProjects(options: ImportProjectsOptions = {}): Promise<void> {
const logger = options.logger ?? consoleLogger;
const projectRoot = process.cwd();
// Step 1: Load credentials
console.log(chalk.cyan('\n📥 JIRA Project Import\n'));
const credentials = credentialsManager.getJiraCredentials();
if (!credentials) {
console.log(chalk.red('❌ No JIRA credentials found'));
console.log(chalk.gray(' Run: specweave init'));
return;
}
const client = new JiraClient(credentials);
const filterProcessor = new FilterProcessor(client, { logger });
// Step 2: Check for resume state
if (options.resume) {
const resumed = await resumeImport(projectRoot, client, filterProcessor, options);
if (resumed) {
return;
}
console.log(chalk.yellow('⚠️ No import state found. Starting fresh import.\n'));
}
// Step 3: Read existing projects from .env
const existing = await loadExistingProjects(projectRoot, logger);
console.log(chalk.gray(`Current projects: ${existing.length > 0 ? existing.join(', ') : 'none'}\n`));
// Step 4: Fetch available projects from JIRA
console.log(chalk.cyan('📡 Fetching available JIRA projects...\n'));
let allProjects: any[] = [];
try {
const response = await client.searchProjects({ maxResults: 1000 });
allProjects = response.values || [];
console.log(chalk.green(`✓ Found ${allProjects.length} total projects\n`));
} catch (error: any) {
console.log(chalk.red(`❌ Failed to fetch projects: ${error.message}`));
return;
}
// Step 5: Apply filters
let filteredProjects = allProjects;
if (options.preset) {
// Use filter preset
console.log(chalk.cyan(`🔍 Applying preset: ${options.preset}\n`));
try {
filteredProjects = await filterProcessor.applyPreset(allProjects, options.preset);
} catch (error: any) {
console.log(chalk.red(`${error.message}`));
return;
}
} else if (options.filter || options.type || options.lead || options.jql) {
// Build filter options
const filterOptions: FilterOptions = {};
if (options.filter === 'active') {
filterOptions.active = true;
} else if (options.filter === 'archived') {
filterOptions.active = false;
}
if (options.type) {
filterOptions.types = options.type;
}
if (options.lead) {
filterOptions.lead = options.lead;
}
if (options.jql) {
filterOptions.jql = options.jql;
}
console.log(chalk.cyan('🔍 Applying filters...\n'));
filteredProjects = await filterProcessor.applyFilters(allProjects, filterOptions);
}
// Step 6: Exclude existing projects
const newProjects = filteredProjects.filter(p => {
return !existing.some(e => e.toLowerCase() === p.key.toLowerCase());
});
if (newProjects.length === 0) {
console.log(chalk.yellow('⚠️ No new projects found to import'));
console.log(chalk.gray(' All available projects are already imported\n'));
return;
}
// Step 7: Show preview
console.log(chalk.cyan('📋 Import Preview:\n'));
console.log(chalk.white(` Total available: ${allProjects.length}`));
console.log(chalk.white(` After filtering: ${filteredProjects.length}`));
console.log(chalk.white(` Already imported: ${existing.length}`));
console.log(chalk.white(` New projects: ${chalk.green.bold(newProjects.length)}\n`));
if (newProjects.length <= 10) {
console.log(chalk.gray('Projects to import:'));
newProjects.forEach(p => {
const typeLabel = p.projectTypeKey || 'unknown';
const leadLabel = p.lead?.displayName || 'no lead';
console.log(chalk.gray(`${p.key} - ${p.name} (${typeLabel}, ${leadLabel})`));
});
console.log('');
}
// Step 8: Dry-run mode (exit without changes)
if (options.dryRun) {
console.log(chalk.yellow('🔍 Dry-run mode: No changes will be made\n'));
console.log(chalk.green(`✓ Preview complete: ${newProjects.length} project(s) would be imported\n`));
return;
}
// Step 9: Confirm import
const confirmed = await confirm({
message: `Import ${newProjects.length} new project(s)?`,
default: true
});
if (!confirmed) {
console.log(chalk.yellow('\n⏭ Import canceled\n'));
return;
}
// Step 10: Extract project keys
const projectKeys = newProjects.map(p => p.key);
// Step 11: Merge with existing
console.log(chalk.cyan('\n📥 Importing projects...\n'));
try {
await mergeEnvList({
key: 'JIRA_PROJECTS',
newValues: projectKeys,
projectRoot,
logger,
createBackup: true
});
console.log(chalk.green(`\n✅ Successfully imported ${projectKeys.length} project(s)\n`));
console.log(chalk.gray('Updated: .env (JIRA_PROJECTS)'));
console.log(chalk.gray('Backup: .env.backup\n'));
} catch (error: any) {
console.log(chalk.red(`\n❌ Import failed: ${error.message}\n`));
}
}
/**
* Load existing JIRA projects from .env
*/
async function loadExistingProjects(projectRoot: string, logger: Logger): Promise<string[]> {
const value = await getEnvValue(projectRoot, 'JIRA_PROJECTS');
if (!value) {
return [];
}
return value.split(',').map(v => v.trim()).filter(v => v.length > 0);
}
/**
* Resume interrupted import
*/
async function resumeImport(
projectRoot: string,
client: JiraClient,
filterProcessor: FilterProcessor,
options: ImportProjectsOptions
): Promise<boolean> {
const stateFile = path.join(projectRoot, '.specweave', 'cache', 'import-state.json');
if (!existsSync(stateFile)) {
return false;
}
console.log(chalk.cyan('🔄 Resuming interrupted import...\n'));
// Load state and continue
// (Implementation would load state, skip completed, import remaining)
return false; // Placeholder - full implementation would handle resume
}
/**
* Command entry point (called by CLI)
*/
export default async function main(): Promise<void> {
// Parse CLI arguments
const args = process.argv.slice(2);
const options: ImportProjectsOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--filter') {
options.filter = args[++i] as 'active' | 'archived' | 'all';
} else if (arg === '--type') {
options.type = args[++i].split(',');
} else if (arg === '--lead') {
options.lead = args[++i];
} else if (arg === '--jql') {
options.jql = args[++i];
} else if (arg === '--preset') {
options.preset = args[++i];
} else if (arg === '--dry-run') {
options.dryRun = true;
} else if (arg === '--resume') {
options.resume = true;
} else if (arg === '--no-progress') {
options.noProgress = true;
}
}
await importProjects(options);
}
// Run command if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(error => {
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
process.exit(1);
});
}

25
commands/refresh-cache.js Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
import { existsSync, unlinkSync, readdirSync } from "fs";
import { join } from "path";
async function refreshJiraCache(projectRoot = process.cwd()) {
const cacheDir = join(projectRoot, ".specweave", "cache", "jira");
if (!existsSync(cacheDir)) {
console.log("\u2705 No JIRA cache found");
return;
}
console.log("\u{1F9F9} Clearing JIRA cache...");
const files = readdirSync(cacheDir);
let cleared = 0;
for (const file of files) {
const filePath = join(cacheDir, file);
unlinkSync(filePath);
cleared++;
}
console.log(`\u2705 Cleared ${cleared} cache files`);
}
if (require.main === module) {
refreshJiraCache().catch(console.error);
}
export {
refreshJiraCache
};

40
commands/refresh-cache.ts Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env node
/**
* refresh-cache.ts - JIRA Plugin Cache Refresh
*
* Clears and refreshes JIRA sync cache to prevent stale data issues
*
* Usage:
* /specweave-jira:refresh-cache
*/
import { existsSync, unlinkSync, readdirSync } from 'fs';
import { join } from 'path';
export async function refreshJiraCache(projectRoot: string = process.cwd()): Promise<void> {
const cacheDir = join(projectRoot, '.specweave', 'cache', 'jira');
if (!existsSync(cacheDir)) {
console.log('✅ No JIRA cache found');
return;
}
console.log('🧹 Clearing JIRA cache...');
const files = readdirSync(cacheDir);
let cleared = 0;
for (const file of files) {
const filePath = join(cacheDir, file);
unlinkSync(filePath);
cleared++;
}
console.log(`✅ Cleared ${cleared} cache files`);
}
// CLI entry
if (require.main === module) {
refreshJiraCache().catch(console.error);
}

View File

@@ -0,0 +1,331 @@
---
name: specweave-jira:import-boards
description: Import JIRA boards from a project and map them to SpecWeave projects. Creates 2-level directory structure with board-based organization.
---
# Import JIRA Boards Command
You are a JIRA integration expert. Help the user import boards from a JIRA project and map them to SpecWeave projects.
## Command Usage
```bash
/specweave-jira:import-boards # Interactive mode (prompts for project)
/specweave-jira:import-boards --project CORE # Specific JIRA project
/specweave-jira:import-boards --dry-run # Preview without creating directories
```
## Your Task
When the user runs this command:
### Step 1: Validate Prerequisites
1. **Check JIRA credentials** exist in `.env`:
- `JIRA_API_TOKEN`
- `JIRA_EMAIL`
- `JIRA_DOMAIN`
2. **Check config.json** for existing board mapping:
- If `sync.profiles.*.config.boardMapping` exists, warn user
### Step 2: Get Project Key
**If `--project` flag provided:**
- Use the provided project key
**If no flag (interactive mode):**
```
📋 JIRA Board Import
Enter the JIRA project key to import boards from:
> CORE
Fetching boards from project CORE...
```
### Step 3: Fetch and Display Boards
```typescript
import { JiraClient } from '../../../src/integrations/jira/jira-client';
import { fetchBoardsForProject } from '../lib/jira-board-resolver';
const client = new JiraClient({
domain: process.env.JIRA_DOMAIN,
email: process.env.JIRA_EMAIL,
apiToken: process.env.JIRA_API_TOKEN,
instanceType: 'cloud'
});
const boards = await fetchBoardsForProject(client, 'CORE');
```
**Display boards:**
```
Found 5 boards in project CORE:
1. ☑ Frontend Board (Scrum, 23 active items)
2. ☑ Backend Board (Kanban, 45 active items)
3. ☑ Mobile Board (Scrum, 12 active items)
4. ☐ Platform Board (Kanban, 3 active items)
5. ☐ Archive Board (Simple, 0 items) [deselected - archive]
Select boards to import (Space to toggle, Enter to confirm)
```
### Step 4: Map Boards to SpecWeave Projects
For each selected board, prompt for SpecWeave project ID:
```
🏷️ Mapping boards to SpecWeave projects:
Board "Frontend Board" → SpecWeave project ID: [fe]
→ Keywords for auto-classification (optional): frontend, ui, react, css
Board "Backend Board" → SpecWeave project ID: [be]
→ Keywords for auto-classification (optional): api, server, database
Board "Mobile Board" → SpecWeave project ID: [mobile]
→ Keywords for auto-classification (optional): ios, android, react-native
```
**Project ID validation:**
- Must be lowercase, alphanumeric with hyphens
- Must not collide with existing project IDs
- If collision detected, suggest prefixed version: `core-fe` instead of `fe`
### Step 5: Create Directory Structure
Create 2-level directory structure:
```
.specweave/docs/internal/specs/
└── JIRA-CORE/ ← Level 1: JIRA project
├── fe/ ← Level 2: SpecWeave project
│ └── .gitkeep
├── be/
│ └── .gitkeep
└── mobile/
└── .gitkeep
```
### Step 6: Update config.json
Add board mapping to config:
```json
{
"sync": {
"profiles": {
"jira-default": {
"provider": "jira",
"config": {
"domain": "example.atlassian.net",
"boardMapping": {
"projectKey": "CORE",
"boards": [
{
"boardId": 123,
"boardName": "Frontend Board",
"specweaveProject": "fe",
"boardType": "scrum",
"keywords": ["frontend", "ui", "react", "css"]
},
{
"boardId": 456,
"boardName": "Backend Board",
"specweaveProject": "be",
"boardType": "kanban",
"keywords": ["api", "server", "database"]
},
{
"boardId": 789,
"boardName": "Mobile Board",
"specweaveProject": "mobile",
"boardType": "scrum",
"keywords": ["ios", "android", "react-native"]
}
]
}
}
}
}
},
"multiProject": {
"enabled": true,
"activeProject": "fe",
"projects": {
"fe": {
"name": "Frontend",
"externalTools": {
"jira": {
"boardId": 123,
"projectKey": "CORE"
}
}
},
"be": {
"name": "Backend",
"externalTools": {
"jira": {
"boardId": 456,
"projectKey": "CORE"
}
}
},
"mobile": {
"name": "Mobile",
"externalTools": {
"jira": {
"boardId": 789,
"projectKey": "CORE"
}
}
}
}
}
}
```
### Step 7: Display Summary
```
✅ JIRA Boards Import Complete!
📋 JIRA Project: CORE
📁 Created: .specweave/docs/internal/specs/JIRA-CORE/
Boards imported:
✓ Frontend Board (scrum) → fe
Keywords: frontend, ui, react, css
✓ Backend Board (kanban) → be
Keywords: api, server, database
✓ Mobile Board (scrum) → mobile
Keywords: ios, android, react-native
💡 Next steps:
1. Use /specweave:switch-project fe to switch active project
2. Create increment: /specweave:increment "feature name"
3. User stories will auto-sync to the correct board based on keywords
📖 Documentation: .specweave/docs/internal/architecture/adr/0143-jira-ado-multi-level-project-mapping.md
```
## Examples
### Example 1: Interactive Import
```
User: /specweave-jira:import-boards
You:
📋 JIRA Board Import
Enter the JIRA project key: CORE
Fetching boards...
Found 3 boards:
☑ Frontend Board (scrum)
☑ Backend Board (kanban)
☐ Archive (simple) [deselected]
Mapping to SpecWeave projects:
Frontend Board → fe
Backend Board → be
✅ Import complete! 2 boards mapped.
```
### Example 2: Dry Run
```
User: /specweave-jira:import-boards --project CORE --dry-run
You:
📋 JIRA Board Import (DRY RUN)
Would import from project: CORE
Would create:
.specweave/docs/internal/specs/JIRA-CORE/
.specweave/docs/internal/specs/JIRA-CORE/fe/
.specweave/docs/internal/specs/JIRA-CORE/be/
Would update config.json with board mapping.
No changes made (dry run).
```
### Example 3: Already Configured
```
User: /specweave-jira:import-boards
You:
⚠️ Board mapping already exists for project CORE
Current mappings:
Frontend Board → fe
Backend Board → be
Do you want to:
1. Add more boards
2. Replace existing mapping
3. Cancel
> 1
Fetching additional boards...
☐ Frontend Board (already mapped)
☐ Backend Board (already mapped)
☑ Mobile Board (new)
☐ Archive (deselected)
Added Mobile Board → mobile
✅ Updated! Now 3 boards mapped.
```
## Error Handling
**Missing credentials:**
```
❌ JIRA credentials not found
Please add to .env:
JIRA_API_TOKEN=your_token
JIRA_EMAIL=your_email@example.com
JIRA_DOMAIN=your-company.atlassian.net
Or run: specweave init . (to configure JIRA)
```
**Project not found:**
```
❌ JIRA project "INVALID" not found
Available projects you have access to:
- CORE (Core Development)
- INFRA (Infrastructure)
- MOBILE (Mobile Team)
Tip: Use /specweave-jira:import-boards --project CORE
```
**No boards found:**
```
⚠️ No boards found in project CORE
This could mean:
1. The project uses classic projects (no boards)
2. You don't have access to boards in this project
Suggestions:
- Use /specweave-jira:import-projects for project-based sync
- Ask your JIRA admin about board access
```
## Related Commands
- `/specweave-jira:import-projects` - Import multiple JIRA projects (not boards)
- `/specweave-jira:sync` - Sync increments with JIRA
- `/specweave:switch-project` - Switch active SpecWeave project
- `/specweave:init-multiproject` - Initialize multi-project mode

View File

@@ -0,0 +1,298 @@
---
name: specweave-jira:import-projects
description: Import additional JIRA projects post-init with filtering, resume support, and dry-run preview
---
# Import JIRA Projects Command
You are a JIRA project import expert. Help users add additional JIRA projects to their SpecWeave workspace after initial setup.
## Purpose
This command allows users to import additional JIRA projects **after** initial SpecWeave setup (`specweave init`), with advanced filtering, resume capability, and dry-run preview.
**Use Cases**:
- Adding new JIRA projects to existing workspace
- Importing archived/paused projects later
- Selective import with filters (active only, specific types, custom JQL)
## Command Syntax
```bash
# Basic import (interactive)
/specweave-jira:import-projects
# With filters
/specweave-jira:import-projects --filter active
/specweave-jira:import-projects --type agile --lead "john.doe@company.com"
/specweave-jira:import-projects --jql "project IN (BACKEND, FRONTEND) AND status != Archived"
# Dry-run (preview)
/specweave-jira:import-projects --dry-run
# Resume interrupted import
/specweave-jira:import-projects --resume
# Combined
/specweave-jira:import-projects --filter active --dry-run
```
## Your Task
When the user runs this command:
### Step 1: Validate Prerequisites
```typescript
import { readEnvFile, parseEnvFile } from '../../../src/utils/env-file.js';
// 1. Check if Jira credentials exist
const envContent = readEnvFile(process.cwd());
const parsed = parseEnvFile(envContent);
if (!parsed.JIRA_API_TOKEN || !parsed.JIRA_EMAIL || !parsed.JIRA_DOMAIN) {
console.log('❌ Missing Jira credentials. Run `specweave init` first.');
return;
}
// 2. Get existing projects
const existingProjects = parsed.JIRA_PROJECTS?.split(',') || [];
console.log(`\n📋 Current projects: ${existingProjects.join(', ') || 'None'}\n`);
```
### Step 2: Fetch Available Projects
```typescript
import { getProjectCount } from '../../../src/cli/helpers/project-count-fetcher.js';
import { AsyncProjectLoader } from '../../../src/cli/helpers/async-project-loader.js';
// Count check (< 1 second)
const countResult = await getProjectCount({
provider: 'jira',
credentials: {
domain: parsed.JIRA_DOMAIN,
email: parsed.JIRA_EMAIL,
token: parsed.JIRA_API_TOKEN,
instanceType: 'cloud'
}
});
console.log(`✓ Found ${countResult.accessible} accessible projects`);
// Fetch all projects (with smart pagination)
const loader = new AsyncProjectLoader(
{
domain: parsed.JIRA_DOMAIN,
email: parsed.JIRA_EMAIL,
token: parsed.JIRA_API_TOKEN,
instanceType: 'cloud'
},
'jira',
{
batchSize: 50,
updateFrequency: 5,
showEta: true
}
);
const result = await loader.fetchAllProjects(countResult.accessible);
let allProjects = result.projects;
```
### Step 3: Apply Filters (if specified)
```typescript
import { FilterProcessor } from '../../../src/integrations/jira/filter-processor.js';
const options = {
filter: args.filter, // 'active' | 'all'
type: args.type, // 'agile' | 'software' | 'business'
lead: args.lead, // Email address
jql: args.jql // Custom JQL
};
const filterProcessor = new FilterProcessor({ domain: parsed.JIRA_DOMAIN, token: parsed.JIRA_API_TOKEN });
const filteredProjects = await filterProcessor.applyFilters(allProjects, options);
console.log(`\n🔍 Filters applied:`);
if (options.filter === 'active') console.log(` • Active projects only`);
if (options.type) console.log(` • Type: ${options.type}`);
if (options.lead) console.log(` • Lead: ${options.lead}`);
if (options.jql) console.log(` • JQL: ${options.jql}`);
console.log(`\n📊 Results: ${filteredProjects.length} projects (down from ${allProjects.length})\n`);
```
### Step 4: Exclude Existing Projects
```typescript
const newProjects = filteredProjects.filter(p => !existingProjects.includes(p.key));
if (newProjects.length === 0) {
console.log('✅ No new projects to import. All filtered projects are already configured.');
return;
}
console.log(`📥 ${newProjects.length} new project(s) available for import:\n`);
newProjects.forEach(p => {
console.log(`${p.key} - ${p.name} (${p.projectTypeKey}, lead: ${p.lead?.displayName || 'N/A'})`);
});
```
### Step 5: Dry-Run or Execute
```typescript
if (args.dryRun) {
console.log('\n🔎 DRY RUN: No changes will be made.\n');
console.log('The following projects would be imported:');
newProjects.forEach(p => {
const status = p.archived ? '⏭️ (archived - skipped)' : '✨';
console.log(` ${status} ${p.key} - ${p.name}`);
});
console.log(`\nTotal: ${newProjects.filter(p => !p.archived).length} projects would be imported\n`);
return;
}
// Confirm import
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
message: `Import ${newProjects.length} project(s)?`,
default: true
}]);
if (!confirmed) {
console.log('⏭️ Import cancelled.');
return;
}
```
### Step 6: Merge with Existing
```typescript
import { updateEnvFile, mergeProjectList } from '../../../src/utils/env-manager.js';
const newKeys = newProjects.map(p => p.key);
const mergedProjects = mergeProjectList(existingProjects, newKeys);
// Update .env file (atomic write)
await updateEnvFile('JIRA_PROJECTS', mergedProjects.join(','));
console.log('\n✅ Projects imported successfully!\n');
console.log(`Updated: ${existingProjects.length}${mergedProjects.length} projects`);
console.log(`\nCurrent projects:\n ${mergedProjects.join(', ')}\n`);
```
### Step 7: Resume Support
```typescript
if (args.resume) {
const { CacheManager } = await import('../../../src/core/cache/cache-manager.js');
const cacheManager = new CacheManager(process.cwd());
const importState = await cacheManager.get('jira-import-state');
if (!importState) {
console.log('⚠️ No import state found. Use without --resume to start fresh.');
return;
}
console.log(`\n📂 Resuming from: ${importState.lastProject} (${importState.completed}/${importState.total})`);
// Skip already-imported projects
const remainingProjects = allProjects.filter(p => !importState.succeeded.includes(p.key));
// Continue import with remaining projects
// (use same logic as Step 6)
}
```
## Examples
### Example 1: Basic Import
**User**: `/specweave-jira:import-projects`
**Output**:
```
📋 Current projects: BACKEND, FRONTEND
✓ Found 127 accessible projects
📥 5 new project(s) available for import:
✨ MOBILE - Mobile App (agile, lead: John Doe)
✨ INFRA - Infrastructure (software, lead: Jane Smith)
✨ QA - Quality Assurance (business, lead: Bob Wilson)
✨ DOCS - Documentation (business, lead: Alice Cooper)
✨ LEGACY - Legacy System (archived - skipped)
Import 5 project(s)? (Y/n)
✅ Projects imported successfully!
Updated: 2 → 6 projects
Current projects:
BACKEND, FRONTEND, MOBILE, INFRA, QA, DOCS
```
### Example 2: Filter Active Only
**User**: `/specweave-jira:import-projects --filter active`
**Output**:
```
🔍 Filters applied:
• Active projects only
📊 Results: 120 projects (down from 127)
📥 4 new project(s) available for import:
(LEGACY project excluded because it's archived)
```
### Example 3: Dry-Run
**User**: `/specweave-jira:import-projects --dry-run`
**Output**:
```
🔎 DRY RUN: No changes will be made.
The following projects would be imported:
✨ MOBILE - Mobile App
✨ INFRA - Infrastructure
✨ QA - Quality Assurance
⏭️ LEGACY - Legacy System (archived - skipped)
Total: 3 projects would be imported
```
### Example 4: Custom JQL Filter
**User**: `/specweave-jira:import-projects --jql "project IN (MOBILE, INFRA) AND status != Archived"`
**Output**:
```
🔍 Filters applied:
• JQL: project IN (MOBILE, INFRA) AND status != Archived
📊 Results: 2 projects (down from 127)
📥 2 new project(s) available for import:
✨ MOBILE - Mobile App
✨ INFRA - Infrastructure
```
## Important Notes
- **Merge Logic**: No duplicates. Existing projects are preserved.
- **Atomic Updates**: Uses temp file + rename to prevent corruption.
- **Progress Tracking**: Shows progress bar for large imports (> 50 projects).
- **Resume Support**: Interrupted imports can be resumed with `--resume`.
- **Backup**: Creates `.env.backup` before modifying `.env`.
## Related Commands
- `/specweave:init` - Initial SpecWeave setup
- `/specweave-jira:sync` - Sync increments with Jira epics
- `/specweave-jira:refresh-cache` - Clear cached project data
## Error Handling
- **Missing Credentials**: Prompt user to run `specweave init` first
- **API Errors**: Show clear error message with suggestion
- **No New Projects**: Inform user all projects already imported
- **Permission Errors**: Check `.env` file permissions
---
**Post-Init Flexibility**: This command provides flexibility to add projects after initial setup, supporting evolving team structures and project lifecycles.

View File

@@ -0,0 +1,240 @@
---
name: specweave-jira:sync
description: Sync SpecWeave increments with JIRA epics/stories. Supports import, export, two-way sync, and granular item operations
---
# Sync Jira Command
You are a Jira synchronization expert. Help the user sync between Jira and SpecWeave with granular control.
## Available Operations
### Epic-Level Operations
**1. Two-way Sync (Default - Recommended)**
```
/specweave-jira:sync 0003 # Two-way sync (default)
/specweave-jira:sync 0003 --direction two-way # Explicit
```
**2. Import Jira Epic as SpecWeave Increment**
```
/specweave-jira:sync import SCRUM-123 # One-time pull
/specweave-jira:sync SCRUM-123 --direction from-jira # Same as import
```
**3. Export SpecWeave Increment to Jira**
```
/specweave-jira:sync export 0001 # One-time push
/specweave-jira:sync 0001 --direction to-jira # Same as export
```
### Sync Direction Options
**Default: `two-way`** (both directions - recommended)
- `--direction two-way`: SpecWeave ↔ Jira (default)
- Pull changes FROM Jira (status, priority, comments)
- Push changes TO Jira (tasks, progress, metadata)
- `--direction to-jira`: SpecWeave → Jira only
- Push increment progress to Jira
- Don't pull Jira changes back
- Same as `export` operation
- `--direction from-jira`: Jira → SpecWeave only
- Pull Jira issue updates
- Don't push SpecWeave changes
- Same as `import` operation
### Granular Item Operations
**4. Add specific Story/Bug/Task to existing Increment**
```
/specweave-jira:sync add SCRUM-1 to 0003
/specweave-jira:sync add SCRUM-1 # Adds to current increment
```
**5. Create Increment from specific items (cherry-pick)**
```
/specweave-jira:sync create "User Authentication" from SCRUM-1 SCRUM-5 SCRUM-7
/specweave-jira:sync create "Bug Fixes Sprint 1" from SCRUM-10 SCRUM-15 SCRUM-20
```
**6. Show sync status**
```
/specweave-jira:sync status
/specweave-jira:sync status 0003 # Status of specific increment
```
## Your Task
When the user runs this command:
1. **Parse the command arguments**:
- Operation: import, sync, export, add, create, or status
- ID: Jira Epic key (e.g., SCRUM-123) or Increment ID (e.g., 0001)
2. **Execute the operation**:
**For import Epic**:
```typescript
import { JiraClient } from './src/integrations/jira/jira-client';
import { JiraMapper } from './src/integrations/jira/jira-mapper';
const client = new JiraClient();
const mapper = new JiraMapper(client);
const result = await mapper.importEpicAsIncrement('SCRUM-123');
```
**For add item**:
```typescript
import { JiraIncrementalMapper } from './src/integrations/jira/jira-incremental-mapper';
const incrementalMapper = new JiraIncrementalMapper(client);
const result = await incrementalMapper.addItemToIncrement('0003', 'SCRUM-1');
```
**For create from items**:
```typescript
const result = await incrementalMapper.createIncrementFromItems(
'User Authentication',
['SCRUM-1', 'SCRUM-5', 'SCRUM-7']
);
```
**For sync**:
```typescript
const result = await mapper.syncIncrement('0003');
```
**For export**:
```typescript
const result = await mapper.exportIncrementAsEpic('0001', 'SCRUM');
```
3. **Show results**:
- Display sync summary
- Show conflicts (if any)
- List created/updated files
- Provide links to Jira and SpecWeave
4. **Handle errors gracefully**:
- Check if .env credentials exist
- Validate increment/epic exists
- Show clear error messages
## Examples
### Example 1: Import Epic
**User**: `/specweave:sync-jira import SCRUM-2`
**You**:
- Import Epic SCRUM-2 from Jira
- Show: "✅ Imported as Increment 0004"
- List: "Created: spec.md, tasks.md, RFC document"
- Link: "Jira: https://... | Increment: .specweave/increments/0004/"
### Example 2: Add Story to Current Increment
**User**: `/specweave:sync-jira add SCRUM-1`
**You**:
- Determine current increment (latest or from context)
- Fetch SCRUM-1 from Jira
- Add to increment's spec.md (under ## User Stories)
- Update tasks.md
- Update RFC
- Show: "✅ Added Story SCRUM-1 to Increment 0003"
- Display: "Type: story | Title: User can login | Status: in-progress"
### Example 3: Add Bug to Specific Increment
**User**: `/specweave:sync-jira add SCRUM-10 to 0003`
**You**:
- Fetch SCRUM-10 from Jira (it's a Bug)
- Add to increment 0003's spec.md (under ## Bugs)
- Update tasks.md
- Update RFC
- Show: "✅ Added Bug SCRUM-10 to Increment 0003"
- Display: "Type: bug | Priority: P1 | Title: Fix login redirect"
### Example 4: Create Increment from Multiple Items
**User**: `/specweave:sync-jira create "User Authentication" from SCRUM-1 SCRUM-5 SCRUM-7`
**You**:
- Fetch all 3 issues from Jira
- Determine types (story, bug, task)
- Create new increment 0005
- Group by type in spec.md:
- ## User Stories (SCRUM-1, SCRUM-5)
- ## Technical Tasks (SCRUM-7)
- Generate RFC with all items
- Show: "✅ Created Increment 0005 with 3 work items"
- Display table:
```
| Type | Jira Key | Title |
|-------|----------|-----------------|
| Story | SCRUM-1 | User login UI |
| Story | SCRUM-5 | OAuth backend |
| Task | SCRUM-7 | Setup provider |
```
### Example 5: Two-way Sync (Default)
**User**: `/specweave-jira:sync 0003`
**You**:
- Read increment 0003
- Find linked Jira items (from spec.md frontmatter.work_items)
- Fetch current state from Jira
**Detect changes (both directions)**:
- FROM Jira: Status changes, priority updates, comments
- FROM SpecWeave: Task completion, progress updates
**Show two-way sync summary**:
```
✅ Two-way Sync Complete: 0003 ↔ Jira
FROM Jira:
• SCRUM-1: Status changed to In Progress
• SCRUM-10: Priority raised to P1
FROM SpecWeave:
• 3 tasks completed (T-005, T-006, T-007)
• Progress: 60% → 75%
Conflicts: None
```
**Handle conflicts if any**:
- Show both versions (Jira vs SpecWeave)
- Ask user which to keep or how to merge
- Apply resolution in both directions
### Example 6: Status Overview
**User**: `/specweave-jira:sync status`
**You**:
- Scan all increments for Jira metadata
- Show table:
```
| Increment | Title | Jira Items | Last Sync |
|-----------|------------------|------------|--------------------|
| 0003 | Test Epic | 0 items | 2025-10-28 17:42 |
| 0004 | User Auth | 3 items | 2025-10-28 18:00 |
| 0005 | Bug Fixes | 5 items | Never |
```
## Important Notes
- Always check if .env has Jira credentials before syncing
- Never log secrets or tokens
- Show clear progress messages
- Display rich output with links
- Save sync results to test-results/ if requested
## Related Commands
- `/specweave-github:sync` - Sync to GitHub issues (also two-way by default)
- `/specweave:increment` - Create new increment
- `/specweave:validate` - Validate increment quality
---
**Two-way by Default**: All sync operations are two-way unless you explicitly specify `--direction to-jira` or `--direction from-jira`. This keeps both systems synchronized automatically.
**Granular Control**: Unlike simple epic import/export, this command supports cherry-picking individual stories, bugs, and tasks for maximum flexibility.

201
hooks/README.md Normal file
View File

@@ -0,0 +1,201 @@
# SpecWeave JIRA Plugin Hooks
**Plugin**: `specweave-jira`
**Location**: `plugins/specweave-jira/hooks/`
---
## Purpose
This hook automatically syncs SpecWeave increment progress to JIRA Issues after each task completion.
**Key Features**:
- ✅ Updates JIRA issue status based on increment progress
- ✅ Syncs task completion state to JIRA
- ✅ Bidirectional sync (local → JIRA)
- ✅ Non-blocking (failures don't stop core workflow)
- ✅ Self-contained (no dependencies on core plugin)
---
## Available Hooks
### 1. `post-task-completion.sh`
**Triggers**: After ANY task is marked complete (via TodoWrite tool)
**Preconditions**:
- ✅ Active increment exists (`.specweave/increments/####/`)
-`metadata.json` has `.jira.issue` field
- ✅ Node.js installed
- ✅ JIRA sync script exists (`dist/commands/jira-sync.js`)
- ✅ JIRA API credentials in `.env`
**Actions**:
1. Reads increment metadata
2. Calls JIRA sync script (Node.js)
3. Updates JIRA issue status
4. Logs all actions to `.specweave/logs/hooks-debug.log`
**Example Flow**:
```
Task T-003 completed in tasks.md
Hook fires (PostToolUse + TodoWrite matcher)
JIRA sync script updates issue PROJ-123
Log: "[JIRA] ✅ JIRA sync complete"
```
**Dependencies**:
- Node.js 18+
- JIRA API credentials (`.env`)
- `jq` for JSON parsing
---
## Configuration
### Hook Registration (`hooks.json`)
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "TodoWrite",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-task-completion.sh"
}
]
}
]
}
}
```
### Metadata Format
Increments must have `metadata.json` with JIRA issue link:
```json
{
"jira": {
"issue": "PROJ-123",
"url": "https://company.atlassian.net/browse/PROJ-123",
"project": "PROJ"
}
}
```
### Environment Variables
Required in `.env`:
```bash
JIRA_HOST=https://company.atlassian.net
JIRA_EMAIL=user@company.com
JIRA_API_TOKEN=your_api_token_here
```
---
## Installation
### Automatic (Recommended)
```bash
npx specweave init my-project
# Installs all plugins including specweave-jira
# Hooks auto-register via hooks.json
```
### Manual (Development)
```bash
# Install plugin
/plugin install specweave-jira
# Verify hook registration
cat ~/.claude/settings.json | grep specweave-jira
```
---
## Testing
### Test Hook Independently
```bash
# Test syntax
bash -n plugins/specweave-jira/hooks/post-task-completion.sh
# Test execution (requires active increment + JIRA issue)
./plugins/specweave-jira/hooks/post-task-completion.sh
```
---
## Logging
All JIRA sync actions are logged to:
```
.specweave/logs/hooks-debug.log
```
**Example Log Output**:
```
[2025-11-10] [JIRA] 🔗 JIRA sync hook fired
[2025-11-10] [JIRA] 🔄 Syncing to JIRA issue PROJ-123
[2025-11-10] [JIRA] ✅ JIRA sync complete
```
**Log Prefixes**:
- `[JIRA]` - All messages from this hook
- `🔗` - Hook fired
- `🔄` - Syncing started
- `✅` - Success
- `⚠️` - Warning (non-blocking failure)
- `` - Info (skipped due to precondition)
---
## Architecture
### Separation from Core Plugin
**Before (v0.12.x)**:
```
Core hook (500+ lines)
├── JIRA sync ← Embedded in core!
```
**After (v0.13.0+)**:
```
Core hook (330 lines) JIRA plugin hook (150 lines)
├── Core concerns ├── Check for JIRA issue
├── Call Node.js sync script
└── Log actions
```
**Key Benefits**:
-**Optional plugin**: JIRA sync only runs if `specweave-jira` installed
-**Independent testing**: Test JIRA sync in isolation
-**No core dependencies**: Core plugin doesn't depend on JIRA API
---
## Related Documentation
- **Core Plugin Hooks**: `plugins/specweave/hooks/README.md`
- **Architecture Analysis**: `.specweave/increments/0018-strict-increment-discipline-enforcement/reports/HOOKS-ARCHITECTURE-ANALYSIS.md`
- **JIRA Sync Command**: `plugins/specweave-jira/commands/specweave-jira-sync.md`
---
**Version**: v0.13.0+
**Last Updated**: 2025-11-10

180
hooks/post-task-completion.sh Executable file
View File

@@ -0,0 +1,180 @@
#!/bin/bash
# SpecWeave JIRA Sync Hook
# Runs after task completion to sync progress to JIRA Issues
#
# This hook is part of the specweave-jira plugin and handles:
# - Syncing task completion state to JIRA issues
# - Updating JIRA issue status based on increment progress
#
# Dependencies:
# - Node.js for running sync scripts
# - jq for JSON parsing
# - metadata.json must have .jira.issue field
# - JIRA API credentials in .env
set +e # EMERGENCY FIX: Prevents Claude Code crashes
# EMERGENCY KILL SWITCH
if [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]]; then
exit 0
fi
# ============================================================================
# PROJECT ROOT DETECTION
# ============================================================================
# Find project root by searching upward for .specweave/ directory
find_project_root() {
local dir="$1"
while [ "$dir" != "/" ]; do
if [ -d "$dir/.specweave" ]; then
echo "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
# Fallback: try current directory
if [ -d "$(pwd)/.specweave" ]; then
pwd
else
echo "$(pwd)"
fi
}
PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
cd "$PROJECT_ROOT" 2>/dev/null || true
# ============================================================================
# CONFIGURATION
# ============================================================================
LOGS_DIR=".specweave/logs"
DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
mkdir -p "$LOGS_DIR" 2>/dev/null || true
# ============================================================================
# PRECONDITIONS CHECK
# ============================================================================
echo "[$(date)] [JIRA] 🔗 JIRA sync hook fired" >> "$DEBUG_LOG" 2>/dev/null || true
# Detect current increment
CURRENT_INCREMENT=$(ls -td .specweave/increments/*/ 2>/dev/null | xargs -n1 basename | grep -v "_backlog" | grep -v "_archive" | grep -v "_working" | head -1)
if [ -z "$CURRENT_INCREMENT" ]; then
echo "[$(date)] [JIRA] No active increment, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
# Check for metadata.json
METADATA_FILE=".specweave/increments/$CURRENT_INCREMENT/metadata.json"
if [ ! -f "$METADATA_FILE" ]; then
echo "[$(date)] [JIRA] No metadata.json for $CURRENT_INCREMENT, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
# Check for JIRA issue link
if ! command -v jq &> /dev/null; then
echo "[$(date)] [JIRA] ⚠️ jq not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
JIRA_ISSUE=$(jq -r '.jira.issue // empty' "$METADATA_FILE" 2>/dev/null)
if [ -z "$JIRA_ISSUE" ]; then
echo "[$(date)] [JIRA] No JIRA issue linked to $CURRENT_INCREMENT, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
# Check for Node.js
if ! command -v node &> /dev/null; then
echo "[$(date)] [JIRA] ⚠️ Node.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
# Check for JIRA sync script
if [ ! -f "dist/commands/jira-sync.js" ]; then
echo "[$(date)] [JIRA] ⚠️ jira-sync.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
cat <<EOF
{
"continue": true
}
EOF
exit 0
fi
# ============================================================================
# JIRA SYNC LOGIC
# ============================================================================
echo "[$(date)] [JIRA] 🔄 Syncing to JIRA issue $JIRA_ISSUE" >> "$DEBUG_LOG" 2>/dev/null || true
# Run JIRA sync command (non-blocking)
node dist/commands/jira-sync.js "$CURRENT_INCREMENT" 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
echo "[$(date)] [JIRA] ⚠️ Failed to sync to JIRA (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
}
echo "[$(date)] [JIRA] ✅ JIRA sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
# ============================================================================
# SPEC COMMIT SYNC (NEW!)
# ============================================================================
echo "[$(date)] [JIRA] 🔗 Checking for spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
# Call TypeScript CLI to sync commits
if command -v node &> /dev/null && [ -f "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" ]; then
echo "[$(date)] [JIRA] 🚀 Running spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
node "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" \
--increment "$PROJECT_ROOT/.specweave/increments/$CURRENT_INCREMENT" \
--provider jira \
2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
echo "[$(date)] [JIRA] ⚠️ Spec commit sync failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
}
echo "[$(date)] [JIRA] ✅ Spec commit sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
else
echo "[$(date)] [JIRA] Spec commit sync not available (node or script not found)" >> "$DEBUG_LOG" 2>/dev/null || true
fi
# ============================================================================
# OUTPUT TO CLAUDE
# ============================================================================
cat <<EOF
{
"continue": true
}
EOF
# ALWAYS exit 0 - NEVER let hook errors crash Claude Code
exit 0

105
plugin.lock.json Normal file
View File

@@ -0,0 +1,105 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:anton-abyzov/specweave:plugins/specweave-jira",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b627db3771b01601090d188e8d2b80fe1e7bd562",
"treeHash": "482cd16f27bdbc1716b62d32d68e0883486ce8e406ce126fd1f6a84eec25ebd7",
"generatedAt": "2025-11-28T10:13:50.336028Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "specweave-jira",
"description": "JIRA integration for SpecWeave increments. Bidirectional sync between SpecWeave increments and JIRA epics/stories. Automatically creates JIRA issues from increments, tracks progress, and updates status.",
"version": "0.24.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "6a26a37431ae13682222bdb2e49d82212496134ca42d163608bd1245a98c60e9"
},
{
"path": "agents/jira-manager/AGENT.md",
"sha256": "892cdeaeccf1d85a7eb2396828e66320e7791fdfd2c07c00f69da333f1c470c6"
},
{
"path": "hooks/post-task-completion.sh",
"sha256": "8081f44b6cff8768227d9498ca5fce059ff86a330f575c49ceba8307da333b6d"
},
{
"path": "hooks/README.md",
"sha256": "1dfffda284be58c8fa3e25631a1bb4798d7cf004ab54df9cff7e3ac11327831e"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "b6bfc364e1e16efce66e54570ea58c6a5a1bb299352d0ceeb991cf7da5d5a3a3"
},
{
"path": "commands/import-projects.md",
"sha256": "e4dc70353f350baf49a8de08b8bf206fe6ca05c7452f194c3742cb8ee25e5b12"
},
{
"path": "commands/specweave-jira-sync.md",
"sha256": "aa91dfc433a7ec49a5a8630c930ad035a98f03e0b05c2a15da0bbf90d97d1cf5"
},
{
"path": "commands/refresh-cache.ts",
"sha256": "69f9d9586f433f463c785216e3f8d85ec7965464efda45c29d7ad24df9b477c0"
},
{
"path": "commands/import-projects.ts",
"sha256": "d1c1c8329ac75ea5f1b730385e9833b8a3d89828bae3fe43e8bdba0b13e841bf"
},
{
"path": "commands/specweave-jira-import-boards.md",
"sha256": "c99147aa6212f881fd988a627ad64a9f2124a3677789ded4238bcdc90b0cea0b"
},
{
"path": "commands/specweave-jira-import-projects.md",
"sha256": "a835eecc8b2314e995496e71da431bfbe02162b822a6f7a16ead0792eb5a87a5"
},
{
"path": "commands/import-projects.js",
"sha256": "a82f8762cbc42de06afaa6e65594bfd705fc4ded77739ecb040a5f2268af2014"
},
{
"path": "commands/refresh-cache.js",
"sha256": "f958a2ace799b2c9e0e47120e44694024c258308e828ecb9f9df70318a338124"
},
{
"path": "skills/jira-sync/README.md",
"sha256": "1880d697c8efb692238da66280a6ddb42067e84cc953cf1132475672ad7ba683"
},
{
"path": "skills/jira-sync/.gitignore",
"sha256": "94141423426c2b1cd10650984dc5f5e10c0f2ec3b54162bb90bcf07f36c6a725"
},
{
"path": "skills/jira-sync/SKILL.md",
"sha256": "f252c0123a17c94bfbf90f9e770b2c9e03b68edbc17b0e3503fc511f686f0908"
},
{
"path": "skills/jira-resource-validator/SKILL.md",
"sha256": "7bf895a8646952e11b188adb0f006e83080c84c5b41996b29d3dc88d7e0e5b28"
},
{
"path": "skills/specweave-jira-mapper/SKILL.md",
"sha256": "2e38da629156c9344c2b3f860478564aac4bbb3084f2d58988e94d52897c0662"
}
],
"dirSha256": "482cd16f27bdbc1716b62d32d68e0883486ce8e406ce126fd1f6a84eec25ebd7"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,677 @@
---
name: jira-resource-validator
description: Validates Jira projects and boards exist, creates missing resources automatically. Smart enough to prompt user to select existing or create new projects. For boards, accepts either IDs (validates existence) or names (creates boards and updates .env with IDs). NEW - Per-project configuration support - JIRA_BOARDS_{ProjectKey} for hierarchical board organization across multiple projects. Activates for jira setup, jira validation, jira configuration, missing jira project, missing jira boards, jira .env setup, per-project boards.
allowed-tools: Read, Bash, Write, Edit
---
# Jira Resource Validator Skill
**Purpose**: Validate and auto-create Jira projects and boards, ensuring .env configuration is correct.
**Auto-Activation**: Triggers when Jira setup or validation is needed.
## What This Skill Does
This skill ensures your Jira configuration in `.env` is valid and all resources exist. It's **smart enough** to:
1. **Validate Jira projects** - Check if `JIRA_PROJECT` exists
2. **Prompt for action** - Select existing project or create new one
3. **Validate Jira boards** - Check if boards exist (by ID or name)
4. **Create missing boards** - If board names provided, create them automatically
5. **Update .env with IDs** - Replace board names with actual board IDs after creation
## When This Skill Activates
**Automatically activates when**:
- You set up Jira integration for the first time
- You run `/specweave-jira:sync` and resources are missing
- Your `.env` has invalid Jira configuration
- You mention "jira setup" or "jira validation"
## Jira Configuration Structure
### Required .env Variables
```bash
JIRA_API_TOKEN=your_token_here
JIRA_EMAIL=your_email@company.com
JIRA_DOMAIN=yourcompany.atlassian.net
JIRA_STRATEGY=board-based
JIRA_PROJECT=PROJECTKEY
JIRA_BOARDS=1,2,3 # IDs (if exist) OR names (if creating)
```
### Smart Per-Board Detection (Mixed Mode Support!)
**The system is smart enough to handle ANY combination of IDs and names:**
**All IDs** (validate existing boards):
```bash
JIRA_BOARDS=1,2,3
```
→ Validates boards 1, 2, 3 exist
**All Names** (create new boards):
```bash
JIRA_BOARDS=Frontend,Backend,Mobile
```
→ Creates 3 boards, updates .env with IDs: `JIRA_BOARDS=101,102,103`
**Mixed IDs and Names** (smart handling!):
```bash
JIRA_BOARDS=101,102,QA,Dashboard
```
→ Validates 101, 102 exist
→ Creates "QA" and "Dashboard" boards
→ Updates .env: `JIRA_BOARDS=101,102,103,104` (all IDs!)
**How it works**: Each entry is checked individually:
- Numeric (e.g., "123") → Validate ID exists
- Non-numeric (e.g., "QA") → Create board with that name
- After creation, .env is updated with ALL board IDs
### NEW: Per-Project Configuration (Advanced - Multiple Projects × Boards)
**Multiple JIRA projects with their own boards:**
```bash
# Multiple projects with their own boards
JIRA_STRATEGY=project-per-team
JIRA_PROJECTS=BACKEND,FRONTEND,MOBILE
# Per-project boards (hierarchical naming)
JIRA_BOARDS_BACKEND=123,456 # Sprint + Kanban (IDs)
JIRA_BOARDS_FRONTEND=Sprint,Bug # Create these boards
JIRA_BOARDS_MOBILE=789,012,345 # iOS + Android + Release (IDs)
```
→ Validates 3 projects exist: BACKEND, FRONTEND, MOBILE
→ Validates/creates boards per project:
- BACKEND: Validates boards 123, 456 exist
- FRONTEND: Creates "Sprint" and "Bug" boards, updates .env with IDs
- MOBILE: Validates boards 789, 012, 345 exist
**Naming Convention**: `{PROVIDER}_{RESOURCE_TYPE}_{PROJECT_KEY}`
**Mixed IDs and Names Per Project**:
```bash
JIRA_BOARDS_BACKEND=123,NewBoard,456
```
→ Validates 123, 456 exist
→ Creates "NewBoard"
→ Updates .env: `JIRA_BOARDS_BACKEND=123,789,456` (all IDs!)
## Validation Flow
### Step 1: Project Validation
**Check if project exists**:
```bash
# API call to Jira
GET /rest/api/3/project/PROJECTKEY
```
**If project exists**:
```
✅ Project "PROJECTKEY" exists
ID: 10001
Name: My Project
```
**If project doesn't exist**:
```
⚠️ Project "PROJECTKEY" not found
What would you like to do?
1. Select an existing project
2. Create a new project
3. Cancel
Your choice [1]:
```
**Option 1: Select Existing**:
```
Available projects:
1. PROJ1 - Project One
2. PROJ2 - Project Two
3. PROJ3 - Project Three
Select a project [1]:
✅ Updated .env: JIRA_PROJECT=PROJ1
```
**Option 2: Create New**:
```
Enter project name: My New Project
📦 Creating Jira project: PROJECTKEY (My New Project)...
✅ Project created: PROJECTKEY (ID: 10005)
```
### Step 2: Board Validation (Per-Board Smart Detection)
**Scenario A: All Board IDs** (all numeric):
```bash
JIRA_BOARDS=1,2,3
```
**Validation**:
```
Checking boards: 1,2,3...
✅ Board 1: Frontend Board (exists)
✅ Board 2: Backend Board (exists)
⚠️ Board 3: Not found
⚠️ Issues found: 1 board(s)
```
**Scenario B: All Board Names** (all non-numeric):
```bash
JIRA_BOARDS=Frontend,Backend,Mobile
```
**Auto-creation**:
```
Checking boards: Frontend,Backend,Mobile...
📦 Creating board: Frontend...
✅ Created: Frontend (ID: 101)
📦 Creating board: Backend...
✅ Created: Backend (ID: 102)
📦 Creating board: Mobile...
✅ Created: Mobile (ID: 103)
📝 Updating .env with board IDs...
✅ Updated JIRA_BOARDS: 101,102,103
✅ All boards validated/created successfully
```
**Scenario C: Mixed IDs and Names** (SMART!):
```bash
JIRA_BOARDS=101,102,QA,Dashboard
```
**Smart handling**:
```
Checking boards: 101,102,QA,Dashboard...
✅ Board 101: Frontend Board (exists)
✅ Board 102: Backend Board (exists)
📦 Creating board: QA...
✅ Created: QA (ID: 103)
📦 Creating board: Dashboard...
✅ Created: Dashboard (ID: 104)
📝 Updating .env with board IDs...
✅ Updated JIRA_BOARDS: 101,102,103,104
✅ All boards validated/created successfully
```
## Usage Examples
### Example 1: Fresh Jira Setup
**Scenario**: New project, no Jira resources exist yet
**Action**: Run `/specweave-jira:sync`
**What Happens**:
```bash
🔍 Validating Jira configuration...
Checking project: MINIDOOM...
⚠️ Project "MINIDOOM" not found
What would you like to do?
1. Select an existing project
2. Create a new project
3. Cancel
Your choice [2]: 2
Enter project name: Mini DOOM Tournament
📦 Creating Jira project: MINIDOOM (Mini DOOM Tournament)...
✅ Project created: MINIDOOM (ID: 10005)
Checking boards: Frontend,Backend,Mobile...
📦 Creating boards from names...
Creating board: Frontend in project MINIDOOM...
✅ Board created: Frontend (ID: 101)
Creating board: Backend in project MINIDOOM...
✅ Board created: Backend (ID: 102)
Creating board: Mobile in project MINIDOOM...
✅ Board created: Mobile (ID: 103)
✅ Updated .env: JIRA_BOARDS=101,102,103
🎉 Jira configuration complete! All resources ready.
```
**Result**: `.env` now has correct project and board IDs
### Example 2: Select Existing Project
**Scenario**: Project already exists in Jira
**Action**: Run validation
**What Happens**:
```bash
🔍 Validating Jira configuration...
Checking project: PROJ...
⚠️ Project "PROJ" not found
What would you like to do?
1. Select an existing project
2. Create a new project
3. Cancel
Your choice [1]: 1
Available projects:
1. FRONTEND - Frontend Team
2. BACKEND - Backend Team
3. MOBILE - Mobile Team
Select a project [1]: 2
✅ Updated .env: JIRA_PROJECT=BACKEND
✅ Project "BACKEND" exists
Checking boards: 45,46...
✅ All boards exist
```
### Example 3: Mixed Board IDs (Some Exist, Some Don't)
**Scenario**: Some board IDs are invalid
**Action**: Run validation
**What Happens**:
```bash
🔍 Validating Jira configuration...
Checking project: PROJECTKEY...
✅ Project "PROJECTKEY" exists
Checking boards: 1,2,999...
Board 1: ✅ Exists (Frontend Board)
Board 2: ✅ Exists (Backend Board)
Board 999: ❌ Not found
⚠️ Boards not found: 999
Available boards in project PROJECTKEY:
1. Frontend Board (ID: 1)
2. Backend Board (ID: 2)
3. QA Board (ID: 3)
4. DevOps Board (ID: 4)
Would you like to:
1. Remove invalid board (999) from configuration
2. Replace with correct board ID
3. Create new board
Your choice [2]: 2
Enter correct board ID or name: 3
✅ Updated .env: JIRA_BOARDS=1,2,3
```
## CLI Command
**Manual validation**:
```bash
# From TypeScript
npx tsx src/utils/external-resource-validator.ts
# Or via skill activation
"Can you validate my Jira configuration?"
```
**Validation output**:
```typescript
{
valid: true,
project: {
exists: true,
key: 'PROJECTKEY',
id: '10001',
name: 'My Project'
},
boards: {
valid: true,
existing: [1, 2, 3],
missing: [],
created: []
},
envUpdated: false
}
```
## Smart Board Creation Logic (Per-Board Detection)
### Detection Algorithm
```typescript
// Parse JIRA_BOARDS from .env
const boardsConfig = "101,102,QA,Dashboard"; // Mixed!
const boardEntries = boardsConfig.split(',').map(b => b.trim());
const finalBoardIds = [];
// Check EACH board individually
for (const entry of boardEntries) {
const isNumeric = /^\d+$/.test(entry);
if (isNumeric) {
// Entry is a board ID - validate it exists
const boardId = parseInt(entry);
const board = await checkBoard(boardId);
if (board) {
console.log(`✅ Board ${boardId}: ${board.name} (exists)`);
finalBoardIds.push(boardId);
} else {
console.error(`⚠️ Board ${boardId}: Not found`);
}
} else {
// Entry is a board name - create it
console.log(`📦 Creating board: ${entry}...`);
const board = await createBoard(entry, projectKey);
console.log(`✅ Created: ${entry} (ID: ${board.id})`);
finalBoardIds.push(board.id);
}
}
// Update .env if any boards were created
if (createdBoardIds.length > 0) {
updateEnv({ JIRA_BOARDS: finalBoardIds.join(',') });
}
```
**Key improvement**: Per-board detection instead of all-or-nothing!
- `JIRA_BOARDS=1,2,3` → Validates all IDs
- `JIRA_BOARDS=A,B,C` → Creates all boards
- `JIRA_BOARDS=1,2,C` → Validates 1,2, creates C (mixed!)
### Board Creation API
**Jira REST API** (v3):
```bash
POST /rest/api/3/board
Content-Type: application/json
{
"name": "Frontend Board",
"type": "scrum",
"filterId": 10000, # Filter for project issues
"location": {
"type": "project",
"projectKeyOrId": "PROJECTKEY" # CRITICAL: Associates board with project
}
}
Response:
{
"id": 101,
"name": "Frontend Board",
"type": "scrum"
}
```
**IMPORTANT**: The `location` field is **MANDATORY** to associate the board with a project. Without it, Jira creates the board but leaves it detached, requiring manual connection via the UI.
**Filter creation** (required for board):
```bash
POST /rest/api/3/filter
Content-Type: application/json
{
"name": "PROJECTKEY Issues",
"jql": "project = PROJECTKEY"
}
Response:
{
"id": 10000
}
```
## Configuration Examples
### Example 1: All Names (Create Boards)
**Before** (`.env`):
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=Frontend,Backend,QA,DevOps
```
**After validation**:
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=101,102,103,104
```
**What happened**:
- Detected non-numeric values (names)
- Created 4 boards in Jira
- Updated .env with actual board IDs
### Example 2: All IDs (Validate Existing)
**Before** (`.env`):
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=1,2,3
```
**After validation**:
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=1,2,3
```
**What happened**:
- Detected numeric values (IDs)
- Validated all boards exist
- No changes needed
### Example 3: Mixed IDs and Names (SMART!)
**Before** (`.env`):
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=101,102,QA,Dashboard
```
**After validation**:
```bash
JIRA_PROJECT=PROJ
JIRA_BOARDS=101,102,103,104
```
**What happened**:
- Validated boards 101, 102 exist
- Created "QA" board (got ID 103)
- Created "Dashboard" board (got ID 104)
- Updated .env with ALL board IDs
- **This is the key feature** - you can mix existing IDs with new board names!
### Example 4: Fix Invalid Project
**Before** (`.env`):
```bash
JIRA_PROJECT=NONEXISTENT
JIRA_BOARDS=1,2
```
**After validation** (user selected existing project):
```bash
JIRA_PROJECT=EXISTINGPROJ
JIRA_BOARDS=1,2
```
**What happened**:
- Project NONEXISTENT not found
- User selected EXISTINGPROJ from list
- Updated .env with correct project key
## Error Handling
### Error 1: Invalid Credentials
**Symptom**: API calls fail with 401 Unauthorized
**Solution**:
```
❌ Jira API authentication failed
Please check:
1. JIRA_API_TOKEN is correct
2. JIRA_EMAIL matches your Jira account
3. JIRA_DOMAIN is correct (yourcompany.atlassian.net)
Generate new token at:
https://id.atlassian.com/manage-profile/security/api-tokens
```
### Error 2: Insufficient Permissions
**Symptom**: Cannot create projects/boards (403 Forbidden)
**Solution**:
```
❌ Insufficient permissions to create resources
You need:
- Project Creator permission (for projects)
- Board Creator permission (for boards)
Contact your Jira administrator to request permissions.
```
### Error 3: Project Key Already Taken
**Symptom**: Project creation fails (key exists)
**Solution**:
```
❌ Project key "PROJ" already exists
Options:
1. Use a different project key
2. Select the existing project
3. Cancel
Your choice [2]:
```
### Error 4: Network/API Errors
**Symptom**: API calls timeout or fail
**Solution**:
```
❌ Jira API error: Request timeout
Please check:
1. Internet connection
2. Jira domain is correct
3. Jira is not down (check status.atlassian.com)
Retry? [Y/n]:
```
## Integration with SpecWeave Workflow
### Automatic Validation
When using `/specweave-jira:sync`, validation runs automatically:
```bash
/specweave-jira:sync 0014
# Internally calls:
1. validateJiraResources()
2. Fix missing project/boards
3. Proceed with sync
```
### Manual Validation
Run validation independently:
```bash
# Via skill
"Validate my Jira configuration"
# Via TypeScript
npx tsx src/utils/external-resource-validator.ts
# Via CLI (future)
specweave validate-jira
```
## Best Practices
**Use board names for initial setup**:
```bash
JIRA_BOARDS=Sprint-1,Sprint-2,Backlog
```
- System creates boards automatically
- Updates .env with IDs
- One-time setup, then use IDs
**Use board IDs after creation**:
```bash
JIRA_BOARDS=101,102,103
```
- Faster validation (no creation needed)
- More reliable (IDs don't change)
**Keep .env in version control** (gitignored tokens):
```bash
# Commit project/board structure
JIRA_PROJECT=PROJ
JIRA_BOARDS=101,102,103
# Don't commit sensitive data
JIRA_API_TOKEN=<redacted>
JIRA_EMAIL=<redacted>
```
**Document board mapping** (in README):
```markdown
## Jira Boards
- Board 101: Frontend Team
- Board 102: Backend Team
- Board 103: QA Team
```
## Summary
This skill ensures your Jira configuration is **always valid** by:
1.**Validating projects** - Check if project exists, prompt to select or create
2.**Validating boards** - Check if boards exist (IDs) or create them (names)
3.**Auto-updating .env** - Replace board names with IDs after creation
4.**Clear error messages** - Actionable guidance for all failures
5.**Non-blocking** - Graceful degradation with manual fallback
**Result**: Zero manual Jira setup - system handles everything!
---
**Skill Version**: 1.0.0
**Introduced**: SpecWeave v0.9.5
**Last Updated**: 2025-11-09

3
skills/jira-sync/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
test-results/
*.log
.DS_Store

291
skills/jira-sync/README.md Normal file
View File

@@ -0,0 +1,291 @@
# jira-sync Skill
**Status**: To be developed
**Priority**: Medium
## Purpose
Bidirectional sync between SpecWeave increments and JIRA (Atlassian)
**Note**: This skill handles ONLY JIRA. For Azure DevOps, see `ado-sync` skill.
## Features
### Export to JIRA
- Create JIRA issues from SpecWeave increments
- Map spec.md user stories → JIRA Stories
- Map tasks.md tasks → JIRA Sub-tasks
- Create Epics if specified in spec.md
- Set priorities, labels, components
### Import from JIRA
- Sync JIRA updates back to SpecWeave
- Import existing JIRA issues as increments
- Update status, assignees, comments
### Bidirectional Sync
- Keep status in sync (To Do, In Progress, Done)
- Sync descriptions and acceptance criteria
- Sync comments
- Handle conflicts intelligently
## JIRA-Specific Concepts
### Mapping: SpecWeave → JIRA
| SpecWeave | JIRA |
|-----------|------|
| spec.md (with Epic) | Epic |
| spec.md User Story | Story |
| tasks.md Task | Sub-task |
| Acceptance Tests (spec.md) | Acceptance Criteria (Story) |
| Acceptance Criteria (tasks.md) | Sub-task checklist |
| Status: planned | To Do |
| Status: in-progress | In Progress |
| Status: completed | Done |
### JIRA Structure Example
**spec.md with JIRA structure**:
```markdown
---
increment: 002-payment-processing
status: planned
structure: jira
jira_epic: PROJ-123
---
## Epic: E-commerce Infrastructure
**JIRA**: PROJ-123
### Story: Subscribe to Plan
**JIRA**: PROJ-124
**Priority**: P1
**Labels**: payments, stripe
**Components**: Backend, Frontend
**Description**:
As a user, I want to subscribe to a monthly plan...
**Acceptance Criteria**:
- User can select plan
- Payment processed
- Subscription activated
```
**tasks.md creates Sub-tasks**:
```markdown
## Tasks for PROJ-124 (Subscribe to Plan)
### Task T001: Create StripeService
**JIRA**: PROJ-125 (Sub-task of PROJ-124)
**Agent**: nodejs-backend
**Description**: Create Stripe service class...
**Acceptance Criteria**:
- [ ] StripeService class exists
- [ ] Unit tests passing
```
## Authentication
**JIRA Cloud**:
**JIRA Server/Data Center**:
```yaml
jira_sync:
type: "server"
url: "https://jira.your-company.com"
username: "user"
password: "${JIRA_PASSWORD}" # From environment variable
project_key: "PROJ"
```
## Configuration
## Workflow
### Export Workflow (SpecWeave → JIRA)
```
User: Creates increment in SpecWeave
.specweave/increments/0002-payment/
spec.md (with structure: jira)
tasks.md
↓ jira-sync detects new increment
Creates in JIRA:
Epic: PROJ-123 "E-commerce Infrastructure"
Story: PROJ-124 "Subscribe to Plan"
Sub-task: PROJ-125 "Create StripeService"
Sub-task: PROJ-126 "Create API endpoints"
Links created:
spec.md → PROJ-124
tasks.md T001 → PROJ-125
tasks.md T002 → PROJ-126
```
### Import Workflow (JIRA → SpecWeave)
```
User: Updates JIRA issue status to "In Progress"
↓ JIRA webhook triggers
jira-sync:
Detects change to PROJ-124
Finds linked increment: 002-payment
Updates: .specweave/increments/0002-payment/spec.md
status: planned → in-progress
```
### Bidirectional Sync
```
User: Checks off task in tasks.md
- [x] T001: Create StripeService
↓ jira-sync detects change
Updates JIRA:
PROJ-125 status → Done
User: Changes PROJ-124 to "Done" in JIRA
↓ JIRA webhook triggers
jira-sync updates SpecWeave:
.specweave/increments/0002-payment/spec.md
status: in-progress → completed
```
## API Integration
### JIRA REST API Endpoints Used
```typescript
// Create Epic
POST /rest/api/3/issue
{
"fields": {
"project": { "key": "PROJ" },
"issuetype": { "name": "Epic" },
"summary": "E-commerce Infrastructure",
"customfield_10011": "epic-name" // Epic Name field
}
}
// Create Story (linked to Epic)
POST /rest/api/3/issue
{
"fields": {
"project": { "key": "PROJ" },
"issuetype": { "name": "Story" },
"summary": "Subscribe to Plan",
"parent": { "key": "PROJ-123" } // Link to Epic
}
}
// Create Sub-task
POST /rest/api/3/issue
{
"fields": {
"project": { "key": "PROJ" },
"issuetype": { "name": "Sub-task" },
"parent": { "key": "PROJ-124" },
"summary": "Create StripeService"
}
}
// Update status
POST /rest/api/3/issue/{issueKey}/transitions
{
"transition": { "id": "31" } // "In Progress"
}
```
## Webhooks
### Setup JIRA Webhook
1. Go to JIRA Settings → System → Webhooks
2. Create webhook:
- URL: `https://your-app.com/api/webhooks/jira`
- Events: Issue created, updated, deleted
- Secret: Random string (store in JIRA_WEBHOOK_SECRET)
### Webhook Handler
```typescript
// Receives JIRA webhook
POST /api/webhooks/jira
// jira-sync processes:
1. Verify webhook signature
2. Extract issue data
3. Find linked SpecWeave increment
4. Update spec.md or tasks.md
5. Commit changes (optional)
```
## Conflict Resolution
**Scenario**: Both SpecWeave and JIRA updated simultaneously
**Strategy**:
1. **Timestamp-based**: Latest change wins
2. **User prompt**: Ask user which to keep
3. **Merge**: Combine changes if possible
**Example**:
```
SpecWeave: status → in-progress (10:00 AM)
JIRA: status → done (10:05 AM)
jira-sync:
Latest is JIRA (10:05 AM)
Update SpecWeave → done
```
## Error Handling
**Common errors**:
- JIRA API rate limits → Retry with exponential backoff
- Authentication failed → Notify user, check credentials
- Issue not found → Create if export, skip if import
- Network errors → Queue for retry
## Testing
**Test scenarios**:
1. Create increment → Creates JIRA issues
2. Update JIRA → Updates SpecWeave
3. Update SpecWeave → Updates JIRA
4. Conflict resolution
5. Webhook handling
6. Error recovery
## Integration with Other Skills
- **task-builder**: Reads JIRA structure from spec.md
- **increment-planner**: Can specify structure: jira
## Future Enhancements
- Support for JIRA sprints/iterations
- Sync custom fields
- Attachment sync
- Advanced filtering (which issues to sync)
- Bulk import from JIRA
---
**To implement**: See task in .specweave/increments/
**See also**: `ado-sync` skill for Azure DevOps integration

226
skills/jira-sync/SKILL.md Normal file
View File

@@ -0,0 +1,226 @@
---
name: jira-sync
description: Sync SpecWeave increments with JIRA epics/stories. Content flows SpecWeave→JIRA, status flows JIRA→SpecWeave. Activates ONLY when user asks questions about JIRA integration or needs help configuring JIRA sync. Does NOT activate for slash commands. For syncing, use /specweave-jira:sync command instead. Coordinates with specweave-jira-mapper agent.
allowed-tools: Read, Write, Edit, Task, Bash
---
# JIRA Sync Skill
Coordinates JIRA synchronization by delegating to `specweave-jira-mapper` agent.
**Sync Behavior**: Content (specs, tasks) syncs SpecWeave → JIRA. Status (open/closed) syncs JIRA → SpecWeave.
**⚠️ IMPORTANT**: This skill provides HELP and GUIDANCE about JIRA sync. For actual syncing, users should use the `/specweave-jira:sync` command directly. This skill should NOT auto-activate when the command is being invoked.
## When to Activate
**Do activate when**:
- User asks: "How do I set up JIRA sync?"
- User asks: "What JIRA credentials do I need?"
- User asks: "How does JIRA sync work?"
- User needs help configuring JIRA integration
**Do NOT activate when**:
- User invokes `/specweave-jira:sync` command (command handles it)
- Command is already running (avoid duplicate invocation)
- Task completion hook is syncing (automatic process)
## Responsibilities
1. Answer questions about JIRA sync configuration
2. Help validate prerequisites (JIRA credentials, increment structure)
3. Explain sync directions: content (SpecWeave→JIRA), status (JIRA→SpecWeave)
4. Provide troubleshooting guidance
---
## ⚠️ CRITICAL: Secrets Required (MANDATORY CHECK)
**BEFORE attempting JIRA sync, CHECK for JIRA credentials.**
### Step 1: Check If Credentials Exist
```bash
# Check .env file for both required credentials
if [ -f .env ] && grep -q "JIRA_API_TOKEN" .env && grep -q "JIRA_EMAIL" .env; then
echo "✅ JIRA credentials found"
else
# Credentials NOT found - STOP and prompt user
fi
```
### Step 2: If Credentials Missing, STOP and Show This Message
```
🔐 **JIRA API Token and Email Required**
I need your JIRA API token and email to sync with JIRA.
**How to get it**:
1. Go to: https://id.atlassian.com/manage-profile/security/api-tokens
2. Log in with your Atlassian account
3. Click "Create API token"
4. Give it a label (e.g., "specweave-sync")
5. Click "Create"
6. **Copy the token immediately** (you can't see it again!)
**Where I'll save it**:
- File: `.env` (gitignored, secure)
- Format:
```
JIRA_API_TOKEN=your-jira-api-token-here
JIRA_EMAIL=your-email@example.com
JIRA_DOMAIN=your-domain.atlassian.net
```
**Security**:
✅ .env is in .gitignore (never committed to git)
✅ Token is random alphanumeric string (variable length)
✅ Stored locally only (not in source code)
Please provide:
1. Your JIRA API token:
2. Your JIRA email:
3. Your JIRA domain (e.g., company.atlassian.net):
```
### Step 3: Validate Credentials Format
```bash
# Validate email format
if [[ ! "$JIRA_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "⚠️ Warning: Email format unexpected"
echo "Expected: valid email address"
echo "Got: $JIRA_EMAIL"
fi
# Validate domain format
if [[ ! "$JIRA_DOMAIN" =~ \.atlassian\.net$ ]]; then
echo "⚠️ Warning: Domain format unexpected"
echo "Expected: *.atlassian.net"
echo "Got: $JIRA_DOMAIN"
echo "Note: Self-hosted JIRA may have different domain format"
fi
# Token validation (just check it's not empty)
if [ -z "$JIRA_API_TOKEN" ]; then
echo "❌ Error: JIRA API token is empty"
exit 1
fi
```
### Step 4: Save Credentials Securely
```bash
# Save to .env
cat >> .env << EOF
JIRA_API_TOKEN=$JIRA_API_TOKEN
JIRA_EMAIL=$JIRA_EMAIL
JIRA_DOMAIN=$JIRA_DOMAIN
EOF
# Ensure .env is gitignored
if ! grep -q "^\\.env$" .gitignore; then
echo ".env" >> .gitignore
fi
# Create .env.example for team
cat > .env.example << 'EOF'
# JIRA API Token
# Get from: https://id.atlassian.com/manage-profile/security/api-tokens
JIRA_API_TOKEN=your-jira-api-token
JIRA_EMAIL=your-email@example.com
JIRA_DOMAIN=your-domain.atlassian.net
EOF
echo "✅ Credentials saved to .env (gitignored)"
echo "✅ Created .env.example for team (commit this)"
```
### Step 5: Use Credentials in Sync
```bash
# Export for JIRA API calls
export JIRA_API_TOKEN=$(grep JIRA_API_TOKEN .env | cut -d '=' -f2)
export JIRA_EMAIL=$(grep JIRA_EMAIL .env | cut -d '=' -f2)
export JIRA_DOMAIN=$(grep JIRA_DOMAIN .env | cut -d '=' -f2)
# Create Basic Auth header (JIRA uses email:token)
AUTH=$(echo -n "$JIRA_EMAIL:$JIRA_API_TOKEN" | base64)
# Use in JIRA API calls
curl -H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
https://$JIRA_DOMAIN/rest/api/3/issue/PROJ-123
```
### Step 6: Never Log Secrets
```bash
# ❌ WRONG - Logs secret
echo "Using token: $JIRA_API_TOKEN"
# ✅ CORRECT - Masks secret
echo "Using JIRA credentials (token present: ✅, email: $JIRA_EMAIL)"
```
### Step 7: Error Handling
```bash
# If API call fails with 401 Unauthorized
if [ $? -eq 401 ]; then
echo "❌ JIRA credentials invalid"
echo ""
echo "Possible causes:"
echo "1. API token expired or revoked"
echo "2. Email address incorrect"
echo "3. Domain incorrect (check: $JIRA_DOMAIN)"
echo "4. Account lacks permissions (need: project admin or issue create/edit)"
echo ""
echo "Please verify credentials:"
echo "https://id.atlassian.com/manage-profile/security/api-tokens"
fi
# If API call fails with 403 Forbidden
if [ $? -eq 403 ]; then
echo "❌ JIRA permission denied"
echo ""
echo "Your account lacks permissions for this operation."
echo "Required permissions:"
echo "- Browse projects"
echo "- Create issues"
echo "- Edit issues"
echo "- Administer projects (for Epic creation)"
echo ""
echo "Contact your JIRA administrator."
fi
```
### Step 8: Production Recommendations
**For production deployments, use OAuth 2.0** instead of API tokens:
**Why OAuth 2.0?**
- ✅ More secure (no long-lived credentials)
- ✅ Fine-grained permissions (scopes)
- ✅ Automatic token refresh
- ✅ Audit trail in JIRA
**How to set up OAuth 2.0**:
1. Go to: https://developer.atlassian.com/console/myapps/
2. Create a new app
3. Configure OAuth 2.0 credentials
4. Add required scopes (read:jira-work, write:jira-work)
5. Use OAuth flow instead of API token
**For self-hosted JIRA**: Use Personal Access Tokens (PAT) instead of API tokens.
---
## Usage
**Export**: `/sync-jira export 0001`
**Import**: `/sync-jira import PROJ-123`
**Sync**: `/sync-jira sync 0001`
All conversion logic is handled by the `specweave-jira-mapper` agent.

View File

@@ -0,0 +1,500 @@
---
name: specweave-jira-mapper
description: Expert in mapping SpecWeave increments to JIRA epics/stories/subtasks. Content flows SpecWeave→JIRA, status flows JIRA→SpecWeave. Handles export (increment → JIRA), import (JIRA → increment). Activates for JIRA sync, issue creation, import from JIRA.
tools: Read, Write, Edit, Bash
model: claude-sonnet-4-5-20250929
---
# Specweave Jira Mapper Skill
You are an expert in mapping SpecWeave concepts to JIRA and vice versa with precision and traceability.
## Core Responsibilities
1. **Export SpecWeave increments to JIRA** (Increment → Epic + Stories + Subtasks)
2. **Import JIRA epics as SpecWeave increments** (Epic → Increment structure)
3. **Sync**: Content flows SpecWeave→JIRA, status flows JIRA→SpecWeave
4. **Maintain traceability** (store keys, URLs, timestamps)
5. **Validate mapping accuracy** using test cases
6. **Handle edge cases** (missing fields, invalid statuses, API errors)
---
## Concept Mappings
### SpecWeave → JIRA
| SpecWeave Concept | JIRA Concept | Mapping Rules |
|-------------------|--------------|---------------|
| **Increment** | Epic | Title: `[Increment ###] [Title]` |
| **User Story** (from spec.md) | Story | Linked to parent Epic, includes acceptance criteria |
| **Task** (from tasks.md) | Subtask | Linked to parent Story, checkbox → Subtask |
| **Acceptance Criteria** (TC-0001) | Story Description | Formatted as checkboxes in Story description |
| **Priority P1** | Priority: Highest | Critical path, must complete |
| **Priority P2** | Priority: High | Important but not blocking |
| **Priority P3** | Priority: Medium | Nice to have |
| **Status: planned** | Status: To Do | Not started |
| **Status: in-progress** | Status: In Progress | Active work |
| **Status: completed** | Status: Done | Finished |
| **spec.md** | Epic Description | Summary + link to spec (if GitHub repo) |
| **context-manifest.yaml** | Custom Field: Context | Serialized YAML in custom field (optional) |
### JIRA → SpecWeave
| JIRA Concept | SpecWeave Concept | Import Rules |
|--------------|-------------------|--------------|
| **Epic** | Increment | Auto-number next available (e.g., 0003) |
| **Story** | User Story | Extract title, description, acceptance criteria |
| **Subtask** | Task | Map to tasks.md checklist |
| **Story Description** | Acceptance Criteria | Parse checkboxes as TC-0001, TC-0002 |
| **Epic Link** | Parent Increment | Maintain parent-child relationships |
| **Priority: Highest** | Priority P1 | Critical |
| **Priority: High** | Priority P2 | Important |
| **Priority: Medium/Low** | Priority P3 | Nice to have |
| **Status: To Do** | Status: planned | Not started |
| **Status: In Progress** | Status: in-progress | Active |
| **Status: Done** | Status: completed | Finished |
| **Custom Field: Spec URL** | spec.md link | Cross-reference |
---
## Conversion Workflows
### 1. Export: Increment → JIRA Epic
**Input**: `.specweave/increments/0001-feature-name/`
**Prerequisites**:
- Increment folder exists
- `spec.md` exists with valid frontmatter
- `tasks.md` exists
- JIRA connection configured
**Process**:
1. **Read increment files**:
```bash
# Read spec.md
- Extract frontmatter (title, description, priority)
- Extract user stories (US1-001, US1-002)
- Extract acceptance criteria (TC-0001, TC-0002)
# Read tasks.md
- Extract task checklist
- Group tasks by user story (if structured)
```
2. **Create JIRA Epic**:
```
Title: [Increment 0001] Feature Name
Description:
{spec.md summary}
Specification: {link to spec.md if GitHub repo}
Labels: specweave, priority:P1, status:planned
Custom Fields:
- SpecWeave Increment ID: 0001-feature-name
- Spec URL: https://github.com/user/repo/blob/main/.specweave/increments/0001-feature-name/spec.md
```
3. **Create JIRA Stories** (one per user story):
```
Title: {User Story title}
Description:
**As a** {role}
**I want to** {goal}
**So that** {benefit}
**Acceptance Criteria**:
- [ ] TC-0001: {criteria}
- [ ] TC-0002: {criteria}
Epic Link: {Epic Key}
Labels: specweave, user-story
```
4. **Create JIRA Subtasks** (from tasks.md):
```
Title: {Task description}
Parent: {Story Key}
Labels: specweave, task
```
5. **Update increment frontmatter**:
```yaml
jira:
epic_key: "PROJ-123"
epic_url: "https://jira.company.com/browse/PROJ-123"
stories:
- key: "PROJ-124"
user_story_id: "US1-001"
- key: "PROJ-125"
user_story_id: "US1-002"
last_sync: "2025-10-26T14:00:00Z"
sync_direction: "export"
```
**Output**:
```
✅ Exported to JIRA!
Epic: PROJ-123
URL: https://jira.company.com/browse/PROJ-123
Stories: 5 created (PROJ-124 to PROJ-128)
Subtasks: 12 created
Last Sync: 2025-10-26T14:00:00Z
```
---
### 2. Import: JIRA Epic → Increment
**Input**: JIRA Epic key (e.g., `PROJ-123`)
**Prerequisites**:
- Valid JIRA Epic key
- Epic exists and is accessible
- JIRA connection configured
**Process**:
1. **Fetch Epic details** (via JIRA API/MCP):
```
- Epic title, description, labels
- Epic custom fields (if SpecWeave ID exists)
- Priority, status
```
2. **Fetch linked Stories and Subtasks**:
```
- All Stories linked to Epic
- All Subtasks linked to each Story
- Story descriptions (acceptance criteria)
```
3. **Auto-number next increment**:
```bash
# Scan .specweave/increments/ for highest number
ls .specweave/increments/ | grep -E '^[0-9]{4}' | sort -n | tail -1
# Increment by 1 → 0003
```
4. **Create increment folder**:
```
.specweave/increments/0003-imported-feature/
```
5. **Generate spec.md**:
```yaml
---
increment_id: "0003"
title: "{Epic title}"
status: "{mapped from JIRA status}"
priority: "{mapped from JIRA priority}"
created_at: "{Epic created date}"
jira:
epic_key: "PROJ-123"
epic_url: "https://jira.company.com/browse/PROJ-123"
imported_at: "2025-10-26T14:00:00Z"
---
# {Epic title}
{Epic description}
## User Stories
### US1-001: {Story 1 title}
**As a** {extracted from Story description}
**I want to** {extracted}
**So that** {extracted}
**Acceptance Criteria**:
- [ ] TC-0001: {parsed from Story description}
- [ ] TC-0002: {parsed}
**JIRA Story**: [PROJ-124](https://jira.company.com/browse/PROJ-124)
```
6. **Generate tasks.md**:
```markdown
# Tasks: {Increment title}
## User Story: US1-001
- [ ] {Subtask 1 title} (JIRA: PROJ-130)
- [ ] {Subtask 2 title} (JIRA: PROJ-131)
## User Story: US1-002
- [ ] {Subtask 3 title} (JIRA: PROJ-132)
```
7. **Generate context-manifest.yaml** (default):
```yaml
---
spec_sections: []
documentation: []
max_context_tokens: 10000
priority: high
auto_refresh: false
---
```
8. **Update JIRA Epic** (add custom field if available):
```
Custom Field: SpecWeave Increment ID = 0003-imported-feature
```
**Output**:
```
✅ Imported from JIRA!
Increment: 0003-imported-feature
Location: .specweave/increments/0003-imported-feature/
User Stories: 5 imported
Tasks: 12 imported
JIRA Epic: PROJ-123
```
---
### 3. Bidirectional Sync
**Trigger**: Manual (`/sync-jira`) or webhook
**Prerequisites**:
- Increment has JIRA metadata in frontmatter
- JIRA Epic/Stories exist
- Last sync timestamp available
**Process**:
1. **Detect changes since last sync**:
```
SpecWeave changes:
- spec.md modified after last_sync
- tasks.md modified after last_sync
- Task checkboxes changed
JIRA changes:
- Epic/Story/Subtask updated after last_sync
- Status changes
- New comments
```
2. **Compare and detect conflicts**:
```
Conflict types:
- Title changed in both (SpecWeave + JIRA)
- Task marked done in SpecWeave, but JIRA Subtask still "In Progress"
- Priority changed in both
```
3. **Present conflicts to user**:
```
⚠️ Sync Conflicts Detected:
1. Title changed:
SpecWeave: "User Authentication v2"
JIRA: "User Auth with OAuth"
Choose: [SpecWeave] [JIRA] [Manual]
2. Task status mismatch:
Task: "Implement login endpoint"
SpecWeave: ✅ completed
JIRA Subtask: In Progress
Choose: [Mark JIRA Done] [Uncheck SpecWeave] [Manual]
```
4. **Apply sync**:
```
SpecWeave → JIRA:
- Update Epic/Story titles
- Update Subtask statuses (checkbox → JIRA status)
- Add comments for significant changes
JIRA → SpecWeave:
- Update spec.md frontmatter (status, priority)
- Update task checkboxes (JIRA Subtask status → checkbox)
- Log JIRA comments to increment logs/
```
5. **Update sync timestamps**:
```yaml
jira:
last_sync: "2025-10-26T16:30:00Z"
sync_direction: "two-way"
conflicts_resolved: 2
```
**Output**:
```
✅ Synced with JIRA!
Direction: Two-way
Changes Applied:
- SpecWeave → JIRA: 3 updates
- JIRA → SpecWeave: 5 updates
Conflicts Resolved: 2 (user decisions)
Last Sync: 2025-10-26T16:30:00Z
```
---
## Edge Cases and Error Handling
### Missing Fields
**Problem**: Increment missing spec.md or JIRA Epic missing required fields
**Solution**:
```
❌ Error: spec.md not found in increment 0001-feature-name
Expected: .specweave/increments/0001-feature-name/spec.md
Please create spec.md before exporting to JIRA.
```
### JIRA API Errors
**Problem**: JIRA API rate limit, authentication failure, network error
**Solution**:
```
❌ JIRA API Error: Rate limit exceeded (429)
Retry in: 60 seconds
Alternative: Export to JSON and manually import to JIRA later.
```
### Invalid Status Mapping
**Problem**: JIRA uses custom workflow statuses not in standard mapping
**Solution**:
```
⚠️ Unknown JIRA status: "Awaiting Review"
Available mappings:
- To Do → planned
- In Progress → in-progress
- Done → completed
Map "Awaiting Review" to: [planned] [in-progress] [completed] [Custom]
```
### Conflict Resolution
**Problem**: Same field changed in both SpecWeave and JIRA
**Solution**:
- Always ask user for resolution
- Provide diff view
- Offer merge options
- Never auto-resolve conflicts silently
---
## Templates
Use templates in `templates/` folder for consistent outputs:
- `epic-from-increment.md` - Epic creation template
- `story-from-user-story.md` - Story creation template
- `increment-from-epic.md` - Increment import template
---
## References
Consult references in `references/` folder:
- `jira-concepts.md` - JIRA terminology (Epic, Story, Subtask, Epic Link)
- `specweave-concepts.md` - SpecWeave structure (Increment, User Story, Task)
- `mapping-examples.md` - Real-world conversion examples
---
## Test Cases
Validate all conversions using test cases in `test-cases/`:
1. **test-1-increment-to-epic.yaml** - Basic export (Increment → Epic)
2. **test-2-epic-to-increment.yaml** - Basic import (Epic → Increment)
3. **test-3-bidirectional-sync.yaml** - Sync with conflict resolution
**Run tests**:
```bash
npm run test:agents:specweave-jira-mapper
```
---
## Best Practices
1. **Always validate before sync** - Check increment structure, JIRA connection
2. **Preserve traceability** - Store JIRA keys in frontmatter, SpecWeave IDs in JIRA
3. **Ask before overwriting** - Never auto-resolve conflicts
4. **Log all operations** - Write sync logs to `.specweave/increments/{id}/logs/jira-sync.log`
5. **Handle errors gracefully** - Provide actionable error messages
6. **Test mappings** - Use test cases to validate accuracy
---
## Usage Examples
### Export to JIRA
```
User: "Export increment 0001 to JIRA"
You:
1. Read .specweave/increments/0001-*/spec.md and tasks.md
2. Extract user stories and tasks
3. Create JIRA Epic with title "[Increment 0001] {title}"
4. Create Stories for each user story
5. Create Subtasks for each task
6. Update increment frontmatter with JIRA keys
7. Present summary with Epic URL
```
### Import from JIRA
```
User: "Import JIRA epic PROJ-123"
You:
1. Fetch Epic PROJ-123 via JIRA API
2. Fetch linked Stories and Subtasks
3. Auto-number next increment (e.g., 0003)
4. Generate spec.md with user stories
5. Generate tasks.md with subtasks
6. Generate context-manifest.yaml (default)
7. Present summary with increment location
```
### Bidirectional Sync
```
User: "Sync increment 0001 with JIRA"
You:
1. Read increment frontmatter for JIRA keys
2. Detect changes since last_sync
3. Compare SpecWeave vs JIRA
4. Present conflicts (if any) for user resolution
5. Apply sync (SpecWeave ↔ JIRA)
6. Update sync timestamps
7. Present summary with changes applied
```
---
**You are the authoritative mapper between SpecWeave and JIRA. Your conversions must be accurate, traceable, and reversible.**