@patricksardinha/agentkit-cli 0.3.0 → 0.5.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.
@@ -24,6 +24,45 @@ const FRAMEWORK_LABELS: Record<StackInfo['framework'], string> = {
24
24
  unknown: 'Unknown (generic)',
25
25
  }
26
26
 
27
+ const STACK_CHOICES = [
28
+ { name: 'React + Vite', value: 'react' },
29
+ { name: 'Next.js', value: 'nextjs' },
30
+ { name: 'Tauri v2 (React + Rust)', value: 'tauri' },
31
+ { name: 'FastAPI (Python)', value: 'fastapi' },
32
+ { name: 'Express (Node.js)', value: 'express' },
33
+ { name: 'Node.js (generic)', value: 'node' },
34
+ { name: 'None of the above — generate a generic CLAUDE.md to fill manually', value: 'none' },
35
+ ]
36
+
37
+ export async function resolveStack(
38
+ detected: StackInfo,
39
+ ): Promise<{ stack: StackInfo; stackNotConfigured: boolean }> {
40
+ if (detected.framework !== 'unknown') {
41
+ return { stack: detected, stackNotConfigured: false }
42
+ }
43
+
44
+ logger.warn('Stack not detected automatically.')
45
+ const { selectedFramework } = await inquirer.prompt<{ selectedFramework: string }>([
46
+ {
47
+ type: 'list',
48
+ name: 'selectedFramework',
49
+ message: 'Stack not detected automatically. Please select your stack:',
50
+ choices: STACK_CHOICES as unknown as string[],
51
+ },
52
+ ])
53
+
54
+ if (selectedFramework === 'none') {
55
+ return { stack: detected, stackNotConfigured: true }
56
+ }
57
+
58
+ const framework = selectedFramework as StackInfo['framework']
59
+ const language: StackInfo['language'] = framework === 'fastapi' ? 'python' : 'javascript'
60
+ return {
61
+ stack: { ...detected, framework, language, hasTypeScript: false },
62
+ stackNotConfigured: false,
63
+ }
64
+ }
65
+
27
66
  async function fileExists(path: string): Promise<boolean> {
28
67
  try {
29
68
  await readFile(path)
@@ -50,8 +89,10 @@ export function registerInit(program: Command): void {
50
89
  logger.warn('Ce dossier n\'est pas un repo git — lancez git init si nécessaire')
51
90
  }
52
91
 
53
- const label = FRAMEWORK_LABELS[stack.framework]
54
- logger.info(`Stack détectée : ${label} (${stack.language})`)
92
+ const { stack: resolvedStack, stackNotConfigured } = await resolveStack(stack)
93
+
94
+ const label = FRAMEWORK_LABELS[resolvedStack.framework]
95
+ logger.info(`Stack : ${label} (${resolvedStack.language})`)
55
96
 
56
97
  const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
57
98
  {
@@ -112,8 +153,8 @@ export function registerInit(program: Command): void {
112
153
  }
113
154
 
114
155
  const genSpinner = ora('Génération des fichiers…').start()
115
- const claudeMdContent = generateClaudeMd(stack, blueprintContent)
116
- const workflowContent = generateWorkflow(stack, blueprintContent)
156
+ const claudeMdContent = generateClaudeMd(resolvedStack, blueprintContent, stackNotConfigured)
157
+ const workflowContent = generateWorkflow(resolvedStack, blueprintContent, projectName)
117
158
  const agents = extractAgentsFromWorkflow(workflowContent)
118
159
  const playbookContent = generatePlaybook({ agents, projectName, hasBlueprint: !!blueprintContent })
119
160
  await writeFile(claudeMdPath, claudeMdContent, 'utf-8')
@@ -1,5 +1,4 @@
1
1
  import type { StackInfo } from '../detectors/stackDetector.js'
2
- import { parseBlueprint } from '../utils/blueprintParser.js'
3
2
  import * as react from '../templates/react.js'
4
3
  import * as nextjs from '../templates/nextjs.js'
5
4
  import * as tauri from '../templates/tauri.js'
@@ -8,7 +7,19 @@ import * as express from '../templates/express.js'
8
7
  import * as node from '../templates/node.js'
9
8
  import * as unknown from '../templates/unknown.js'
10
9
 
11
- export function generateClaudeMd(stack: StackInfo, blueprintContent?: string): string {
10
+ const STACK_NOT_CONFIGURED_WARNING = `
11
+ ## ⚠️ Stack not configured
12
+ AgentKit could not detect your stack and no stack was selected.
13
+ Before running Claude Code, fill in the following sections:
14
+ - Stack (framework, runtime, DB, tools)
15
+ - Commands (dev, build, test)
16
+ - Structure (folder layout)
17
+
18
+ Once filled, give Claude Code this instruction:
19
+ "Read PLAYBOOK.md and execute the procedure."
20
+ `
21
+
22
+ export function generateClaudeMd(stack: StackInfo, blueprintContent?: string, stackNotConfigured?: boolean): string {
12
23
  let base: string
13
24
  switch (stack.framework) {
14
25
  case 'react': base = react.claudeMd(stack); break
@@ -20,23 +31,18 @@ export function generateClaudeMd(stack: StackInfo, blueprintContent?: string): s
20
31
  default: base = unknown.claudeMd(stack)
21
32
  }
22
33
 
23
- if (!blueprintContent) return base
24
-
25
- const features = parseBlueprint(blueprintContent)
26
- if (features.length === 0) return base
34
+ if (stackNotConfigured) {
35
+ const firstNewline = base.indexOf('\n')
36
+ base = base.slice(0, firstNewline + 1) + STACK_NOT_CONFIGURED_WARNING + base.slice(firstNewline + 1)
37
+ }
27
38
 
28
- const featureLines = features
29
- .map((f, i) => {
30
- const sub = f.items.length > 0 ? '\n' + f.items.map((it) => ` - ${it}`).join('\n') : ''
31
- return `${i + 1}. **${f.name}**${sub}`
32
- })
33
- .join('\n')
39
+ if (!blueprintContent) return base
34
40
 
35
- const featureSection = `\n## Features (Blueprint)\n\n${featureLines}\n`
41
+ const blueprintNote = '\n> A PROJECT_BLUEPRINT.md is present — Claude Code will read it during Phase 0.\n'
36
42
 
37
43
  const conventionsIdx = base.indexOf('\n## Conventions')
38
44
  if (conventionsIdx !== -1) {
39
- return base.slice(0, conventionsIdx) + featureSection + base.slice(conventionsIdx)
45
+ return base.slice(0, conventionsIdx) + blueprintNote + base.slice(conventionsIdx)
40
46
  }
41
- return base + featureSection
47
+ return base + blueprintNote
42
48
  }
@@ -1,6 +1,4 @@
1
1
  import type { StackInfo } from '../detectors/stackDetector.js'
2
- import { parseBlueprint } from '../utils/blueprintParser.js'
3
- import { toSlug } from '../utils/agentParser.js'
4
2
  import * as react from '../templates/react.js'
5
3
  import * as nextjs from '../templates/nextjs.js'
6
4
  import * as tauri from '../templates/tauri.js'
@@ -9,8 +7,8 @@ import * as express from '../templates/express.js'
9
7
  import * as node from '../templates/node.js'
10
8
  import * as unknown from '../templates/unknown.js'
11
9
 
12
- export function generateWorkflow(stack: StackInfo, blueprintContent?: string): string {
13
- if (blueprintContent) return blueprintWorkflow(stack, blueprintContent)
10
+ export function generateWorkflow(stack: StackInfo, blueprintContent?: string, projectName?: string): string {
11
+ if (blueprintContent) return blueprintPlaceholder(projectName ?? stack.framework)
14
12
 
15
13
  switch (stack.framework) {
16
14
  case 'react': return react.workflow(stack)
@@ -23,40 +21,15 @@ export function generateWorkflow(stack: StackInfo, blueprintContent?: string): s
23
21
  }
24
22
  }
25
23
 
26
- function blueprintWorkflow(stack: StackInfo, blueprintContent: string): string {
27
- const features = parseBlueprint(blueprintContent)
24
+ function blueprintPlaceholder(projectName: string): string {
25
+ return `# AGENT_WORKFLOW.md — ${projectName}
28
26
 
29
- const agentBlocks = features.map((feature, i) => {
30
- const n = i + 1
31
- const slug = toSlug(feature.name)
32
- const outputLines =
33
- feature.items.length > 0
34
- ? feature.items.map((item) => ` - ${item}`).join('\n')
35
- : ` - src/${slug}/`
36
- return `### Agent ${n} · ${feature.name}
37
- Périmètre : Implémenter la fonctionnalité ${feature.name.toLowerCase()}
38
- Produit :
39
- ${outputLines}
40
- Critère : npm test (tests ${feature.name.toLowerCase()} passent)`
41
- })
27
+ > This file will be filled in by Claude Code during Phase 0.
28
+ > Claude Code will read PROJECT_BLUEPRINT.md, propose a decomposition,
29
+ > and replace this content after human validation.
42
30
 
43
- const ciN = features.length + 1
44
- agentBlocks.push(
45
- `### Agent ${ciN} · Tests & CI
46
- Périmètre : Couverture de tests complète et configuration du pipeline CI
47
- Produit :
48
- - tests/
49
- - .github/workflows/
50
- Critère : npm test passe, pipeline CI vert`,
51
- )
31
+ ---
52
32
 
53
- return `# Agent Workflow — ${stack.framework} (Blueprint)
54
-
55
- ## Stack détectée
56
- Framework: ${stack.framework} | Language: ${stack.language}
57
-
58
- ## Agents
59
-
60
- ${agentBlocks.join('\n\n')}
33
+ *Waiting for Phase 0 decomposition...*
61
34
  `
62
35
  }
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import type { StackInfo } from '../../src/detectors/stackDetector.js'
3
+
4
+ vi.mock('inquirer', () => ({
5
+ default: {
6
+ prompt: vi.fn(),
7
+ },
8
+ }))
9
+
10
+ vi.mock('../../src/utils/logger.js', () => ({
11
+ logger: {
12
+ info: vi.fn(),
13
+ success: vi.fn(),
14
+ warn: vi.fn(),
15
+ error: vi.fn(),
16
+ },
17
+ }))
18
+
19
+ import inquirer from 'inquirer'
20
+ import { resolveStack } from '../../src/commands/init.js'
21
+ import { generateClaudeMd } from '../../src/generators/claudeMdGenerator.js'
22
+
23
+ function makeStack(
24
+ framework: StackInfo['framework'],
25
+ opts: Partial<Omit<StackInfo, 'framework'>> = {},
26
+ ): StackInfo {
27
+ return {
28
+ framework,
29
+ language: framework === 'fastapi' ? 'python' : opts.hasTypeScript ? 'typescript' : 'javascript',
30
+ hasTypeScript: opts.hasTypeScript ?? false,
31
+ extras: opts.extras ?? [],
32
+ ...opts,
33
+ }
34
+ }
35
+
36
+ describe('resolveStack', () => {
37
+ beforeEach(() => {
38
+ vi.clearAllMocks()
39
+ })
40
+
41
+ it('returns detected stack unchanged when framework is not unknown — no prompt shown', async () => {
42
+ const detected = makeStack('react', { hasTypeScript: true })
43
+ const result = await resolveStack(detected)
44
+
45
+ expect(result.stack).toEqual(detected)
46
+ expect(result.stackNotConfigured).toBe(false)
47
+ expect(vi.mocked(inquirer.prompt)).not.toHaveBeenCalled()
48
+ })
49
+
50
+ it('does not prompt for any known framework', async () => {
51
+ const frameworks: StackInfo['framework'][] = ['nextjs', 'tauri', 'fastapi', 'express', 'node']
52
+ for (const framework of frameworks) {
53
+ vi.clearAllMocks()
54
+ const result = await resolveStack(makeStack(framework))
55
+ expect(result.stackNotConfigured).toBe(false)
56
+ expect(vi.mocked(inquirer.prompt)).not.toHaveBeenCalled()
57
+ }
58
+ })
59
+
60
+ it('prompts for stack selection when framework is unknown', async () => {
61
+ vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedFramework: 'tauri' })
62
+
63
+ const result = await resolveStack(makeStack('unknown'))
64
+
65
+ expect(vi.mocked(inquirer.prompt)).toHaveBeenCalledOnce()
66
+ expect(result.stack.framework).toBe('tauri')
67
+ expect(result.stack.language).toBe('javascript')
68
+ expect(result.stackNotConfigured).toBe(false)
69
+ })
70
+
71
+ it('unknown + user selects Tauri → stack.framework is tauri', async () => {
72
+ vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedFramework: 'tauri' })
73
+
74
+ const result = await resolveStack(makeStack('unknown'))
75
+
76
+ expect(result.stack.framework).toBe('tauri')
77
+ expect(result.stackNotConfigured).toBe(false)
78
+ })
79
+
80
+ it('unknown + user selects FastAPI → language is python', async () => {
81
+ vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedFramework: 'fastapi' })
82
+
83
+ const result = await resolveStack(makeStack('unknown'))
84
+
85
+ expect(result.stack.framework).toBe('fastapi')
86
+ expect(result.stack.language).toBe('python')
87
+ expect(result.stackNotConfigured).toBe(false)
88
+ })
89
+
90
+ it('unknown + user selects "None of the above" → stackNotConfigured is true, framework stays unknown', async () => {
91
+ vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedFramework: 'none' })
92
+
93
+ const result = await resolveStack(makeStack('unknown'))
94
+
95
+ expect(result.stack.framework).toBe('unknown')
96
+ expect(result.stackNotConfigured).toBe(true)
97
+ })
98
+ })
99
+
100
+ describe('generateClaudeMd — stackNotConfigured warning', () => {
101
+ it('includes warning block when stackNotConfigured is true', () => {
102
+ const result = generateClaudeMd(makeStack('unknown'), undefined, true)
103
+ expect(result).toContain('⚠️ Stack not configured')
104
+ expect(result).toContain('AgentKit could not detect your stack')
105
+ expect(result).toContain('Stack (framework, runtime, DB, tools)')
106
+ expect(result).toContain('Read PLAYBOOK.md and execute the procedure.')
107
+ })
108
+
109
+ it('does not include warning block when stackNotConfigured is false', () => {
110
+ const result = generateClaudeMd(makeStack('unknown'), undefined, false)
111
+ expect(result).not.toContain('⚠️ Stack not configured')
112
+ })
113
+
114
+ it('does not include warning block when stackNotConfigured is omitted', () => {
115
+ const result = generateClaudeMd(makeStack('unknown'))
116
+ expect(result).not.toContain('⚠️ Stack not configured')
117
+ })
118
+
119
+ it('unknown + None selected → uses unknown template content', () => {
120
+ const result = generateClaudeMd(makeStack('unknown'), undefined, true)
121
+ expect(result).toContain('## Stack')
122
+ expect(result).toContain('## Commands')
123
+ expect(result).toContain('## Conventions')
124
+ })
125
+
126
+ it('unknown + Tauri selected → resolveStack returns tauri, generateClaudeMd uses tauri template', async () => {
127
+ vi.mocked(inquirer.prompt).mockResolvedValueOnce({ selectedFramework: 'tauri' })
128
+
129
+ const { stack, stackNotConfigured } = await resolveStack(makeStack('unknown'))
130
+ const result = generateClaudeMd(stack, undefined, stackNotConfigured)
131
+
132
+ expect(stack.framework).toBe('tauri')
133
+ expect(stackNotConfigured).toBe(false)
134
+ expect(result).toContain('Tauri')
135
+ expect(result).toContain('Rust')
136
+ expect(result).not.toContain('⚠️ Stack not configured')
137
+ })
138
+
139
+ it('warning block still present alongside blueprint note when both are set', () => {
140
+ const blueprint = '# My App\n\n## Goal\nSomething\n'
141
+ const result = generateClaudeMd(makeStack('unknown'), blueprint, true)
142
+ expect(result).toContain('⚠️ Stack not configured')
143
+ expect(result).toContain('PROJECT_BLUEPRINT.md is present')
144
+ })
145
+ })
@@ -63,27 +63,30 @@ describe('generateClaudeMd with blueprint', () => {
63
63
  expect(generateClaudeMd(REACT_STACK)).toBe(generateClaudeMd(REACT_STACK, undefined))
64
64
  })
65
65
 
66
- it('includes a Features section when blueprint is provided', () => {
66
+ it('adds the blueprint note when blueprint is provided', () => {
67
67
  const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
68
- expect(result).toContain('## Features (Blueprint)')
69
- expect(result).toContain('Authentication')
70
- expect(result).toContain('Dashboard')
71
- expect(result).toContain('API')
68
+ expect(result).toContain('PROJECT_BLUEPRINT.md is present')
69
+ expect(result).toContain('Phase 0')
72
70
  })
73
71
 
74
- it('includes blueprint sub-items', () => {
72
+ it('does NOT include a Features (Blueprint) section', () => {
75
73
  const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
76
- expect(result).toContain('JWT tokens')
77
- expect(result).toContain('User statistics')
74
+ expect(result).not.toContain('## Features (Blueprint)')
78
75
  })
79
76
 
80
- it('places Features section before Conventions', () => {
77
+ it('does NOT include blueprint sub-items as feature bullets', () => {
81
78
  const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
82
- const featIdx = result.indexOf('## Features (Blueprint)')
79
+ expect(result).not.toContain('JWT tokens')
80
+ expect(result).not.toContain('User statistics')
81
+ })
82
+
83
+ it('places blueprint note before Conventions', () => {
84
+ const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
85
+ const noteIdx = result.indexOf('PROJECT_BLUEPRINT.md is present')
83
86
  const convIdx = result.indexOf('## Conventions')
84
- expect(featIdx).toBeGreaterThan(-1)
87
+ expect(noteIdx).toBeGreaterThan(-1)
85
88
  expect(convIdx).toBeGreaterThan(-1)
86
- expect(featIdx).toBeLessThan(convIdx)
89
+ expect(noteIdx).toBeLessThan(convIdx)
87
90
  })
88
91
 
89
92
  it('still contains stack-specific content', () => {
@@ -98,33 +101,35 @@ describe('generateWorkflow with blueprint', () => {
98
101
  expect(generateWorkflow(REACT_STACK)).toBe(generateWorkflow(REACT_STACK, undefined))
99
102
  })
100
103
 
101
- it('generates one agent per blueprint feature plus a CI agent', () => {
104
+ it('returns a Phase 0 placeholder instead of agent blocks', () => {
102
105
  const result = generateWorkflow(REACT_STACK, BLUEPRINT)
103
- expect(result).toContain('Agent 1 · Authentication')
104
- expect(result).toContain('Agent 2 · Dashboard')
105
- expect(result).toContain('Agent 3 · API')
106
- expect(result).toContain('Agent 4 · Tests & CI')
106
+ expect(result).toContain('AGENT_WORKFLOW.md')
107
+ expect(result).toContain('Phase 0')
108
+ expect(result).toContain('PROJECT_BLUEPRINT.md')
109
+ expect(result).toContain('Waiting for Phase 0 decomposition')
107
110
  })
108
111
 
109
- it('each agent block is parseable by extractAgentsFromWorkflow', () => {
112
+ it('does NOT generate agent blocks from blueprint sections', () => {
110
113
  const result = generateWorkflow(REACT_STACK, BLUEPRINT)
111
- const agents = extractAgentsFromWorkflow(result)
112
- expect(agents).toHaveLength(4)
113
- expect(agents[0].name).toBe('Authentication')
114
- expect(agents[0].slug).toBe('authentication')
115
- expect(agents[3].name).toBe('Tests & CI')
116
- expect(agents[3].slug).toBe('tests-ci')
114
+ expect(result).not.toContain('Agent 1')
115
+ expect(result).not.toContain('Authentication')
116
+ expect(result).not.toContain('Dashboard')
117
+ expect(result).not.toContain('Tests & CI')
117
118
  })
118
119
 
119
- it('includes blueprint feature items as outputs', () => {
120
+ it('does NOT include blueprint feature items as outputs', () => {
120
121
  const result = generateWorkflow(REACT_STACK, BLUEPRINT)
121
- expect(result).toContain('JWT tokens')
122
- expect(result).toContain('User statistics')
122
+ expect(result).not.toContain('JWT tokens')
123
+ expect(result).not.toContain('User statistics')
124
+ })
125
+
126
+ it('uses projectName in heading when provided', () => {
127
+ const result = generateWorkflow(REACT_STACK, BLUEPRINT, 'my-app')
128
+ expect(result).toContain('AGENT_WORKFLOW.md — my-app')
123
129
  })
124
130
 
125
- it('includes stack information in the header', () => {
131
+ it('falls back to framework name when projectName is omitted', () => {
126
132
  const result = generateWorkflow(REACT_STACK, BLUEPRINT)
127
- expect(result).toContain('react')
128
- expect(result).toContain('typescript')
133
+ expect(result).toContain('AGENT_WORKFLOW.md — react')
129
134
  })
130
135
  })
@@ -83,4 +83,52 @@ describe('generateClaudeMd', () => {
83
83
  expect(typeof result).toBe('string')
84
84
  expect(result.length).toBeGreaterThan(0)
85
85
  })
86
+
87
+ describe('blueprint note', () => {
88
+ const blueprint = `# My Project\n\n## Goal\nBuild something\n\n## Features\n- Auth\n- Dashboard\n`
89
+
90
+ it('adds the blueprint note when blueprintContent is provided', () => {
91
+ const result = generateClaudeMd(makeStack('react'), blueprint)
92
+ expect(result).toContain('PROJECT_BLUEPRINT.md is present')
93
+ expect(result).toContain('Phase 0')
94
+ })
95
+
96
+ it('does NOT list blueprint sections as features', () => {
97
+ const result = generateClaudeMd(makeStack('react'), blueprint)
98
+ expect(result).not.toContain('Features (Blueprint)')
99
+ expect(result).not.toContain('**Goal**')
100
+ expect(result).not.toContain('**Features**')
101
+ })
102
+
103
+ it('still contains the stack-based template content', () => {
104
+ const result = generateClaudeMd(makeStack('react'), blueprint)
105
+ expect(result).toContain('React')
106
+ expect(result).toContain('## Stack')
107
+ expect(result).toContain('## Commands')
108
+ })
109
+
110
+ it('adds blueprint note for unknown stack with blueprint', () => {
111
+ const result = generateClaudeMd(makeStack('unknown'), blueprint)
112
+ expect(result).toContain('PROJECT_BLUEPRINT.md is present')
113
+ expect(result).toContain('Phase 0')
114
+ })
115
+
116
+ it('returns base template unchanged when blueprintContent is absent', () => {
117
+ const withBlueprint = generateClaudeMd(makeStack('react'), blueprint)
118
+ const withoutBlueprint = generateClaudeMd(makeStack('react'))
119
+ expect(withBlueprint).not.toBe(withoutBlueprint)
120
+ expect(withoutBlueprint).not.toContain('PROJECT_BLUEPRINT.md is present')
121
+ })
122
+
123
+ it('adds blueprint note for every framework', () => {
124
+ const frameworks: StackInfo['framework'][] = [
125
+ 'react', 'nextjs', 'tauri', 'fastapi', 'express', 'node', 'unknown',
126
+ ]
127
+ for (const framework of frameworks) {
128
+ const result = generateClaudeMd(makeStack(framework), blueprint)
129
+ expect(result).toContain('PROJECT_BLUEPRINT.md is present')
130
+ expect(result).not.toContain('Features (Blueprint)')
131
+ }
132
+ })
133
+ })
86
134
  })
@@ -81,4 +81,44 @@ describe('generateWorkflow', () => {
81
81
  expect(typeof result).toBe('string')
82
82
  expect(result.length).toBeGreaterThan(0)
83
83
  })
84
+
85
+ describe('blueprint placeholder', () => {
86
+ const blueprint = `# My Project\n\n## Goal\nBuild something\n\n## Features\n- Auth\n- Dashboard\n`
87
+
88
+ it('returns a placeholder when blueprintContent is provided', () => {
89
+ const result = generateWorkflow(makeStack('react'), blueprint)
90
+ expect(result).toContain('AGENT_WORKFLOW.md')
91
+ expect(result).toContain('Phase 0')
92
+ expect(result).toContain('PROJECT_BLUEPRINT.md')
93
+ expect(result).toContain('Waiting for Phase 0 decomposition')
94
+ })
95
+
96
+ it('uses projectName in heading when provided', () => {
97
+ const result = generateWorkflow(makeStack('react'), blueprint, 'my-app')
98
+ expect(result).toContain('AGENT_WORKFLOW.md — my-app')
99
+ })
100
+
101
+ it('falls back to framework name when projectName is omitted', () => {
102
+ const result = generateWorkflow(makeStack('react'), blueprint)
103
+ expect(result).toContain('AGENT_WORKFLOW.md — react')
104
+ })
105
+
106
+ it('does NOT parse blueprint sections as agents', () => {
107
+ const result = generateWorkflow(makeStack('react'), blueprint, 'my-app')
108
+ expect(result).not.toContain('Agent · Goal')
109
+ expect(result).not.toContain('Agent · Features')
110
+ expect(result).not.toContain('Agent 1')
111
+ })
112
+
113
+ it('returns the placeholder for every framework when blueprint is provided', () => {
114
+ const frameworks: StackInfo['framework'][] = [
115
+ 'react', 'nextjs', 'tauri', 'fastapi', 'express', 'node', 'unknown',
116
+ ]
117
+ for (const framework of frameworks) {
118
+ const result = generateWorkflow(makeStack(framework), blueprint, 'proj')
119
+ expect(result).toContain('Phase 0')
120
+ expect(result).not.toContain('Agent 1')
121
+ }
122
+ })
123
+ })
84
124
  })