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.
- package/README.md +376 -95
- package/bin/sdd.js +14 -6
- package/lib/bmad/agents/architect.js +96 -0
- package/lib/bmad/agents/pm.js +67 -0
- package/lib/bmad/agents/ux.js +103 -0
- package/lib/bmad/confidence.js +129 -0
- package/lib/bmad/index.js +140 -0
- package/lib/bmad/problem-first.js +85 -0
- package/lib/bmad/questions.js +110 -0
- package/lib/bmad/synthesis.js +109 -0
- package/lib/bmad.js +45 -51
- package/lib/bundle/.claude/commands/sdd-bmad.md +360 -20
- package/lib/bundle/.claude/commands/sdd-converge.md +21 -7
- package/lib/bundle/.claude/commands/sdd-cook.md +335 -0
- package/lib/bundle/.claude/commands/sdd-init.md +71 -13
- package/lib/bundle/.claude/commands/sdd-spec.md +103 -35
- package/lib/bundle/.claude/commands/sdd-task-status.md +95 -0
- package/lib/bundle/.claude/commands/sdd-tasks.md +100 -17
- package/lib/bundle/.claude/commands/sdd.md +48 -8
- package/lib/bundle/.sdd/config.json +2 -2
- package/lib/bundle/CLAUDE.md +6 -4
- package/lib/bundle/commands/sdd-status.ps1 +40 -6
- package/lib/bundle/commands/sdd-task-status.ps1 +78 -0
- package/lib/bundle/run-converge.ps1 +84 -23
- package/lib/bundle/skills/sdd-cook/SKILL.md +381 -0
- package/lib/bundle/skills/sdd-cook/references/intent-detection.md +67 -0
- package/lib/bundle/skills/sdd-cook/references/subagent-patterns.md +352 -0
- package/lib/bundle/skills/sdd-cook/references/subagent-prompts.md +346 -0
- package/lib/bundle/skills/sdd-cook/references/task-execution.md +259 -0
- package/lib/bundle/skills/sdd-cook/references/workflow-steps.md +215 -0
- package/lib/bundle/templates/BMAD-brief.template.md +75 -14
- package/lib/bundle/templates/SPEC.template.md +193 -20
- package/lib/bundle/templates/TASKS.template.md +32 -13
- package/lib/bundle/templates/task-detail.template.md +40 -0
- package/lib/bundle/templates/task-master.template.md +41 -0
- package/lib/converge-task.ps1 +260 -0
- package/lib/init.js +2 -0
- package/lib/output-dirs.ps1 +461 -0
- package/lib/sdd-check.ps1 +130 -0
- package/lib/shared-converge.ps1 +270 -0
- package/lib/verify-patterns.ps1 +208 -0
- 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 }
|