sdd-pipeline 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +376 -95
  2. package/bin/sdd.js +14 -6
  3. package/lib/bmad/agents/architect.js +96 -0
  4. package/lib/bmad/agents/pm.js +67 -0
  5. package/lib/bmad/agents/ux.js +103 -0
  6. package/lib/bmad/confidence.js +129 -0
  7. package/lib/bmad/index.js +140 -0
  8. package/lib/bmad/problem-first.js +85 -0
  9. package/lib/bmad/questions.js +110 -0
  10. package/lib/bmad/synthesis.js +109 -0
  11. package/lib/bmad.js +45 -51
  12. package/lib/bundle/.claude/commands/sdd-bmad.md +360 -20
  13. package/lib/bundle/.claude/commands/sdd-converge.md +21 -7
  14. package/lib/bundle/.claude/commands/sdd-cook.md +335 -0
  15. package/lib/bundle/.claude/commands/sdd-init.md +71 -13
  16. package/lib/bundle/.claude/commands/sdd-spec.md +103 -35
  17. package/lib/bundle/.claude/commands/sdd-task-status.md +95 -0
  18. package/lib/bundle/.claude/commands/sdd-tasks.md +100 -17
  19. package/lib/bundle/.claude/commands/sdd.md +48 -8
  20. package/lib/bundle/.sdd/config.json +2 -2
  21. package/lib/bundle/CLAUDE.md +6 -4
  22. package/lib/bundle/commands/sdd-status.ps1 +40 -6
  23. package/lib/bundle/commands/sdd-task-status.ps1 +78 -0
  24. package/lib/bundle/run-converge.ps1 +84 -23
  25. package/lib/bundle/skills/sdd-cook/SKILL.md +381 -0
  26. package/lib/bundle/skills/sdd-cook/references/intent-detection.md +67 -0
  27. package/lib/bundle/skills/sdd-cook/references/subagent-patterns.md +352 -0
  28. package/lib/bundle/skills/sdd-cook/references/subagent-prompts.md +346 -0
  29. package/lib/bundle/skills/sdd-cook/references/task-execution.md +259 -0
  30. package/lib/bundle/skills/sdd-cook/references/workflow-steps.md +215 -0
  31. package/lib/bundle/templates/BMAD-brief.template.md +75 -14
  32. package/lib/bundle/templates/SPEC.template.md +193 -20
  33. package/lib/bundle/templates/TASKS.template.md +32 -13
  34. package/lib/bundle/templates/task-detail.template.md +40 -0
  35. package/lib/bundle/templates/task-master.template.md +41 -0
  36. package/lib/converge-task.ps1 +260 -0
  37. package/lib/init.js +2 -0
  38. package/lib/output-dirs.ps1 +461 -0
  39. package/lib/sdd-check.ps1 +130 -0
  40. package/lib/shared-converge.ps1 +270 -0
  41. package/lib/verify-patterns.ps1 +208 -0
  42. package/package.json +2 -2
