@polymorphism-tech/morph-spec 2.4.0 → 3.0.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/CLAUDE.md +158 -26
- package/LICENSE +72 -72
- package/bin/detect-agents.js +225 -225
- package/bin/morph-spec.js +8 -0
- package/bin/render-template.js +302 -302
- package/bin/semantic-detect-agents.js +246 -246
- package/bin/validate-agents-skills.js +251 -251
- package/bin/validate-agents.js +69 -69
- package/bin/validate-phase.js +263 -263
- package/content/.azure/README.md +293 -293
- package/content/.azure/docs/azure-devops-setup.md +454 -454
- package/content/.azure/docs/branch-strategy.md +398 -398
- package/content/.azure/docs/local-development.md +515 -515
- package/content/.azure/pipelines/pipeline-variables.yml +34 -34
- package/content/.azure/pipelines/prod-pipeline.yml +319 -319
- package/content/.azure/pipelines/staging-pipeline.yml +234 -234
- package/content/.azure/pipelines/templates/build-dotnet.yml +75 -75
- package/content/.azure/pipelines/templates/deploy-app-service.yml +94 -94
- package/content/.azure/pipelines/templates/deploy-container-app.yml +120 -120
- package/content/.azure/pipelines/templates/infra-deploy.yml +90 -90
- package/content/.claude/commands/morph-archive.md +79 -79
- package/content/.claude/commands/morph-deploy.md +529 -0
- package/content/.claude/commands/morph-infra.md +209 -209
- package/content/.claude/commands/morph-preflight.md +227 -227
- package/content/.claude/commands/morph-troubleshoot.md +122 -122
- package/content/.claude/settings.local.json +15 -15
- package/content/.claude/skills/infra/azure-deploy-specialist.md +699 -0
- package/content/.claude/skills/level-0-meta/README.md +7 -0
- package/content/.claude/skills/{checklists → level-0-meta}/morph-checklist.md +117 -117
- package/content/.claude/skills/level-1-workflows/README.md +7 -0
- package/content/.claude/skills/{workflows → level-1-workflows}/morph-replicate.md +213 -213
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-clarify.md +131 -131
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-design.md +213 -205
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-setup.md +106 -92
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-tasks.md +164 -164
- package/content/.claude/skills/{workflows → level-1-workflows}/phase-uiux.md +169 -138
- package/content/.claude/skills/level-2-domains/README.md +14 -0
- package/content/.claude/skills/{specialists → level-2-domains/quality}/testing-specialist.md +126 -126
- package/content/.claude/skills/level-3-technologies/README.md +7 -0
- package/content/.claude/skills/level-4-patterns/README.md +7 -0
- package/content/.claude/skills/specialists/prompt-engineer.md +189 -0
- package/content/.claude/skills/specialists/seo-growth-hacker.md +320 -0
- package/content/.morph/.morphversion +5 -5
- package/content/.morph/archive/.gitkeep +25 -25
- package/content/.morph/config/agents.json +742 -358
- package/content/.morph/config/config.template.json +33 -0
- package/content/.morph/docs/STORY-DRIVEN-DEVELOPMENT.md +392 -392
- package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -241
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -307
- package/content/.morph/examples/api-nextjs/spec.md +399 -399
- package/content/.morph/examples/api-nextjs/tasks.md +168 -168
- package/content/.morph/examples/micro-saas/README.md +125 -125
- package/content/.morph/examples/micro-saas/contracts.cs +358 -358
- package/content/.morph/examples/micro-saas/decisions.md +246 -246
- package/content/.morph/examples/micro-saas/spec.md +236 -236
- package/content/.morph/examples/micro-saas/tasks.md +150 -150
- package/content/.morph/examples/multi-agent/README.md +309 -309
- package/content/.morph/examples/multi-agent/contracts.cs +433 -433
- package/content/.morph/examples/multi-agent/spec.md +479 -479
- package/content/.morph/examples/multi-agent/tasks.md +185 -185
- package/content/.morph/examples/scheduled-reports/decisions.md +158 -158
- package/content/.morph/examples/scheduled-reports/proposal.md +95 -95
- package/content/.morph/examples/scheduled-reports/spec.md +267 -267
- package/content/.morph/examples/state-v3.json +188 -188
- package/content/.morph/features/.gitkeep +25 -25
- package/content/.morph/hooks/README.md +158 -0
- package/content/.morph/hooks/pre-commit-all.sh +48 -48
- package/content/.morph/hooks/pre-commit-specs.sh +49 -49
- package/content/.morph/hooks/pre-commit-tests.sh +60 -60
- package/content/.morph/hooks/task-completed.js +73 -0
- package/content/.morph/hooks/teammate-idle.js +68 -0
- package/content/.morph/project.md +160 -160
- package/content/.morph/schemas/agent.schema.json +296 -296
- package/content/.morph/schemas/tasks.schema.json +220 -220
- package/content/.morph/specs/.gitkeep +20 -20
- package/content/.morph/standards/agent-teams-workflow.md +474 -0
- package/content/.morph/standards/coding.md +377 -377
- package/content/.morph/standards/fluent-ui-setup.md +590 -590
- package/content/.morph/standards/migration-guide.md +514 -514
- package/content/.morph/standards/passkeys-auth.md +423 -423
- package/content/.morph/standards/vector-search-rag.md +536 -536
- package/content/.morph/state.json +17 -17
- package/content/.morph/templates/CONTEXT-FEATURE.md +276 -0
- package/content/.morph/templates/CONTEXT.md +170 -0
- package/content/.morph/templates/FluentDesignTheme.cs +149 -149
- package/content/.morph/templates/MudTheme.cs +281 -281
- package/content/.morph/templates/clarify-questions.md +159 -159
- package/content/.morph/templates/component.razor +239 -239
- package/content/.morph/templates/contracts/Commands.cs +74 -74
- package/content/.morph/templates/contracts/Entities.cs +25 -25
- package/content/.morph/templates/contracts/Queries.cs +74 -74
- package/content/.morph/templates/contracts/README.md +74 -74
- package/content/.morph/templates/contracts.cs +217 -217
- package/content/.morph/templates/design-system.css +226 -226
- package/content/.morph/templates/infra/.dockerignore.example +89 -89
- package/content/.morph/templates/infra/Dockerfile.example +82 -82
- package/content/.morph/templates/infra/README.md +286 -286
- package/content/.morph/templates/infra/app-insights.bicep +63 -63
- package/content/.morph/templates/infra/app-service.bicep +164 -164
- package/content/.morph/templates/infra/azure-pipelines-deploy.yml +480 -0
- package/content/.morph/templates/infra/container-app-env.bicep +49 -49
- package/content/.morph/templates/infra/container-app.bicep +156 -156
- package/content/.morph/templates/infra/deploy-checklist.md +426 -426
- package/content/.morph/templates/infra/deploy.ps1 +229 -229
- package/content/.morph/templates/infra/deploy.sh +208 -208
- package/content/.morph/templates/infra/key-vault.bicep +91 -91
- package/content/.morph/templates/infra/main.bicep +189 -189
- package/content/.morph/templates/infra/parameters.dev.json +29 -29
- package/content/.morph/templates/infra/parameters.prod.json +29 -29
- package/content/.morph/templates/infra/parameters.staging.json +29 -29
- package/content/.morph/templates/infra/sql-database.bicep +103 -103
- package/content/.morph/templates/infra/storage.bicep +106 -106
- package/content/.morph/templates/integrations/asaas-client.cs +387 -387
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -351
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -288
- package/content/.morph/templates/integrations/clerk-config.cs +258 -258
- package/content/.morph/templates/job.cs +171 -171
- package/content/.morph/templates/migration.cs +83 -83
- package/content/.morph/templates/repository.cs +141 -141
- package/content/.morph/templates/saas/subscription.cs +347 -347
- package/content/.morph/templates/saas/tenant.cs +338 -338
- package/content/.morph/templates/service.cs +139 -139
- package/content/.morph/templates/sprint-status.yaml +68 -68
- package/content/.morph/templates/story.md +143 -143
- package/content/.morph/templates/test.cs +239 -239
- package/content/.morph/templates/ui-design-system.md +286 -286
- package/content/.morph/templates/ui-flows.md +336 -336
- package/content/.morph/templates/ui-mockups.md +133 -133
- package/content/.morph/test-infra/example.bicep +59 -59
- package/content/README.md +79 -79
- package/detectors/config-detector.js +223 -223
- package/detectors/conversation-analyzer.js +163 -163
- package/detectors/index.js +84 -84
- package/detectors/standards-generator.js +275 -275
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +977 -977
- package/docs/api/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1048 -1048
- package/docs/api/scripts/collapse.js +38 -38
- package/docs/api/scripts/commonNav.js +28 -28
- package/docs/api/scripts/linenumber.js +25 -25
- package/docs/api/scripts/nav.js +12 -12
- package/docs/api/scripts/polyfill.js +3 -3
- package/docs/api/scripts/prettify/Apache-License-2.0.txt +202 -202
- package/docs/api/scripts/prettify/lang-css.js +2 -2
- package/docs/api/scripts/prettify/prettify.js +28 -28
- package/docs/api/scripts/search.js +98 -98
- package/docs/api/styles/jsdoc.css +776 -776
- package/docs/api/styles/prettify.css +80 -80
- package/docs/examples.md +328 -328
- package/docs/templates.md +418 -418
- package/package.json +1 -1
- package/scripts/postinstall.js +132 -132
- package/src/commands/advance-phase.js +83 -0
- package/src/commands/analyze-blazor-concurrency.js +193 -193
- package/src/commands/create-story.js +351 -351
- package/src/commands/deploy.js +780 -0
- package/src/commands/detect-agents.js +34 -6
- package/src/commands/detect.js +104 -104
- package/src/commands/generate-context.js +40 -0
- package/src/commands/generate.js +149 -149
- package/src/commands/lint-fluent.js +352 -352
- package/src/commands/rollback-phase.js +185 -185
- package/src/commands/session-summary.js +291 -291
- package/src/commands/shard-spec.js +224 -224
- package/src/commands/sprint-status.js +250 -250
- package/src/commands/state.js +333 -333
- package/src/commands/sync.js +167 -167
- package/src/commands/troubleshoot.js +222 -222
- package/src/commands/validate-blazor-state.js +210 -210
- package/src/commands/validate-blazor.js +156 -156
- package/src/commands/validate-css.js +84 -84
- package/src/commands/validate-phase.js +221 -221
- package/src/lib/blazor-concurrency-analyzer.js +288 -288
- package/src/lib/blazor-state-validator.js +291 -291
- package/src/lib/blazor-validator.js +374 -374
- package/src/lib/context-generator.js +513 -0
- package/src/lib/css-validator.js +352 -352
- package/src/lib/design-system-detector.js +187 -0
- package/src/lib/design-system-generator.js +298 -298
- package/src/lib/design-system-scaffolder.js +299 -0
- package/src/lib/hook-executor.js +256 -0
- package/src/lib/learning-system.js +520 -520
- package/src/lib/mockup-generator.js +366 -366
- package/src/lib/spec-validator.js +258 -0
- package/src/lib/standards-context-injector.js +287 -0
- package/src/lib/team-orchestrator.js +322 -0
- package/src/lib/troubleshoot-grep.js +194 -194
- package/src/lib/troubleshoot-index.js +144 -144
- package/src/lib/ui-detector.js +350 -350
- package/src/lib/validation-runner.js +65 -13
- package/src/lib/validators/architecture-validator.js +387 -387
- package/src/lib/validators/design-system-validator.js +231 -0
- package/src/lib/validators/package-validator.js +360 -360
- package/src/lib/validators/ui-contrast-validator.js +422 -422
- package/src/utils/file-copier.js +9 -1
- package/src/utils/logger.js +32 -32
- package/src/utils/version-checker.js +175 -175
- /package/content/.claude/skills/{checklists → level-0-meta}/code-review.md +0 -0
- /package/content/.claude/skills/{checklists → level-0-meta}/simulation-checklist.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/ai-agents}/ai-system-architect.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/architecture}/po-pm-advisor.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/architecture}/standards-architect.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/dotnet-senior.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/ef-modeler.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/hangfire-orchestrator.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/backend}/ms-agent-expert.md +0 -0
- /package/content/.claude/skills/{stacks/dotnet-blazor.md → level-2-domains/frontend/blazor-builder.md} +0 -0
- /package/content/.claude/skills/{stacks/dotnet-nextjs.md → level-2-domains/frontend/nextjs-expert.md} +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/frontend}/ui-ux-designer.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/infrastructure}/azure-architect.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/bicep-architect.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/container-specialist.md +0 -0
- /package/content/.claude/skills/{infra → level-2-domains/infrastructure}/devops-engineer.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/asaas-financial.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/azure-identity.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/clerk-auth.md +0 -0
- /package/content/.claude/skills/{integrations → level-2-domains/integrations}/resend-email.md +0 -0
- /package/content/.claude/skills/{specialists → level-2-domains/quality}/code-analyzer.md +0 -0
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC Story Creator
|
|
3
|
-
* Creates self-contained story files with auto-injected Dev Notes
|
|
4
|
-
* Inspired by BMAD Method story-driven development
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import yaml from 'yaml';
|
|
10
|
-
import ora from 'ora';
|
|
11
|
-
import chalk from 'chalk';
|
|
12
|
-
import { logger } from '../utils/logger.js';
|
|
13
|
-
import { ensureDir, writeFile } from '../utils/file-copier.js';
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Helper Functions
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
function readTemplate(templateName) {
|
|
20
|
-
const templatePath = path.join(process.cwd(), 'node_modules/@polymorphism-tech/morph-spec/content/.morph/templates', templateName);
|
|
21
|
-
|
|
22
|
-
// Fallback to local if in development
|
|
23
|
-
if (!fs.existsSync(templatePath)) {
|
|
24
|
-
const localPath = path.join(process.cwd(), `content/.morph/templates/${templateName}`);
|
|
25
|
-
if (fs.existsSync(localPath)) {
|
|
26
|
-
return fs.readFileSync(localPath, 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
throw new Error(`Template not found: ${templateName}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return fs.readFileSync(templatePath, 'utf-8');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readSpec(featureName) {
|
|
35
|
-
const specPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec.md`);
|
|
36
|
-
const shardedIndexPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec/index.md`);
|
|
37
|
-
|
|
38
|
-
// Try sharded spec first (BMAD pattern)
|
|
39
|
-
if (fs.existsSync(shardedIndexPath)) {
|
|
40
|
-
return { isSharded: true, indexPath: shardedIndexPath };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Fallback to monolithic spec
|
|
44
|
-
if (fs.existsSync(specPath)) {
|
|
45
|
-
return { isSharded: false, content: fs.readFileSync(specPath, 'utf-8') };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
throw new Error(`Spec not found for feature: ${featureName}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractContextFromSpec(specContent, sectionHeading) {
|
|
52
|
-
// Extract section content between ## heading and next ##
|
|
53
|
-
const regex = new RegExp(`## ${sectionHeading}\\s*([\\s\\S]*?)(?=\\n## |$)`, 'i');
|
|
54
|
-
const match = specContent.match(regex);
|
|
55
|
-
return match ? match[1].trim() : 'See spec.md for complete context';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function detectPatterns(tasks, specContent) {
|
|
59
|
-
const patterns = {
|
|
60
|
-
hasEntity: tasks.some(t => /entity|model|domain/i.test(t)),
|
|
61
|
-
hasService: tasks.some(t => /service|business|logic/i.test(t)),
|
|
62
|
-
hasComponent: tasks.some(t => /component|razor|blazor|ui/i.test(t)),
|
|
63
|
-
hasRepository: tasks.some(t => /repository|data|persistence/i.test(t)),
|
|
64
|
-
hasJob: tasks.some(t => /job|background|hangfire|scheduled/i.test(t)),
|
|
65
|
-
hasAI: tasks.some(t => /agent|ai|llm|semantic|rag/i.test(t)),
|
|
66
|
-
hasAzure: tasks.some(t => /bicep|azure|infra|deploy/i.test(t)),
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return patterns;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function injectDevNotes(patterns, storyData) {
|
|
73
|
-
const notes = [];
|
|
74
|
-
|
|
75
|
-
// Entity pattern
|
|
76
|
-
if (patterns.hasEntity) {
|
|
77
|
-
notes.push('Use Primary Constructor (.NET 10 feature)');
|
|
78
|
-
notes.push('Follow entity pattern: .morph/project/standards/coding.md#entity-pattern');
|
|
79
|
-
notes.push('Navigation properties: configure in DbContext with Fluent API');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Service pattern
|
|
83
|
-
if (patterns.hasService) {
|
|
84
|
-
notes.push('Service layer: implement interface-first (IXService → XService)');
|
|
85
|
-
notes.push('Dependency injection: register in Program.cs as Scoped');
|
|
86
|
-
notes.push('Error handling: use Result<T> pattern from standards/architecture.md');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Blazor component
|
|
90
|
-
if (patterns.hasComponent) {
|
|
91
|
-
notes.push('UI Library: Fluent UI Blazor (see .morph/project/standards/ui.md)');
|
|
92
|
-
notes.push('Design System: use CSS variables from wwwroot/css/design-system.css');
|
|
93
|
-
notes.push('State management: @inject for services, [Parameter] for props');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Repository pattern
|
|
97
|
-
if (patterns.hasRepository) {
|
|
98
|
-
notes.push('Repository pattern: IRepository<T> generic base');
|
|
99
|
-
notes.push('EF Core: use AsNoTracking() for read-only queries');
|
|
100
|
-
notes.push('Transactions: wrap in DbContext.Database.BeginTransactionAsync()');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Background job
|
|
104
|
-
if (patterns.hasJob) {
|
|
105
|
-
notes.push('Hangfire: implement IJob interface, register in HangfireConfig.cs');
|
|
106
|
-
notes.push('Retry policy: use [AutomaticRetry(Attempts = 3)]');
|
|
107
|
-
notes.push('Logging: inject ILogger<TJob> for monitoring');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// AI/Agent
|
|
111
|
-
if (patterns.hasAI) {
|
|
112
|
-
notes.push('Microsoft Agent Framework: use AgentBuilder (not Semantic Kernel)');
|
|
113
|
-
notes.push('Prompts: store in /prompts/{agent-name}.md');
|
|
114
|
-
notes.push('Vector search: use EF Core 10 Vector Search (see standards/vector-search-rag.md)');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Azure/IaC
|
|
118
|
-
if (patterns.hasAzure) {
|
|
119
|
-
notes.push('Infrastructure as Code: ALWAYS use Bicep (never portal)');
|
|
120
|
-
notes.push('Cost validation: run morph-spec cost before commit');
|
|
121
|
-
notes.push('Deployment: Azure Container Apps (not App Service)');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Add spec reference
|
|
125
|
-
if (storyData.specShard) {
|
|
126
|
-
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec/${storyData.specShard}.md`);
|
|
127
|
-
} else {
|
|
128
|
-
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec.md`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return notes;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function renderStory(template, data) {
|
|
135
|
-
let rendered = template;
|
|
136
|
-
|
|
137
|
-
// Simple placeholder replacement (non-Mustache for simplicity)
|
|
138
|
-
const replacements = {
|
|
139
|
-
'{{STORY_ID}}': data.storyId || 'STORY-XXX',
|
|
140
|
-
'{{STORY_TITLE}}': data.storyTitle || 'Story Title',
|
|
141
|
-
'{{FEATURE_NAME}}': data.featureName || 'feature-name',
|
|
142
|
-
'{{FEATURE_NAME_TITLE}}': data.featureNameTitle || 'Feature Name',
|
|
143
|
-
'{{EPIC_NAME}}': data.epicName || 'Epic Name',
|
|
144
|
-
'{{DATE}}': new Date().toISOString().split('T')[0],
|
|
145
|
-
'{{STORY_CONTEXT}}': data.context || 'Context not provided',
|
|
146
|
-
'{{SPEC_SHARD}}': data.specShard || 'spec',
|
|
147
|
-
'{{EFFORT}}': data.effort || '1 day',
|
|
148
|
-
'{{CODING_PATTERN}}': data.codingPattern || 'general',
|
|
149
|
-
'{{ARCH_PATTERN}}': data.archPattern || 'clean-architecture',
|
|
150
|
-
'{{STATUS}}': 'Ready',
|
|
151
|
-
'{{CREATED_DATE}}': new Date().toISOString().split('T')[0],
|
|
152
|
-
'{{ADDITIONAL_NOTES}}': data.additionalNotes || '',
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
156
|
-
rendered = rendered.replaceAll(placeholder, value);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Handle arrays (tasks, dev notes, acceptance criteria)
|
|
160
|
-
if (data.tasks && data.tasks.length > 0) {
|
|
161
|
-
const tasksSection = data.tasks.map(t => `- [ ] ${t}`).join('\n');
|
|
162
|
-
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, tasksSection);
|
|
163
|
-
} else {
|
|
164
|
-
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, '- [ ] Task 1\n- [ ] Task 2');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (data.devNotes && data.devNotes.length > 0) {
|
|
168
|
-
const devNotesSection = data.devNotes.map(n => `- ${n}`).join('\n');
|
|
169
|
-
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, devNotesSection);
|
|
170
|
-
} else {
|
|
171
|
-
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, '- Follow standards in .morph/project/standards/');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (data.acceptanceCriteria && data.acceptanceCriteria.length > 0) {
|
|
175
|
-
const acSection = data.acceptanceCriteria.map(ac =>
|
|
176
|
-
`- [ ] **GIVEN** ${ac.given} **WHEN** ${ac.when} **THEN** ${ac.then}`
|
|
177
|
-
).join('\n');
|
|
178
|
-
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g, acSection);
|
|
179
|
-
} else {
|
|
180
|
-
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g,
|
|
181
|
-
'- [ ] **GIVEN** [condition] **WHEN** [action] **THEN** [result]');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Clean up remaining Mustache sections (empty arrays)
|
|
185
|
-
rendered = rendered.replace(/{{#\w+}}[\s\S]*?{{\/\w+}}/g, '');
|
|
186
|
-
|
|
187
|
-
return rendered;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function toTitleCase(str) {
|
|
191
|
-
return str
|
|
192
|
-
.split('-')
|
|
193
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
194
|
-
.join(' ');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ============================================================================
|
|
198
|
-
// Command Function
|
|
199
|
-
// ============================================================================
|
|
200
|
-
|
|
201
|
-
export async function createStoryCommand(feature, storyId, options) {
|
|
202
|
-
logger.header('MORPH-SPEC Story Creator');
|
|
203
|
-
logger.dim(`Feature: ${feature}`);
|
|
204
|
-
logger.dim(`Story ID: ${storyId}`);
|
|
205
|
-
logger.blank();
|
|
206
|
-
|
|
207
|
-
const spinner = ora('Creating story...').start();
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
// Read template
|
|
211
|
-
const template = readTemplate('story.md');
|
|
212
|
-
|
|
213
|
-
// Read spec (detect if sharded)
|
|
214
|
-
const spec = readSpec(feature);
|
|
215
|
-
|
|
216
|
-
// Build story data
|
|
217
|
-
const storyData = {
|
|
218
|
-
featureName: feature,
|
|
219
|
-
featureNameTitle: toTitleCase(feature),
|
|
220
|
-
storyId,
|
|
221
|
-
storyTitle: options.title || 'Story Title',
|
|
222
|
-
epicName: options.epic || toTitleCase(feature),
|
|
223
|
-
context: options.context || (spec.isSharded
|
|
224
|
-
? 'See sharded spec for complete context'
|
|
225
|
-
: extractContextFromSpec(spec.content, 'Overview')),
|
|
226
|
-
specShard: spec.isSharded ? 'index' : null,
|
|
227
|
-
tasks: options.tasks ? options.tasks.split(',').map(t => t.trim()) : ['Task 1', 'Task 2'],
|
|
228
|
-
effort: '1 day',
|
|
229
|
-
additionalNotes: '',
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// Detect patterns and inject Dev Notes
|
|
233
|
-
const patterns = detectPatterns(storyData.tasks, spec.content || '');
|
|
234
|
-
storyData.devNotes = injectDevNotes(patterns, storyData);
|
|
235
|
-
|
|
236
|
-
// Default acceptance criteria
|
|
237
|
-
storyData.acceptanceCriteria = [
|
|
238
|
-
{ given: '[condition]', when: '[action]', then: '[expected result]' },
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
// Render story
|
|
242
|
-
const renderedStory = renderStory(template, storyData);
|
|
243
|
-
|
|
244
|
-
// Output
|
|
245
|
-
const outputDir = path.join(process.cwd(), `.morph/project/outputs/${feature}/stories`);
|
|
246
|
-
const outputPath = path.join(outputDir, `${storyId}.md`);
|
|
247
|
-
|
|
248
|
-
if (options.dryRun) {
|
|
249
|
-
spinner.info('Dry run - preview only');
|
|
250
|
-
logger.blank();
|
|
251
|
-
console.log(renderedStory);
|
|
252
|
-
logger.blank();
|
|
253
|
-
logger.dim(`Would be written to: ${outputPath}`);
|
|
254
|
-
} else {
|
|
255
|
-
// Ensure directory exists
|
|
256
|
-
await ensureDir(outputDir);
|
|
257
|
-
|
|
258
|
-
// Write file
|
|
259
|
-
await writeFile(outputPath, renderedStory);
|
|
260
|
-
|
|
261
|
-
// ============================================================================
|
|
262
|
-
// Update sprint-status.yaml (create if doesn't exist)
|
|
263
|
-
// ============================================================================
|
|
264
|
-
const sprintStatusPath = path.join(process.cwd(), `.morph/project/outputs/${feature}/sprint-status.yaml`);
|
|
265
|
-
let sprintStatus;
|
|
266
|
-
|
|
267
|
-
if (fs.existsSync(sprintStatusPath)) {
|
|
268
|
-
// Load existing
|
|
269
|
-
const content = fs.readFileSync(sprintStatusPath, 'utf-8');
|
|
270
|
-
sprintStatus = yaml.parse(content);
|
|
271
|
-
} else {
|
|
272
|
-
// Create new
|
|
273
|
-
sprintStatus = {
|
|
274
|
-
feature: feature,
|
|
275
|
-
epic: options.epic || toTitleCase(feature),
|
|
276
|
-
created: new Date().toISOString().split('T')[0],
|
|
277
|
-
updated: new Date().toISOString().split('T')[0],
|
|
278
|
-
stories: [],
|
|
279
|
-
metrics: {
|
|
280
|
-
total_stories: 0,
|
|
281
|
-
ready: 0,
|
|
282
|
-
in_progress: 0,
|
|
283
|
-
ready_for_qa: 0,
|
|
284
|
-
done: 0,
|
|
285
|
-
completion_percent: 0
|
|
286
|
-
},
|
|
287
|
-
current: null,
|
|
288
|
-
next: null
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Add new story to sprint-status
|
|
293
|
-
sprintStatus.stories.push({
|
|
294
|
-
id: storyId,
|
|
295
|
-
title: storyData.storyTitle,
|
|
296
|
-
file: `stories/${storyId}.md`,
|
|
297
|
-
status: 'ready',
|
|
298
|
-
created: new Date().toISOString().split('T')[0],
|
|
299
|
-
assigned: null,
|
|
300
|
-
started: null,
|
|
301
|
-
completed: null
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Update metrics
|
|
305
|
-
sprintStatus.metrics.total_stories = sprintStatus.stories.length;
|
|
306
|
-
sprintStatus.metrics.ready = sprintStatus.stories.filter(s => s.status === 'ready').length;
|
|
307
|
-
sprintStatus.metrics.in_progress = sprintStatus.stories.filter(s => s.status === 'in_progress').length;
|
|
308
|
-
sprintStatus.metrics.ready_for_qa = sprintStatus.stories.filter(s => s.status === 'ready_for_qa').length;
|
|
309
|
-
sprintStatus.metrics.done = sprintStatus.stories.filter(s => s.status === 'done').length;
|
|
310
|
-
sprintStatus.metrics.completion_percent = sprintStatus.stories.length > 0
|
|
311
|
-
? Math.round((sprintStatus.metrics.done / sprintStatus.stories.length) * 100)
|
|
312
|
-
: 0;
|
|
313
|
-
sprintStatus.updated = new Date().toISOString().split('T')[0];
|
|
314
|
-
|
|
315
|
-
// Set next story if null
|
|
316
|
-
if (!sprintStatus.next) {
|
|
317
|
-
sprintStatus.next = {
|
|
318
|
-
story_id: storyId,
|
|
319
|
-
recommendation: `Story ${storyId} is ready for development`
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Write sprint-status.yaml
|
|
324
|
-
const yamlContent = yaml.stringify(sprintStatus);
|
|
325
|
-
fs.writeFileSync(sprintStatusPath, yamlContent);
|
|
326
|
-
|
|
327
|
-
spinner.succeed('Story created!');
|
|
328
|
-
logger.blank();
|
|
329
|
-
|
|
330
|
-
logger.success(`Story file: ${chalk.cyan(outputPath)}`);
|
|
331
|
-
logger.success(`Updated: ${chalk.cyan('sprint-status.yaml')}`);
|
|
332
|
-
logger.blank();
|
|
333
|
-
|
|
334
|
-
logger.header('Dev Notes Auto-Injected:');
|
|
335
|
-
storyData.devNotes.forEach(note => logger.dim(` - ${note}`));
|
|
336
|
-
logger.blank();
|
|
337
|
-
|
|
338
|
-
logger.header('Next Steps:');
|
|
339
|
-
logger.dim(' 1. Review and customize story file');
|
|
340
|
-
logger.dim(' 2. Run in fresh Claude session: /dev → implement story');
|
|
341
|
-
logger.dim(' 3. Dev adds implementation notes');
|
|
342
|
-
logger.dim(' 4. QA reviews and adds QA notes');
|
|
343
|
-
logger.blank();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
} catch (error) {
|
|
347
|
-
spinner.fail('Failed to create story');
|
|
348
|
-
logger.error(error.message);
|
|
349
|
-
process.exit(1);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Story Creator
|
|
3
|
+
* Creates self-contained story files with auto-injected Dev Notes
|
|
4
|
+
* Inspired by BMAD Method story-driven development
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import yaml from 'yaml';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
import { ensureDir, writeFile } from '../utils/file-copier.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helper Functions
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
function readTemplate(templateName) {
|
|
20
|
+
const templatePath = path.join(process.cwd(), 'node_modules/@polymorphism-tech/morph-spec/content/.morph/templates', templateName);
|
|
21
|
+
|
|
22
|
+
// Fallback to local if in development
|
|
23
|
+
if (!fs.existsSync(templatePath)) {
|
|
24
|
+
const localPath = path.join(process.cwd(), `content/.morph/templates/${templateName}`);
|
|
25
|
+
if (fs.existsSync(localPath)) {
|
|
26
|
+
return fs.readFileSync(localPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readSpec(featureName) {
|
|
35
|
+
const specPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec.md`);
|
|
36
|
+
const shardedIndexPath = path.join(process.cwd(), `.morph/project/outputs/${featureName}/spec/index.md`);
|
|
37
|
+
|
|
38
|
+
// Try sharded spec first (BMAD pattern)
|
|
39
|
+
if (fs.existsSync(shardedIndexPath)) {
|
|
40
|
+
return { isSharded: true, indexPath: shardedIndexPath };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback to monolithic spec
|
|
44
|
+
if (fs.existsSync(specPath)) {
|
|
45
|
+
return { isSharded: false, content: fs.readFileSync(specPath, 'utf-8') };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Spec not found for feature: ${featureName}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractContextFromSpec(specContent, sectionHeading) {
|
|
52
|
+
// Extract section content between ## heading and next ##
|
|
53
|
+
const regex = new RegExp(`## ${sectionHeading}\\s*([\\s\\S]*?)(?=\\n## |$)`, 'i');
|
|
54
|
+
const match = specContent.match(regex);
|
|
55
|
+
return match ? match[1].trim() : 'See spec.md for complete context';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectPatterns(tasks, specContent) {
|
|
59
|
+
const patterns = {
|
|
60
|
+
hasEntity: tasks.some(t => /entity|model|domain/i.test(t)),
|
|
61
|
+
hasService: tasks.some(t => /service|business|logic/i.test(t)),
|
|
62
|
+
hasComponent: tasks.some(t => /component|razor|blazor|ui/i.test(t)),
|
|
63
|
+
hasRepository: tasks.some(t => /repository|data|persistence/i.test(t)),
|
|
64
|
+
hasJob: tasks.some(t => /job|background|hangfire|scheduled/i.test(t)),
|
|
65
|
+
hasAI: tasks.some(t => /agent|ai|llm|semantic|rag/i.test(t)),
|
|
66
|
+
hasAzure: tasks.some(t => /bicep|azure|infra|deploy/i.test(t)),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return patterns;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function injectDevNotes(patterns, storyData) {
|
|
73
|
+
const notes = [];
|
|
74
|
+
|
|
75
|
+
// Entity pattern
|
|
76
|
+
if (patterns.hasEntity) {
|
|
77
|
+
notes.push('Use Primary Constructor (.NET 10 feature)');
|
|
78
|
+
notes.push('Follow entity pattern: .morph/project/standards/coding.md#entity-pattern');
|
|
79
|
+
notes.push('Navigation properties: configure in DbContext with Fluent API');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Service pattern
|
|
83
|
+
if (patterns.hasService) {
|
|
84
|
+
notes.push('Service layer: implement interface-first (IXService → XService)');
|
|
85
|
+
notes.push('Dependency injection: register in Program.cs as Scoped');
|
|
86
|
+
notes.push('Error handling: use Result<T> pattern from standards/architecture.md');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Blazor component
|
|
90
|
+
if (patterns.hasComponent) {
|
|
91
|
+
notes.push('UI Library: Fluent UI Blazor (see .morph/project/standards/ui.md)');
|
|
92
|
+
notes.push('Design System: use CSS variables from wwwroot/css/design-system.css');
|
|
93
|
+
notes.push('State management: @inject for services, [Parameter] for props');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Repository pattern
|
|
97
|
+
if (patterns.hasRepository) {
|
|
98
|
+
notes.push('Repository pattern: IRepository<T> generic base');
|
|
99
|
+
notes.push('EF Core: use AsNoTracking() for read-only queries');
|
|
100
|
+
notes.push('Transactions: wrap in DbContext.Database.BeginTransactionAsync()');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Background job
|
|
104
|
+
if (patterns.hasJob) {
|
|
105
|
+
notes.push('Hangfire: implement IJob interface, register in HangfireConfig.cs');
|
|
106
|
+
notes.push('Retry policy: use [AutomaticRetry(Attempts = 3)]');
|
|
107
|
+
notes.push('Logging: inject ILogger<TJob> for monitoring');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// AI/Agent
|
|
111
|
+
if (patterns.hasAI) {
|
|
112
|
+
notes.push('Microsoft Agent Framework: use AgentBuilder (not Semantic Kernel)');
|
|
113
|
+
notes.push('Prompts: store in /prompts/{agent-name}.md');
|
|
114
|
+
notes.push('Vector search: use EF Core 10 Vector Search (see standards/vector-search-rag.md)');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Azure/IaC
|
|
118
|
+
if (patterns.hasAzure) {
|
|
119
|
+
notes.push('Infrastructure as Code: ALWAYS use Bicep (never portal)');
|
|
120
|
+
notes.push('Cost validation: run morph-spec cost before commit');
|
|
121
|
+
notes.push('Deployment: Azure Container Apps (not App Service)');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add spec reference
|
|
125
|
+
if (storyData.specShard) {
|
|
126
|
+
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec/${storyData.specShard}.md`);
|
|
127
|
+
} else {
|
|
128
|
+
notes.push(`Spec reference: .morph/project/outputs/${storyData.featureName}/spec.md`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return notes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderStory(template, data) {
|
|
135
|
+
let rendered = template;
|
|
136
|
+
|
|
137
|
+
// Simple placeholder replacement (non-Mustache for simplicity)
|
|
138
|
+
const replacements = {
|
|
139
|
+
'{{STORY_ID}}': data.storyId || 'STORY-XXX',
|
|
140
|
+
'{{STORY_TITLE}}': data.storyTitle || 'Story Title',
|
|
141
|
+
'{{FEATURE_NAME}}': data.featureName || 'feature-name',
|
|
142
|
+
'{{FEATURE_NAME_TITLE}}': data.featureNameTitle || 'Feature Name',
|
|
143
|
+
'{{EPIC_NAME}}': data.epicName || 'Epic Name',
|
|
144
|
+
'{{DATE}}': new Date().toISOString().split('T')[0],
|
|
145
|
+
'{{STORY_CONTEXT}}': data.context || 'Context not provided',
|
|
146
|
+
'{{SPEC_SHARD}}': data.specShard || 'spec',
|
|
147
|
+
'{{EFFORT}}': data.effort || '1 day',
|
|
148
|
+
'{{CODING_PATTERN}}': data.codingPattern || 'general',
|
|
149
|
+
'{{ARCH_PATTERN}}': data.archPattern || 'clean-architecture',
|
|
150
|
+
'{{STATUS}}': 'Ready',
|
|
151
|
+
'{{CREATED_DATE}}': new Date().toISOString().split('T')[0],
|
|
152
|
+
'{{ADDITIONAL_NOTES}}': data.additionalNotes || '',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
156
|
+
rendered = rendered.replaceAll(placeholder, value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle arrays (tasks, dev notes, acceptance criteria)
|
|
160
|
+
if (data.tasks && data.tasks.length > 0) {
|
|
161
|
+
const tasksSection = data.tasks.map(t => `- [ ] ${t}`).join('\n');
|
|
162
|
+
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, tasksSection);
|
|
163
|
+
} else {
|
|
164
|
+
rendered = rendered.replace(/{{#TASKS}}[\s\S]*?{{\/TASKS}}/g, '- [ ] Task 1\n- [ ] Task 2');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (data.devNotes && data.devNotes.length > 0) {
|
|
168
|
+
const devNotesSection = data.devNotes.map(n => `- ${n}`).join('\n');
|
|
169
|
+
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, devNotesSection);
|
|
170
|
+
} else {
|
|
171
|
+
rendered = rendered.replace(/{{#DEV_NOTES}}[\s\S]*?{{\/DEV_NOTES}}/g, '- Follow standards in .morph/project/standards/');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (data.acceptanceCriteria && data.acceptanceCriteria.length > 0) {
|
|
175
|
+
const acSection = data.acceptanceCriteria.map(ac =>
|
|
176
|
+
`- [ ] **GIVEN** ${ac.given} **WHEN** ${ac.when} **THEN** ${ac.then}`
|
|
177
|
+
).join('\n');
|
|
178
|
+
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g, acSection);
|
|
179
|
+
} else {
|
|
180
|
+
rendered = rendered.replace(/{{#ACCEPTANCE_CRITERIA}}[\s\S]*?{{\/ACCEPTANCE_CRITERIA}}/g,
|
|
181
|
+
'- [ ] **GIVEN** [condition] **WHEN** [action] **THEN** [result]');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clean up remaining Mustache sections (empty arrays)
|
|
185
|
+
rendered = rendered.replace(/{{#\w+}}[\s\S]*?{{\/\w+}}/g, '');
|
|
186
|
+
|
|
187
|
+
return rendered;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function toTitleCase(str) {
|
|
191
|
+
return str
|
|
192
|
+
.split('-')
|
|
193
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
194
|
+
.join(' ');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Command Function
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
export async function createStoryCommand(feature, storyId, options) {
|
|
202
|
+
logger.header('MORPH-SPEC Story Creator');
|
|
203
|
+
logger.dim(`Feature: ${feature}`);
|
|
204
|
+
logger.dim(`Story ID: ${storyId}`);
|
|
205
|
+
logger.blank();
|
|
206
|
+
|
|
207
|
+
const spinner = ora('Creating story...').start();
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Read template
|
|
211
|
+
const template = readTemplate('story.md');
|
|
212
|
+
|
|
213
|
+
// Read spec (detect if sharded)
|
|
214
|
+
const spec = readSpec(feature);
|
|
215
|
+
|
|
216
|
+
// Build story data
|
|
217
|
+
const storyData = {
|
|
218
|
+
featureName: feature,
|
|
219
|
+
featureNameTitle: toTitleCase(feature),
|
|
220
|
+
storyId,
|
|
221
|
+
storyTitle: options.title || 'Story Title',
|
|
222
|
+
epicName: options.epic || toTitleCase(feature),
|
|
223
|
+
context: options.context || (spec.isSharded
|
|
224
|
+
? 'See sharded spec for complete context'
|
|
225
|
+
: extractContextFromSpec(spec.content, 'Overview')),
|
|
226
|
+
specShard: spec.isSharded ? 'index' : null,
|
|
227
|
+
tasks: options.tasks ? options.tasks.split(',').map(t => t.trim()) : ['Task 1', 'Task 2'],
|
|
228
|
+
effort: '1 day',
|
|
229
|
+
additionalNotes: '',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Detect patterns and inject Dev Notes
|
|
233
|
+
const patterns = detectPatterns(storyData.tasks, spec.content || '');
|
|
234
|
+
storyData.devNotes = injectDevNotes(patterns, storyData);
|
|
235
|
+
|
|
236
|
+
// Default acceptance criteria
|
|
237
|
+
storyData.acceptanceCriteria = [
|
|
238
|
+
{ given: '[condition]', when: '[action]', then: '[expected result]' },
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// Render story
|
|
242
|
+
const renderedStory = renderStory(template, storyData);
|
|
243
|
+
|
|
244
|
+
// Output
|
|
245
|
+
const outputDir = path.join(process.cwd(), `.morph/project/outputs/${feature}/stories`);
|
|
246
|
+
const outputPath = path.join(outputDir, `${storyId}.md`);
|
|
247
|
+
|
|
248
|
+
if (options.dryRun) {
|
|
249
|
+
spinner.info('Dry run - preview only');
|
|
250
|
+
logger.blank();
|
|
251
|
+
console.log(renderedStory);
|
|
252
|
+
logger.blank();
|
|
253
|
+
logger.dim(`Would be written to: ${outputPath}`);
|
|
254
|
+
} else {
|
|
255
|
+
// Ensure directory exists
|
|
256
|
+
await ensureDir(outputDir);
|
|
257
|
+
|
|
258
|
+
// Write file
|
|
259
|
+
await writeFile(outputPath, renderedStory);
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Update sprint-status.yaml (create if doesn't exist)
|
|
263
|
+
// ============================================================================
|
|
264
|
+
const sprintStatusPath = path.join(process.cwd(), `.morph/project/outputs/${feature}/sprint-status.yaml`);
|
|
265
|
+
let sprintStatus;
|
|
266
|
+
|
|
267
|
+
if (fs.existsSync(sprintStatusPath)) {
|
|
268
|
+
// Load existing
|
|
269
|
+
const content = fs.readFileSync(sprintStatusPath, 'utf-8');
|
|
270
|
+
sprintStatus = yaml.parse(content);
|
|
271
|
+
} else {
|
|
272
|
+
// Create new
|
|
273
|
+
sprintStatus = {
|
|
274
|
+
feature: feature,
|
|
275
|
+
epic: options.epic || toTitleCase(feature),
|
|
276
|
+
created: new Date().toISOString().split('T')[0],
|
|
277
|
+
updated: new Date().toISOString().split('T')[0],
|
|
278
|
+
stories: [],
|
|
279
|
+
metrics: {
|
|
280
|
+
total_stories: 0,
|
|
281
|
+
ready: 0,
|
|
282
|
+
in_progress: 0,
|
|
283
|
+
ready_for_qa: 0,
|
|
284
|
+
done: 0,
|
|
285
|
+
completion_percent: 0
|
|
286
|
+
},
|
|
287
|
+
current: null,
|
|
288
|
+
next: null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add new story to sprint-status
|
|
293
|
+
sprintStatus.stories.push({
|
|
294
|
+
id: storyId,
|
|
295
|
+
title: storyData.storyTitle,
|
|
296
|
+
file: `stories/${storyId}.md`,
|
|
297
|
+
status: 'ready',
|
|
298
|
+
created: new Date().toISOString().split('T')[0],
|
|
299
|
+
assigned: null,
|
|
300
|
+
started: null,
|
|
301
|
+
completed: null
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Update metrics
|
|
305
|
+
sprintStatus.metrics.total_stories = sprintStatus.stories.length;
|
|
306
|
+
sprintStatus.metrics.ready = sprintStatus.stories.filter(s => s.status === 'ready').length;
|
|
307
|
+
sprintStatus.metrics.in_progress = sprintStatus.stories.filter(s => s.status === 'in_progress').length;
|
|
308
|
+
sprintStatus.metrics.ready_for_qa = sprintStatus.stories.filter(s => s.status === 'ready_for_qa').length;
|
|
309
|
+
sprintStatus.metrics.done = sprintStatus.stories.filter(s => s.status === 'done').length;
|
|
310
|
+
sprintStatus.metrics.completion_percent = sprintStatus.stories.length > 0
|
|
311
|
+
? Math.round((sprintStatus.metrics.done / sprintStatus.stories.length) * 100)
|
|
312
|
+
: 0;
|
|
313
|
+
sprintStatus.updated = new Date().toISOString().split('T')[0];
|
|
314
|
+
|
|
315
|
+
// Set next story if null
|
|
316
|
+
if (!sprintStatus.next) {
|
|
317
|
+
sprintStatus.next = {
|
|
318
|
+
story_id: storyId,
|
|
319
|
+
recommendation: `Story ${storyId} is ready for development`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Write sprint-status.yaml
|
|
324
|
+
const yamlContent = yaml.stringify(sprintStatus);
|
|
325
|
+
fs.writeFileSync(sprintStatusPath, yamlContent);
|
|
326
|
+
|
|
327
|
+
spinner.succeed('Story created!');
|
|
328
|
+
logger.blank();
|
|
329
|
+
|
|
330
|
+
logger.success(`Story file: ${chalk.cyan(outputPath)}`);
|
|
331
|
+
logger.success(`Updated: ${chalk.cyan('sprint-status.yaml')}`);
|
|
332
|
+
logger.blank();
|
|
333
|
+
|
|
334
|
+
logger.header('Dev Notes Auto-Injected:');
|
|
335
|
+
storyData.devNotes.forEach(note => logger.dim(` - ${note}`));
|
|
336
|
+
logger.blank();
|
|
337
|
+
|
|
338
|
+
logger.header('Next Steps:');
|
|
339
|
+
logger.dim(' 1. Review and customize story file');
|
|
340
|
+
logger.dim(' 2. Run in fresh Claude session: /dev → implement story');
|
|
341
|
+
logger.dim(' 3. Dev adds implementation notes');
|
|
342
|
+
logger.dim(' 4. QA reviews and adds QA notes');
|
|
343
|
+
logger.blank();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
} catch (error) {
|
|
347
|
+
spinner.fail('Failed to create story');
|
|
348
|
+
logger.error(error.message);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|