From 93c16ce8f26ca666ef00e6d294b14363f7248f4a Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 17:56:44 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 21 + README.md | 3 + agents/jira-manager/AGENT.md | 416 +++++++++++++ commands/import-projects.js | 181 ++++++ commands/import-projects.md | 97 +++ commands/import-projects.ts | 286 +++++++++ commands/refresh-cache.js | 25 + commands/refresh-cache.ts | 40 ++ commands/specweave-jira-import-boards.md | 331 ++++++++++ commands/specweave-jira-import-projects.md | 298 +++++++++ commands/specweave-jira-sync.md | 240 ++++++++ hooks/README.md | 201 ++++++ hooks/post-task-completion.sh | 180 ++++++ plugin.lock.json | 105 ++++ skills/jira-resource-validator/SKILL.md | 677 +++++++++++++++++++++ skills/jira-sync/.gitignore | 3 + skills/jira-sync/README.md | 291 +++++++++ skills/jira-sync/SKILL.md | 226 +++++++ skills/specweave-jira-mapper/SKILL.md | 500 +++++++++++++++ 19 files changed, 4121 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 agents/jira-manager/AGENT.md create mode 100644 commands/import-projects.js create mode 100644 commands/import-projects.md create mode 100644 commands/import-projects.ts create mode 100755 commands/refresh-cache.js create mode 100644 commands/refresh-cache.ts create mode 100644 commands/specweave-jira-import-boards.md create mode 100644 commands/specweave-jira-import-projects.md create mode 100644 commands/specweave-jira-sync.md create mode 100644 hooks/README.md create mode 100755 hooks/post-task-completion.sh create mode 100644 plugin.lock.json create mode 100644 skills/jira-resource-validator/SKILL.md create mode 100644 skills/jira-sync/.gitignore create mode 100644 skills/jira-sync/README.md create mode 100644 skills/jira-sync/SKILL.md create mode 100644 skills/specweave-jira-mapper/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c03cbd1 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3f8905 --- /dev/null +++ b/README.md @@ -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. diff --git a/agents/jira-manager/AGENT.md b/agents/jira-manager/AGENT.md new file mode 100644 index 0000000..69b5adf --- /dev/null +++ b/agents/jira-manager/AGENT.md @@ -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 diff --git a/commands/import-projects.js b/commands/import-projects.js new file mode 100644 index 0000000..51f9675 --- /dev/null +++ b/commands/import-projects.js @@ -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 +}; diff --git a/commands/import-projects.md b/commands/import-projects.md new file mode 100644 index 0000000..1c79df9 --- /dev/null +++ b/commands/import-projects.md @@ -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 ` - Filter by status (active, archived, all) +- `--type ` - Filter by project type (software, business, service_desk) +- `--lead ` - Filter by project lead +- `--jql ` - Custom JQL filter +- `--preset ` - 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. diff --git a/commands/import-projects.ts b/commands/import-projects.ts new file mode 100644 index 0000000..53ea9e2 --- /dev/null +++ b/commands/import-projects.ts @@ -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 { + 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 { + 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 { + 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 { + // 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); + }); +} diff --git a/commands/refresh-cache.js b/commands/refresh-cache.js new file mode 100755 index 0000000..d7f71ed --- /dev/null +++ b/commands/refresh-cache.js @@ -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 +}; diff --git a/commands/refresh-cache.ts b/commands/refresh-cache.ts new file mode 100644 index 0000000..80ead64 --- /dev/null +++ b/commands/refresh-cache.ts @@ -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 { + 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); +} diff --git a/commands/specweave-jira-import-boards.md b/commands/specweave-jira-import-boards.md new file mode 100644 index 0000000..c06bff3 --- /dev/null +++ b/commands/specweave-jira-import-boards.md @@ -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 diff --git a/commands/specweave-jira-import-projects.md b/commands/specweave-jira-import-projects.md new file mode 100644 index 0000000..afd87dc --- /dev/null +++ b/commands/specweave-jira-import-projects.md @@ -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. diff --git a/commands/specweave-jira-sync.md b/commands/specweave-jira-sync.md new file mode 100644 index 0000000..1ee2e96 --- /dev/null +++ b/commands/specweave-jira-sync.md @@ -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. diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..f6e0b58 --- /dev/null +++ b/hooks/README.md @@ -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 diff --git a/hooks/post-task-completion.sh b/hooks/post-task-completion.sh new file mode 100755 index 0000000..3cde33a --- /dev/null +++ b/hooks/post-task-completion.sh @@ -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 <> "$DEBUG_LOG" 2>/dev/null || true + cat < /dev/null; then + echo "[$(date)] [JIRA] ⚠️ jq not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true + cat </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 < /dev/null; then + echo "[$(date)] [JIRA] ⚠️ Node.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true + cat <> "$DEBUG_LOG" 2>/dev/null || true + cat <> "$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 < 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= +JIRA_EMAIL= +``` + +βœ… **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 diff --git a/skills/jira-sync/.gitignore b/skills/jira-sync/.gitignore new file mode 100644 index 0000000..a3a8cb2 --- /dev/null +++ b/skills/jira-sync/.gitignore @@ -0,0 +1,3 @@ +test-results/ +*.log +.DS_Store diff --git a/skills/jira-sync/README.md b/skills/jira-sync/README.md new file mode 100644 index 0000000..5f3acf3 --- /dev/null +++ b/skills/jira-sync/README.md @@ -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 diff --git a/skills/jira-sync/SKILL.md b/skills/jira-sync/SKILL.md new file mode 100644 index 0000000..7d43ee3 --- /dev/null +++ b/skills/jira-sync/SKILL.md @@ -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. diff --git a/skills/specweave-jira-mapper/SKILL.md b/skills/specweave-jira-mapper/SKILL.md new file mode 100644 index 0000000..f4676d4 --- /dev/null +++ b/skills/specweave-jira-mapper/SKILL.md @@ -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.**