@@ -0,0 +1,103 @@
1
+ /**
2
+ * BMAD UX Agent — UX Designer perspective
3
+ * Analyzes from a user experience, accessibility, and microcopy lens.
4
+ */
5
+
6
+ /**
7
+ * @param {object} state - BMAD interview state
8
+ * @returns {{ section: string, confidence: string }}
9
+ */
10
+ function analyze(state) {
11
+ const input = state.input || ''
12
+ const answers = state.answers || {}
13
+
14
+ // UX key questions: q7, q8, q9
15
+ const q7 = answers.q7 || ''
16
+ const q8 = answers.q8 || ''
17
+ const q9 = answers.q9 || ''
18
+
19
+ const firstImpression = q7 || inferFirstImpression(input)
20
+ const microcopy = parseMicrocopy(q8)
21
+ const errorStates = parseErrorStates(q9)
22
+
23
+ const section = `## UX Perspective
24
+ **First impression goal:** ${firstImpression}
25
+
26
+ **Essential microcopy:**
27
+ ${microcopy}
28
+
29
+ **Error states to handle gracefully:**
30
+ ${errorStates}
31
+
32
+ **Accessibility requirements:**
33
+ - WCAG 2.1 AA compliance
34
+ - Keyboard navigable
35
+ - Screen reader labels on all interactive elements
36
+
37
+ **Delight factors (if time allows):**
38
+ - ${inferDelight(input)}
39
+ `
40
+
41
+ const confidence = inferConfidence(q7, q8, q9)
42
+ return { section, confidence }
43
+ }
44
+
45
+ function inferFirstImpression(input) {
46
+ if (!input) return '[describe what the user sees and can do in the first 30 seconds]'
47
+ const lower = input.toLowerCase()
48
+ if (lower.includes('landing page')) return 'Professional, credible, immediately clear value proposition above the fold'
49
+ if (lower.includes('dashboard')) return 'Clean overview, key metric visible, clear navigation to primary action'
50
+ if (lower.includes('form')) return 'Clear field labels, visible submit button, no distraction from the task'
51
+ if (lower.includes('notification')) return 'Brief, actionable, dismissible without friction'
52
+ return `User sees: ${input.slice(0, 100)}. User can: [primary action]. No confusion about what to do next.`
53
+ }
54
+
55
+ function parseMicrocopy(q8) {
56
+ if (!q8 || q8.trim().length < 3) {
57
+ return '- [Button labels]\n- [Field labels]\n- [Error messages]\n- [Success messages]\n- [Empty state messages]'
58
+ }
59
+ const items = q8
60
+ .split(/[,;\n]/)
61
+ .filter((l) => l.trim())
62
+ .slice(0, 8)
63
+ .map((l) => `- ${l.trim()}`)
64
+ return items.length > 0 ? items.join('\n') : '- [from Q8]'
65
+ }
66
+
67
+ function parseErrorStates(q9) {
68
+ if (!q9 || q9.trim().length < 5) {
69
+ return '| Scenario | Behavior |\n|----------|---------|\n| Network failure | [retry with exponential backoff, user-friendly message] |\n| Empty data | [helpful empty state with clear CTA] |\n| Permission denied | [clear explanation + next action] |'
70
+ }
71
+ const lines = q9.split(/[,;\n]/).filter((l) => l.trim()).slice(0, 5)
72
+ if (lines.length === 0) {
73
+ return '| Scenario | Behavior |\n|----------|---------|\n| [Error scenario] | [How to handle it] |'
74
+ }
75
+ const rows = lines
76
+ .map((line) => {
77
+ const parts = line.split(/[-:]/).map((p) => p.trim())
78
+ return `| ${parts[0] || 'Scenario'} | ${parts.slice(1).join(': ') || 'Handle gracefully'} |`
79
+ })
80
+ .join('\n')
81
+ return `| Scenario | Behavior |\n|----------|---------|\n${rows}`
82
+ }
83
+
84
+ function inferDelight(input) {
85
+ const lower = (input || '').toLowerCase()
86
+ if (lower.includes('landing page') || lower.includes('marketing'))
87
+ return 'Subtle entrance animations, smooth scroll between sections, trust signals visible'
88
+ if (lower.includes('dashboard'))
89
+ return 'Real-time data updates, keyboard shortcuts for power users, customizable views'
90
+ if (lower.includes('form') || lower.includes('checkout'))
91
+ return 'Progress indicator, inline validation, satisfying submit confirmation'
92
+ return 'Consider: entrance animation, hover state feedback, success celebration moment'
93
+ }
94
+
95
+ function inferConfidence(q7, q8, q9) {
96
+ const filled = [q7, q8, q9].filter((a) => a.trim().length > 10).length
97
+ if (filled >= 3) return 'High'
98
+ if (filled >= 2) return 'Medium'
99
+ if (filled >= 1) return 'Low'
100
+ return 'None'
101
+ }
102
+
103
+ module.exports = { analyze }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * BMAD Confidence Score Calculator
3
+ * Computes a 0-100 score from interview answers and user input.
4
+ * Thresholds: None <20 | Weak <40 | Medium <70 | Strong ≥70
5
+ */
6
+
7
+ const THRESHOLDS = {
8
+ NONE: 20,
9
+ WEAK: 40,
10
+ MEDIUM: 70,
11
+ // Strong: ≥70
12
+ }
13
+
14
+ /**
15
+ * Calculate confidence score from interview state.
16
+ * @param {object} state - BMAD interview state
17
+ * @returns {{ score: number, level: string, factors: object }}
18
+ */
19
+ function calculate(state) {
20
+ let score = 0
21
+ const factors = {}
22
+
23
+ // --- Factor 1: Input quality (0-30 points) ---
24
+ const inputLength = (state.input || '').trim().length
25
+ if (inputLength === 0) {
26
+ factors.input = { points: 0, note: 'No input provided' }
27
+ } else if (inputLength < 20) {
28
+ factors.input = { points: 5, note: 'Very brief input' }
29
+ } else if (inputLength < 80) {
30
+ factors.input = { points: 15, note: 'Brief input — interview fills gaps' }
31
+ } else if (inputLength < 300) {
32
+ factors.input = { points: 25, note: 'Good input detail' }
33
+ } else {
34
+ factors.input = { points: 30, note: 'Detailed input' }
35
+ }
36
+ score += factors.input.points
37
+
38
+ // --- Factor 2: Interview completion (0-30 points) ---
39
+ const totalQuestions = 9
40
+ const answeredCount = Object.keys(state.answers || {}).length
41
+ const completionRatio = answeredCount / totalQuestions
42
+
43
+ if (answeredCount === 0) {
44
+ factors.interview = { points: 0, note: 'No questions answered' }
45
+ } else if (completionRatio < 0.4) {
46
+ factors.interview = { points: 5, note: `${answeredCount}/${totalQuestions} answered` }
47
+ } else if (completionRatio < 0.7) {
48
+ factors.interview = { points: 15, note: `${answeredCount}/${totalQuestions} answered` }
49
+ } else if (completionRatio < 1) {
50
+ factors.interview = { points: 25, note: `${answeredCount}/${totalQuestions} answered` }
51
+ } else {
52
+ factors.interview = { points: 30, note: 'All 9 questions answered' }
53
+ }
54
+ score += factors.interview.points
55
+
56
+ // --- Factor 3: Problem clarity (0-20 points) ---
57
+ const problemAnswer = state.answers?.q1 || state.input || ''
58
+ const hasProblem = (problemAnswer || '').trim().length > 20
59
+ const problemSpecific =
60
+ /\d+%|\$\d+|\d+x/i.test(problemAnswer)
61
+
62
+ if (!hasProblem) {
63
+ factors.problemClarity = { points: 0, note: 'No problem statement' }
64
+ } else if (problemSpecific) {
65
+ factors.problemClarity = { points: 20, note: 'Specific, measurable problem' }
66
+ } else if (hasProblem) {
67
+ factors.problemClarity = { points: 10, note: 'Problem stated but generic' }
68
+ }
69
+ score += factors.problemClarity.points
70
+
71
+ // --- Factor 4: Technical awareness (0-20 points) ---
72
+ const techAnswer = state.answers?.q4 || ''
73
+ const techTerms = /database|api|frontend|backend|auth|sql|json|server|deploy/i.test(
74
+ techAnswer
75
+ )
76
+ const integrationAnswer = state.answers?.q5 || ''
77
+ const hasIntegration = integrationAnswer.trim().length > 10
78
+
79
+ if (!techAnswer && !integrationAnswer) {
80
+ factors.technicalAwareness = { points: 0, note: 'No technical detail' }
81
+ } else if (techTerms && hasIntegration) {
82
+ factors.technicalAwareness = { points: 20, note: 'Good technical awareness' }
83
+ } else if (techTerms || hasIntegration) {
84
+ factors.technicalAwareness = { points: 10, note: 'Some technical detail' }
85
+ } else {
86
+ factors.technicalAwareness = { points: 5, note: 'Minimal technical detail' }
87
+ }
88
+ score += factors.technicalAwareness.points
89
+
90
+ // Cap at 100
91
+ score = Math.min(100, Math.max(0, score))
92
+
93
+ // Determine level
94
+ let level
95
+ if (score < THRESHOLDS.NONE) {
96
+ level = 'None'
97
+ } else if (score < THRESHOLDS.WEAK) {
98
+ level = 'Weak'
99
+ } else if (score < THRESHOLDS.MEDIUM) {
100
+ level = 'Medium'
101
+ } else {
102
+ level = 'Strong'
103
+ }
104
+
105
+ return { score, level, factors }
106
+ }
107
+
108
+ function getLevel(score) {
109
+ if (score < THRESHOLDS.NONE) return 'None'
110
+ if (score < THRESHOLDS.WEAK) return 'Weak'
111
+ if (score < THRESHOLDS.MEDIUM) return 'Medium'
112
+ return 'Strong'
113
+ }
114
+
115
+ function canProceed(score) {
116
+ return score >= THRESHOLDS.NONE
117
+ }
118
+
119
+ function getWarning(score) {
120
+ if (score < THRESHOLDS.WEAK) {
121
+ return '⚠️ Confidence is Weak — consider answering more questions before proceeding.'
122
+ }
123
+ if (score < THRESHOLDS.MEDIUM) {
124
+ return '⚠️ Confidence is Medium — spec may need iteration during implementation.'
125
+ }
126
+ return ''
127
+ }
128
+
129
+ module.exports = { calculate, getLevel, canProceed, getWarning, THRESHOLDS }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * BMAD Orchestrator — lib/bmad/index.js
3
+ * Main entry point for BMAD interactive system.
4
+ *
5
+ * Coordinates:
6
+ * - Problem-first inversion
7
+ * - 9-question interview framework
8
+ * - 3-agent synthesis (single-pass LLM mode)
9
+ * - Confidence scoring
10
+ * - BMAD-brief.md output
11
+ */
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+
16
+ const questions = require('./questions')
17
+ const problemFirst = require('./problem-first')
18
+ const synthesis = require('./synthesis')
19
+ const confidenceCalc = require('./confidence')
20
+
21
+ const OUTPUT_FILE = 'BMAD-brief.md'
22
+ const TEMPLATE_DATE = new Date().toISOString().split('T')[0]
23
+ const TEMPLATE_EXPIRY = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
24
+ .toISOString()
25
+ .split('T')[0]
26
+
27
+ // In-memory interview state (resumable)
28
+ let state = {
29
+ input: '',
30
+ answers: {},
31
+ inversion: {},
32
+ startedAt: null,
33
+ currentTier: 0,
34
+ }
35
+
36
+ /**
37
+ * Initialize a new BMAD interview session.
38
+ * @param {string} rawInput - Raw user feature description
39
+ * @returns {{ sessionId: string, tier: number, questions: array, inversion: object }}
40
+ */
41
+ function init(rawInput) {
42
+ state = {
43
+ input: rawInput.trim(),
44
+ answers: {},
45
+ inversion: problemFirst.invert(rawInput),
46
+ startedAt: new Date().toISOString(),
47
+ currentTier: 1,
48
+ }
49
+ return {
50
+ tier: 1,
51
+ inversion: state.inversion,
52
+ questions: questions.getAllQuestions().filter((q) => q.tier === 1),
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Record an answer to a question.
58
+ * @param {string} questionId - e.g. 'q1'
59
+ * @param {string} answer - User's answer
60
+ */
61
+ function answer(questionId, answer) {
62
+ state.answers[questionId] = answer
63
+ }
64
+
65
+ /**
66
+ * Get the current confidence score.
67
+ * @returns {{ score: number, level: string, factors: object }}
68
+ */
69
+ function score() {
70
+ return confidenceCalc.calculate(state)
71
+ }
72
+
73
+ /**
74
+ * Check if the user can proceed to /sdd-spec.
75
+ * @returns {{ canProceed: boolean, level: string, message: string }}
76
+ */
77
+ function canProceed() {
78
+ const c = confidenceCalc.calculate(state)
79
+ return {
80
+ canProceed: confidenceCalc.canProceed(c.score),
81
+ level: c.level,
82
+ score: c.score,
83
+ message: confidenceCalc.getWarning(c.score),
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Generate the BMAD-brief.md file.
89
+ * @returns {{ filePath: string, confidence: object }}
90
+ */
91
+ function generateBrief() {
92
+ const { brief, confidence } = synthesis.synthesize(state)
93
+
94
+ const header = `# BMAD Brief — [Project Name]
95
+
96
+ > Generated: ${TEMPLATE_DATE}. Expires: ${TEMPLATE_EXPIRY}.
97
+ > Confidence: **${confidence.score}/100** — ${confidence.level}
98
+
99
+ ## Input
100
+ > ${state.input}
101
+ `
102
+
103
+ const fullContent = header + brief
104
+
105
+ fs.writeFileSync(OUTPUT_FILE, fullContent, 'utf8')
106
+
107
+ return {
108
+ filePath: path.resolve(OUTPUT_FILE),
109
+ confidence,
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Get current interview state (for resume/debug).
115
+ */
116
+ function getState() {
117
+ return { ...state }
118
+ }
119
+
120
+ /**
121
+ * Get all questions with current answer status.
122
+ */
123
+ function getQuestionsWithStatus() {
124
+ return questions.getAllQuestions().map((q) => ({
125
+ ...q,
126
+ answered: !!state.answers[q.id],
127
+ answer: state.answers[q.id] || null,
128
+ }))
129
+ }
130
+
131
+ module.exports = {
132
+ init,
133
+ answer,
134
+ score,
135
+ canProceed,
136
+ generateBrief,
137
+ getState,
138
+ getQuestionsWithStatus,
139
+ OUTPUT_FILE,
140
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Problem-First Inversion
3
+ * Transforms a solution-first description into a problem-first brief.
4
+ * Rule: Always state the PROBLEM before describing the solution.
5
+ */
6
+
7
+ /**
8
+ * Apply problem-first inversion to user input.
9
+ * @param {string} input - Raw user description (often solution-first)
10
+ * @returns {{ problem: string, solution: string, inversionNote: string }}
11
+ */
12
+ function invert(input) {
13
+ // Common solution-first patterns to detect
14
+ const SOLUTION_PATTERNS = [
15
+ /\bbuild\b/i,
16
+ /\bcreate\b/i,
17
+ /\bimplement\b/i,
18
+ /\bmake\b/i,
19
+ /\bput\b/i,
20
+ /\badd\b/i,
21
+ /\blanding page\b/i,
22
+ /\bdashboard\b/i,
23
+ /\bfeature\b/i,
24
+ /\bfunctionality\b/i,
25
+ /\bbutton\b/i,
26
+ /\bform\b/i,
27
+ /\bapp\b/i,
28
+ /\bwebsite\b/i,
29
+ ]
30
+
31
+ const hasSolutionFocus = SOLUTION_PATTERNS.some((p) => p.test(input))
32
+
33
+ if (!hasSolutionFocus) {
34
+ return {
35
+ problem: input,
36
+ solution: '',
37
+ inversionNote: 'Input already problem-focused.',
38
+ }
39
+ }
40
+
41
+ // Extract what they think they want (the solution)
42
+ const solutionFragment = input
43
+
44
+ // Infer the likely problem (best effort heuristic)
45
+ const problem = inferProblem(input)
46
+
47
+ return {
48
+ problem,
49
+ solution: solutionFragment,
50
+ inversionNote:
51
+ '⚠️ INVERSION APPLIED: Your request described a solution. The problem it addresses is stated first above.',
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Best-effort problem inference from solution-first input.
57
+ * This is heuristic — the user corrects it during the interview.
58
+ */
59
+ function inferProblem(input) {
60
+ const lower = input.toLowerCase()
61
+
62
+ if (lower.includes('landing page')) {
63
+ return 'Users are not converting or not finding us credible — we need a way to communicate value and capture interest quickly.'
64
+ }
65
+ if (lower.includes('dashboard')) {
66
+ return 'Users lack visibility into key metrics or workflows — decisions are made blind or based on incomplete data.'
67
+ }
68
+ if (lower.includes('login') || lower.includes('auth')) {
69
+ return 'Users cannot securely access their accounts or data — engagement and retention are blocked.'
70
+ }
71
+ if (lower.includes('notification') || lower.includes('email')) {
72
+ return 'Users are missing important information or actions — engagement and retention suffer.'
73
+ }
74
+ if (lower.includes('search')) {
75
+ return 'Users cannot find what they need — they leave frustrated or submit support requests.'
76
+ }
77
+ if (lower.includes('export') || lower.includes('report')) {
78
+ return 'Users cannot act on their data — business decisions are slowed or made without full information.'
79
+ }
80
+
81
+ // Generic fallback
82
+ return 'Users face a friction point that prevents them from completing their goal — specific symptoms and impact are being confirmed in the interview.'
83
+ }
84
+
85
+ module.exports = { invert }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * BMAD 9-Question Framework
3
+ * Tier 1 (3) → Tier 2 (3) → Tier 3 (3) = 9 questions total
4
+ */
5
+
6
+ const TIERS = {
7
+ TIER1: {
8
+ name: 'Problem Definition',
9
+ questions: [
10
+ {
11
+ id: 'q1',
12
+ tier: 1,
13
+ question: 'What specific problem does this solve for the user?',
14
+ hint: 'Be specific — not "improve UX" but "reduce checkout abandonment by 40%"',
15
+ perspective: 'pm',
16
+ },
17
+ {
18
+ id: 'q2',
19
+ tier: 1,
20
+ question: 'Who is the target user, and what is their current workaround?',
21
+ hint: 'Describe the person and what they do today without this feature',
22
+ perspective: 'pm',
23
+ },
24
+ {
25
+ id: 'q3',
26
+ tier: 1,
27
+ question: 'What does success look like in 90 days? (measurable outcome)',
28
+ hint: 'Specific metric: "X% conversion", "N fewer support tickets", "$Y revenue"',
29
+ perspective: 'pm',
30
+ },
31
+ ],
32
+ },
33
+ TIER2: {
34
+ name: 'Technical & Feasibility',
35
+ questions: [
36
+ {
37
+ id: 'q4',
38
+ tier: 2,
39
+ question: 'What is the minimum viable version of this — one paragraph?',
40
+ hint: 'Strip everything optional. What remains is the core?',
41
+ perspective: 'architect',
42
+ },
43
+ {
44
+ id: 'q5',
45
+ tier: 2,
46
+ question: 'What existing systems or data does this need to connect to?',
47
+ hint: 'List integrations, APIs, databases, or services required',
48
+ perspective: 'architect',
49
+ },
50
+ {
51
+ id: 'q6',
52
+ tier: 2,
53
+ question: 'What are the top 2 technical risks, and how do we mitigate them?',
54
+ hint: 'Think: data loss, downtime, security, scale, third-party dependency',
55
+ perspective: 'architect',
56
+ },
57
+ ],
58
+ },
59
+ TIER3: {
60
+ name: 'UX & Polish',
61
+ questions: [
62
+ {
63
+ id: 'q7',
64
+ tier: 3,
65
+ question: 'What is the first thing the user sees and does? (first impression)',
66
+ hint: 'Describe the screen — what appears, what can they click?',
67
+ perspective: 'ux',
68
+ },
69
+ {
70
+ id: 'q8',
71
+ tier: 3,
72
+ question: 'What microcopy is essential? (labels, buttons, errors, empty states)',
73
+ hint: 'List specific phrases that must appear in the UI',
74
+ perspective: 'ux',
75
+ },
76
+ {
77
+ id: 'q9',
78
+ tier: 3,
79
+ question: 'What could go wrong that we need to handle gracefully?',
80
+ hint: 'Network failure, empty data, permission denied, slow loading',
81
+ perspective: 'ux',
82
+ },
83
+ ],
84
+ },
85
+ }
86
+
87
+ function getAllQuestions() {
88
+ return [
89
+ ...TIERS.TIER1.questions,
90
+ ...TIERS.TIER2.questions,
91
+ ...TIERS.TIER3.questions,
92
+ ]
93
+ }
94
+
95
+ function getQuestionById(id) {
96
+ for (const tier of Object.values(TIERS)) {
97
+ const found = tier.questions.find((q) => q.id === id)
98
+ if (found) return found
99
+ }
100
+ return null
101
+ }
102
+
103
+ function getQuestionsByTier(tier) {
104
+ for (const t of Object.values(TIERS)) {
105
+ if (t.questions[0]?.tier === tier) return t.questions
106
+ }
107
+ return []
108
+ }
109
+
110
+ module.exports = { TIERS, getAllQuestions, getQuestionById, getQuestionsByTier }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * BMAD Synthesis — merges three agent perspectives into a unified brief.
3
+ * Uses single-pass LLM synthesis mode (Claude Code analyzes all 3 perspectives).
4
+ */
5
+
6
+ const pmAgent = require('./agents/pm')
7
+ const architectAgent = require('./agents/architect')
8
+ const uxAgent = require('./agents/ux')
9
+ const confidenceCalc = require('./confidence')
10
+
11
+ /**
12
+ * Synthesize a full BMAD brief from interview state.
13
+ * @param {object} state - { input, answers, inversion }
14
+ * @returns {{ brief: string, confidence: object }}
15
+ */
16
+ function synthesize(state) {
17
+ // Run all three agents in parallel (synchronous in Node.js — single pass)
18
+ const pmResult = pmAgent.analyze(state)
19
+ const architectResult = architectAgent.analyze(state)
20
+ const uxResult = uxAgent.analyze(state)
21
+
22
+ // Calculate confidence
23
+ const confidence = confidenceCalc.calculate(state)
24
+
25
+ // Build problem-first note if inversion was applied
26
+ const inversionBlock = state.inversion?.inversionNote
27
+ ? `\n## ⚠️ Problem-First Inversion Applied\n${state.inversion.inversionNote}\n`
28
+ : ''
29
+
30
+ const synthesis = `${inversionBlock}
31
+ ## Synthesis
32
+
33
+ **What we're building:** ${synthesizeWhat(state)}
34
+
35
+ **What we're NOT building:** ${synthesizeNotBuilding(state)}
36
+
37
+ ## Open Questions
38
+ | # | Question | Impact |
39
+ |---|----------|--------|
40
+ | 1 | [Open question from interview] | [Blocking?] |
41
+ | 2 | [Another unresolved item] | [Clarify before implementation] |
42
+ `
43
+
44
+ // Concatenate all sections
45
+ const brief = `${pmResult.section}
46
+
47
+ ${architectResult.section}
48
+
49
+ ${uxResult.section}
50
+
51
+ ${synthesis}
52
+
53
+ ## Confidence Score
54
+ **Score:** ${confidence.score}/100 — ${confidence.level}
55
+ ${confidenceCalc.getWarning(confidence.score)}
56
+
57
+ _Scoring breakdown:_
58
+ ${formatFactors(confidence.factors)}
59
+
60
+ ## Prompt Injection Check
61
+ - Input was sanitized before processing
62
+ - No system prompt overrides accepted
63
+ `
64
+
65
+ return { brief, confidence }
66
+ }
67
+
68
+ function synthesizeWhat(state) {
69
+ const input = state.input || ''
70
+ const problem = state.inversion?.problem || input
71
+ const q1 = state.answers?.q1 || ''
72
+ const q4 = state.answers?.q4 || ''
73
+
74
+ const problemStatement = q1 || problem
75
+ const approach = q4
76
+ ? q4.split('.')[0] + '.'
77
+ : `Build the feature described as: ${input.slice(0, 80)}`
78
+
79
+ return `${problemStatement}. ${approach}`
80
+ }
81
+
82
+ function synthesizeNotBuilding(state) {
83
+ const exclusions = [
84
+ 'Second-order features — ship core first, add polish in v2',
85
+ 'Nice-to-have integrations — MVP ships with zero mandatory external dependencies',
86
+ 'Re-architecture of existing systems — integrate within current constraints',
87
+ ]
88
+
89
+ // Extract explicit exclusions from Q9 or other answers
90
+ const q9 = state.answers?.q9 || ''
91
+ if (q9.length > 0) {
92
+ const dynamic = q9
93
+ .split(/[,;\n]/)
94
+ .filter((l) => l.toLowerCase().includes('not ') || l.toLowerCase().includes("won't "))
95
+ .slice(0, 2)
96
+ .map((l) => l.trim())
97
+ exclusions.unshift(...dynamic)
98
+ }
99
+
100
+ return exclusions.map((e, i) => `${i + 1}. ${e}`).join('\n')
101
+ }
102
+
103
+ function formatFactors(factors) {
104
+ return Object.entries(factors)
105
+ .map(([key, val]) => ` - **${key}:** ${val.points} pts — ${val.note}`)
106
+ .join('\n')
107
+ }
108
+
109
+ module.exports = { synthesize }