@polymorphism-tech/morph-spec 2.4.0 → 3.0.1
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 +75 -239
- package/bin/morph-spec.js +8 -0
- package/content/.claude/commands/morph-deploy.md +529 -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/level-2-domains/architecture/prompt-engineer.md +189 -0
- package/content/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +320 -0
- package/content/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +699 -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/.morph/config/agents.json +744 -358
- package/content/.morph/config/config.template.json +33 -0
- package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -0
- 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/hooks/README.md +158 -0
- package/content/.morph/hooks/task-completed.js +73 -0
- package/content/.morph/hooks/teammate-idle.js +68 -0
- package/content/.morph/schemas/tasks.schema.json +220 -220
- package/content/.morph/standards/agent-teams-workflow.md +474 -0
- package/content/.morph/templates/CONTEXT-FEATURE.md +276 -0
- package/content/.morph/templates/CONTEXT.md +170 -0
- package/content/.morph/templates/clarify-questions.md +159 -159
- 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/infra/azure-pipelines-deploy.yml +480 -0
- package/package.json +1 -2
- package/scripts/reorganize-skills.cjs +175 -0
- package/scripts/validate-agents-structure.cjs +52 -0
- package/scripts/validate-skills.cjs +180 -0
- package/src/commands/advance-phase.js +83 -0
- package/src/commands/deploy.js +780 -0
- package/src/commands/detect-agents.js +43 -6
- package/src/commands/detect.js +1 -1
- package/src/commands/generate-context.js +40 -0
- package/src/commands/state.js +2 -1
- package/src/commands/sync.js +1 -1
- package/src/commands/update.js +13 -1
- package/src/lib/context-generator.js +513 -0
- package/src/lib/design-system-detector.js +187 -0
- package/src/lib/design-system-scaffolder.js +299 -0
- package/src/lib/hook-executor.js +256 -0
- package/src/lib/spec-validator.js +258 -0
- package/src/lib/standards-context-injector.js +287 -0
- package/src/lib/state-manager.js +21 -4
- package/src/lib/team-orchestrator.js +322 -0
- package/src/lib/validation-runner.js +65 -13
- package/src/lib/validators/design-system-validator.js +231 -0
- package/src/utils/file-copier.js +9 -1
- /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
- /package/{detectors → src/lib/detectors}/config-detector.js +0 -0
- /package/{detectors → src/lib/detectors}/conversation-analyzer.js +0 -0
- /package/{detectors → src/lib/detectors}/index.js +0 -0
- /package/{detectors → src/lib/detectors}/standards-generator.js +0 -0
- /package/{detectors → src/lib/detectors}/structure-detector.js +0 -0
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { analyzeRequestComplexity } from '../lib/complexity-analyzer.js';
|
|
7
7
|
import { LearningSystem } from '../lib/learning-system.js';
|
|
8
|
+
import { loadStandardsForAgent, getStandardsListForAgent } from '../lib/standards-context-injector.js';
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const AGENTS_CONFIG_PATH = join(__dirname, '../../content/.morph/config/agents.json');
|
|
@@ -21,14 +22,31 @@ function loadAgentsConfig() {
|
|
|
21
22
|
|
|
22
23
|
function detectAgents(userInput, config) {
|
|
23
24
|
const input = userInput.toLowerCase();
|
|
24
|
-
const coreAgents = config.agents.core || [];
|
|
25
|
-
const specialists = config.agents.specialists || [];
|
|
26
25
|
|
|
26
|
+
// MORPH 3.0 Hierarchical: agents is a flat object, not arrays
|
|
27
|
+
const allAgents = Object.entries(config.agents || {})
|
|
28
|
+
.filter(([id]) => !id.startsWith('_comment'))
|
|
29
|
+
.map(([id, agent]) => ({ id, ...agent }));
|
|
30
|
+
|
|
31
|
+
// Core agents = always_active agents (typically Tier 1)
|
|
32
|
+
const coreAgents = allAgents.filter(a => a.always_active === true);
|
|
27
33
|
const coreIds = coreAgents.map(a => a.id);
|
|
34
|
+
|
|
35
|
+
// Specialist agents = keyword-matched agents
|
|
28
36
|
const specialistMatches = [];
|
|
29
37
|
|
|
30
|
-
for (const agent of
|
|
31
|
-
|
|
38
|
+
for (const agent of allAgents) {
|
|
39
|
+
if (agent.always_active) continue; // Skip core agents
|
|
40
|
+
|
|
41
|
+
// MORPH 3.0.1: Check negative keywords first — if any match, skip agent entirely
|
|
42
|
+
const negativeKeywords = agent.negativeKeywords || [];
|
|
43
|
+
const hasNegativeMatch = negativeKeywords.some(neg =>
|
|
44
|
+
input.includes(neg.toLowerCase())
|
|
45
|
+
);
|
|
46
|
+
if (hasNegativeMatch) continue;
|
|
47
|
+
|
|
48
|
+
// MORPH 3.0: keywords is directly on agent, not nested in autoActivation
|
|
49
|
+
const keywords = agent.keywords || [];
|
|
32
50
|
const matchedKeywords = [];
|
|
33
51
|
|
|
34
52
|
for (const keyword of keywords) {
|
|
@@ -40,8 +58,8 @@ function detectAgents(userInput, config) {
|
|
|
40
58
|
if (matchedKeywords.length > 0) {
|
|
41
59
|
specialistMatches.push({
|
|
42
60
|
id: agent.id,
|
|
43
|
-
name: agent.name
|
|
44
|
-
emoji: agent.
|
|
61
|
+
name: agent.title || agent.id, // Use title as name
|
|
62
|
+
emoji: agent.teammate?.icon || '🤖',
|
|
45
63
|
matchedKeywords,
|
|
46
64
|
matchCount: matchedKeywords.length
|
|
47
65
|
});
|
|
@@ -89,6 +107,8 @@ function formatVerbose(result) {
|
|
|
89
107
|
logger.blank();
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
export { detectAgents, loadAgentsConfig };
|
|
111
|
+
|
|
92
112
|
export function detectAgentsCommand(input, options) {
|
|
93
113
|
const userInput = Array.isArray(input) ? input.join(' ') : input;
|
|
94
114
|
|
|
@@ -127,6 +147,23 @@ export function detectAgentsCommand(input, options) {
|
|
|
127
147
|
})
|
|
128
148
|
.filter(Boolean);
|
|
129
149
|
|
|
150
|
+
// Enrich with standards context when JSON output requested
|
|
151
|
+
if (options.json) {
|
|
152
|
+
const projectPath = process.cwd();
|
|
153
|
+
result.standardsSummary = {};
|
|
154
|
+
|
|
155
|
+
for (const agentId of result.all) {
|
|
156
|
+
const standardsList = getStandardsListForAgent(agentId);
|
|
157
|
+
const { fullContent } = loadStandardsForAgent(agentId, projectPath);
|
|
158
|
+
|
|
159
|
+
result.standardsSummary[agentId] = {
|
|
160
|
+
standards: standardsList,
|
|
161
|
+
fullContent: fullContent,
|
|
162
|
+
count: standardsList.length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
130
167
|
if (options.json) {
|
|
131
168
|
console.log(JSON.stringify(result, null, 2));
|
|
132
169
|
} else if (options.verbose) {
|
package/src/commands/detect.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from 'path';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
-
import { detectProject, getDetectionSummary } from '
|
|
5
|
+
import { detectProject, getDetectionSummary } from '../lib/detectors/index.js';
|
|
6
6
|
import { ensureDir, writeFile } from '../utils/file-copier.js';
|
|
7
7
|
|
|
8
8
|
export async function detectCommand(options) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generate Context Command
|
|
3
|
+
*
|
|
4
|
+
* CLI: morph-spec generate context [feature]
|
|
5
|
+
*
|
|
6
|
+
* Generates CONTEXT.md (project-level) or CONTEXT-{feature}.md (feature-level)
|
|
7
|
+
* with all relevant project data for Claude's consumption.
|
|
8
|
+
*
|
|
9
|
+
* @module generate-context
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { generateContext } from '../lib/context-generator.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate context files
|
|
16
|
+
* @param {string} projectPath - Root path of the project
|
|
17
|
+
* @param {string} featureName - Optional feature name
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
export async function generateContextCommand(projectPath, featureName = null) {
|
|
21
|
+
console.log('📄 Generating CONTEXT files...\n');
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const results = await generateContext(projectPath, featureName);
|
|
25
|
+
|
|
26
|
+
console.log('✅ Generated:');
|
|
27
|
+
console.log(` - ${results.projectContext}`);
|
|
28
|
+
|
|
29
|
+
if (results.featureContext) {
|
|
30
|
+
console.log(` - ${results.featureContext}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('\n💡 Use these files to provide full context to Claude Code.');
|
|
34
|
+
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('❌ Error generating context:');
|
|
37
|
+
console.error(` ${err.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/commands/state.js
CHANGED
|
@@ -316,7 +316,8 @@ async function markOutputCommand(featureName, outputType, options) {
|
|
|
316
316
|
logger.dim(' Usage: morph-spec state mark-output <feature> <output-type>');
|
|
317
317
|
logger.blank();
|
|
318
318
|
logger.dim(' Valid types: proposal, spec, contracts, tasks, decisions, recap');
|
|
319
|
-
logger.dim(' uiDesignSystem, uiMockups
|
|
319
|
+
logger.dim(' uiDesignSystem (or ui-design-system), uiMockups (or ui-mockups)');
|
|
320
|
+
logger.dim(' uiComponents (or ui-components), uiFlows (or ui-flows)');
|
|
320
321
|
process.exit(1);
|
|
321
322
|
}
|
|
322
323
|
|
package/src/commands/sync.js
CHANGED
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { readFile, writeFile, ensureDir } from '../utils/file-copier.js';
|
|
7
|
-
import { analyzeConversation } from '
|
|
7
|
+
import { analyzeConversation } from '../lib/detectors/conversation-analyzer.js';
|
|
8
8
|
|
|
9
9
|
export async function syncCommand(options) {
|
|
10
10
|
const targetPath = options.path || process.cwd();
|
package/src/commands/update.js
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
copyDirectory,
|
|
9
9
|
pathExists,
|
|
10
10
|
ensureDir,
|
|
11
|
-
createDirectoryLink
|
|
11
|
+
createDirectoryLink,
|
|
12
|
+
readJson,
|
|
13
|
+
writeJson
|
|
12
14
|
} from '../utils/file-copier.js';
|
|
13
15
|
import {
|
|
14
16
|
checkCLIOutdated,
|
|
@@ -160,6 +162,16 @@ export async function updateCommand(options) {
|
|
|
160
162
|
updateSpinner.text = 'Saving version info...';
|
|
161
163
|
await saveProjectMorphVersion(targetPath, cliCheck.current);
|
|
162
164
|
|
|
165
|
+
// Update config.json frameworkVersion (BUG #9 fix)
|
|
166
|
+
const configPath = join(morphPath, 'config', 'config.json');
|
|
167
|
+
if (await pathExists(configPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const config = await readJson(configPath);
|
|
170
|
+
config.frameworkVersion = cliCheck.current;
|
|
171
|
+
await writeJson(configPath, config);
|
|
172
|
+
} catch { /* preserve existing config if read/write fails */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
updateSpinner.succeed('MORPH-SPEC updated successfully!');
|
|
164
176
|
logger.blank();
|
|
165
177
|
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Context Generator - Auto-generates CONTEXT.md files
|
|
3
|
+
*
|
|
4
|
+
* Populates CONTEXT.md templates with real project data from:
|
|
5
|
+
* - state.json (features, phases, tasks, checkpoints)
|
|
6
|
+
* - config.json (project settings, Azure resources, agent config)
|
|
7
|
+
* - agents.json (hierarchical agent structure)
|
|
8
|
+
* - project.md (project description)
|
|
9
|
+
* - standards/*.md (project-specific standards)
|
|
10
|
+
*
|
|
11
|
+
* @module context-generator
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load project state from state.json
|
|
19
|
+
* @param {string} projectPath - Root path of the project
|
|
20
|
+
* @returns {Promise<Object>} Parsed state
|
|
21
|
+
*/
|
|
22
|
+
async function loadState(projectPath) {
|
|
23
|
+
const statePath = path.join(projectPath, '.morph/state.json');
|
|
24
|
+
try {
|
|
25
|
+
const content = await fs.readFile(statePath, 'utf8');
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { features: [], checkpoints: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load project config from config.json
|
|
34
|
+
* @param {string} projectPath - Root path of the project
|
|
35
|
+
* @returns {Promise<Object>} Parsed config
|
|
36
|
+
*/
|
|
37
|
+
async function loadConfig(projectPath) {
|
|
38
|
+
const configPath = path.join(projectPath, '.morph/config/config.json');
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load agents hierarchy from agents.json
|
|
49
|
+
* @param {string} projectPath - Root path of the project
|
|
50
|
+
* @returns {Promise<Object>} Parsed agents config
|
|
51
|
+
*/
|
|
52
|
+
async function loadAgents(projectPath) {
|
|
53
|
+
const agentsPath = path.join(projectPath, 'content/.morph/config/agents.json');
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(agentsPath, 'utf8');
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load project description from project.md
|
|
64
|
+
* @param {string} projectPath - Root path of the project
|
|
65
|
+
* @returns {Promise<string>} Project description
|
|
66
|
+
*/
|
|
67
|
+
async function loadProjectDescription(projectPath) {
|
|
68
|
+
const projectMdPath = path.join(projectPath, '.morph/project.md');
|
|
69
|
+
try {
|
|
70
|
+
return await fs.readFile(projectMdPath, 'utf8');
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load project-specific standards
|
|
78
|
+
* @param {string} projectPath - Root path of the project
|
|
79
|
+
* @returns {Promise<Array>} Array of {name, path, description}
|
|
80
|
+
*/
|
|
81
|
+
async function loadProjectStandards(projectPath) {
|
|
82
|
+
const standardsDir = path.join(projectPath, '.morph/project/standards');
|
|
83
|
+
try {
|
|
84
|
+
const files = await fs.readdir(standardsDir);
|
|
85
|
+
return files
|
|
86
|
+
.filter(f => f.endsWith('.md'))
|
|
87
|
+
.map(f => ({
|
|
88
|
+
name: f.replace('.md', ''),
|
|
89
|
+
path: `standards/${f}`,
|
|
90
|
+
description: null
|
|
91
|
+
}));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get nested property value from object using dot notation
|
|
99
|
+
* @param {Object} obj - Object to get value from
|
|
100
|
+
* @param {string} path - Dot-notation path (e.g., 'tasks.completed')
|
|
101
|
+
* @returns {*} Value at path or empty string
|
|
102
|
+
*/
|
|
103
|
+
function getNestedValue(obj, path) {
|
|
104
|
+
const keys = path.split('.');
|
|
105
|
+
let value = obj;
|
|
106
|
+
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
if (value && typeof value === 'object' && key in value) {
|
|
109
|
+
value = value[key];
|
|
110
|
+
} else {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return value !== null && value !== undefined ? value : '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Simple template renderer (Handlebars-like syntax)
|
|
120
|
+
* @param {string} template - Template string with {{VAR}} placeholders
|
|
121
|
+
* @param {Object} data - Data object with values
|
|
122
|
+
* @returns {string} Rendered template
|
|
123
|
+
*/
|
|
124
|
+
function renderTemplate(template, data) {
|
|
125
|
+
let result = template;
|
|
126
|
+
|
|
127
|
+
// Handle {{#each VAR}}...{{/each}} blocks first (before variable replacement)
|
|
128
|
+
result = result.replace(/{{#each\s+(\w+)}}([\s\S]*?){{\/each}}/g, (_, varName, itemTemplate) => {
|
|
129
|
+
const array = data[varName];
|
|
130
|
+
if (!Array.isArray(array) || array.length === 0) return '';
|
|
131
|
+
|
|
132
|
+
return array.map(item => {
|
|
133
|
+
let itemResult = itemTemplate;
|
|
134
|
+
|
|
135
|
+
// Replace all {{prop}} and {{nested.prop}} in the item template
|
|
136
|
+
itemResult = itemResult.replace(/{{([\w.]+)}}/g, (__, propPath) => {
|
|
137
|
+
return String(getNestedValue(item, propPath));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return itemResult;
|
|
141
|
+
}).join('');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Handle {{#if VAR}}...{{/if}} blocks
|
|
145
|
+
result = result.replace(/{{#if\s+(\w+)}}([\s\S]*?){{\/if}}/g, (_, varName, content) => {
|
|
146
|
+
return data[varName] ? content : '';
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Handle {{#unless VAR}}...{{/unless}} blocks
|
|
150
|
+
result = result.replace(/{{#unless\s+(\w+)}}([\s\S]*?){{\/unless}}/g, (_, varName, content) => {
|
|
151
|
+
const value = data[varName];
|
|
152
|
+
// Check if value is falsy, empty array, or empty object
|
|
153
|
+
const isFalsy = !value ||
|
|
154
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
155
|
+
(typeof value === 'object' && Object.keys(value).length === 0);
|
|
156
|
+
return isFalsy ? content : '';
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Replace simple {{VAR}} and {{nested.prop}} placeholders
|
|
160
|
+
result = result.replace(/{{([\w.]+)}}/g, (_, path) => {
|
|
161
|
+
return String(getNestedValue(data, path));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate project-level CONTEXT.md
|
|
169
|
+
* @param {string} projectPath - Root path of the project
|
|
170
|
+
* @returns {Promise<string>} Generated CONTEXT.md content
|
|
171
|
+
*/
|
|
172
|
+
export async function generateProjectContext(projectPath) {
|
|
173
|
+
const [state, config, agents, projectDesc, standards] = await Promise.all([
|
|
174
|
+
loadState(projectPath),
|
|
175
|
+
loadConfig(projectPath),
|
|
176
|
+
loadAgents(projectPath),
|
|
177
|
+
loadProjectDescription(projectPath),
|
|
178
|
+
loadProjectStandards(projectPath)
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// Load template
|
|
182
|
+
const templatePath = path.join(projectPath, 'content/.morph/templates/CONTEXT.md');
|
|
183
|
+
const template = await fs.readFile(templatePath, 'utf8');
|
|
184
|
+
|
|
185
|
+
// Build data object
|
|
186
|
+
const data = {
|
|
187
|
+
PROJECT_NAME: config?.project?.name || 'Unknown Project',
|
|
188
|
+
PROJECT_TYPE: config?.project?.type || 'blazor-server',
|
|
189
|
+
PROJECT_DESCRIPTION: projectDesc || config?.project?.description || '',
|
|
190
|
+
REPOSITORY_URL: config?.project?.repository || '',
|
|
191
|
+
TIMESTAMP: new Date().toISOString(),
|
|
192
|
+
|
|
193
|
+
// Stack detection
|
|
194
|
+
BLAZOR_ENABLED: config?.project?.type?.includes('blazor') ? true : false,
|
|
195
|
+
NEXTJS_ENABLED: config?.project?.type?.includes('nextjs') ? true : false,
|
|
196
|
+
DOTNET_VERSION: '10.0',
|
|
197
|
+
EF_VERSION: '10.0',
|
|
198
|
+
|
|
199
|
+
// Azure config
|
|
200
|
+
AZURE_LOCATION: config?.azure?.location || 'eastus2',
|
|
201
|
+
CONTAINER_APPS_ENABLED: config?.azure?.resources?.containerApps?.enabled || false,
|
|
202
|
+
CONTAINER_ENV: config?.azure?.resources?.containerApps?.environment || '',
|
|
203
|
+
SQL_ENABLED: config?.azure?.resources?.sql?.enabled || false,
|
|
204
|
+
SQL_SERVER: config?.azure?.resources?.sql?.server || '',
|
|
205
|
+
SQL_DATABASE: config?.azure?.resources?.sql?.database || '',
|
|
206
|
+
OPENAI_ENABLED: config?.azure?.resources?.openai?.enabled || false,
|
|
207
|
+
OPENAI_MODEL: config?.azure?.resources?.openai?.model || '',
|
|
208
|
+
MONITORING_ENABLED: config?.azure?.resources?.monitoring?.enabled || false,
|
|
209
|
+
APP_INSIGHTS: config?.azure?.resources?.monitoring?.appInsights || '',
|
|
210
|
+
|
|
211
|
+
// Active features (convert object to array if needed)
|
|
212
|
+
ACTIVE_FEATURES: (Array.isArray(state?.features)
|
|
213
|
+
? state.features
|
|
214
|
+
: Object.entries(state?.features || {}).map(([name, data]) => ({ name, ...data }))
|
|
215
|
+
).map(f => ({
|
|
216
|
+
name: f.name,
|
|
217
|
+
phase: f.phase,
|
|
218
|
+
status: f.status,
|
|
219
|
+
complexity: f.complexity || 'medium',
|
|
220
|
+
activeAgents: (f.activeAgents || []).join(', '),
|
|
221
|
+
description: f.description || '',
|
|
222
|
+
tasks: {
|
|
223
|
+
completed: f.tasks?.completed || 0,
|
|
224
|
+
total: f.tasks?.total || 0
|
|
225
|
+
},
|
|
226
|
+
outputs: Array.isArray(f.outputs)
|
|
227
|
+
? f.outputs.join(', ')
|
|
228
|
+
: (f.outputs ? Object.keys(f.outputs).join(', ') : '')
|
|
229
|
+
})),
|
|
230
|
+
|
|
231
|
+
// Project standards
|
|
232
|
+
PROJECT_STANDARDS: standards,
|
|
233
|
+
|
|
234
|
+
// Agents by tier
|
|
235
|
+
AGENT_COUNT: agents?.total_agents || 0,
|
|
236
|
+
TIER_1_AGENTS: Object.entries(agents?.agents || {})
|
|
237
|
+
.filter(([id, agent]) => !id.startsWith('_') && agent.tier === 1)
|
|
238
|
+
.map(([id, agent]) => ({
|
|
239
|
+
id,
|
|
240
|
+
title: agent.title,
|
|
241
|
+
always_active: agent.always_active
|
|
242
|
+
})),
|
|
243
|
+
TIER_2_AGENTS: Object.entries(agents?.agents || {})
|
|
244
|
+
.filter(([id, agent]) => !id.startsWith('_') && agent.tier === 2)
|
|
245
|
+
.map(([id, agent]) => ({
|
|
246
|
+
id,
|
|
247
|
+
title: agent.title,
|
|
248
|
+
domains: (agent.domains || []).join(', ')
|
|
249
|
+
})),
|
|
250
|
+
TIER_3_AGENTS: Object.entries(agents?.agents || {})
|
|
251
|
+
.filter(([id, agent]) => !id.startsWith('_') && agent.tier === 3)
|
|
252
|
+
.map(([id, agent]) => ({
|
|
253
|
+
id,
|
|
254
|
+
title: agent.title,
|
|
255
|
+
reports_to: agent.relationships?.reports_to || 'N/A'
|
|
256
|
+
})),
|
|
257
|
+
|
|
258
|
+
// Agent Teams config
|
|
259
|
+
AGENT_TEAMS_ENABLED: config?.agentTeams?.enabled || false,
|
|
260
|
+
AGENT_TEAMS_DISPLAY_MODE: config?.agentTeams?.displayMode || 'auto',
|
|
261
|
+
AGENT_TEAMS_THRESHOLDS: config?.agentTeams?.spawnThresholds || {},
|
|
262
|
+
|
|
263
|
+
// Recent checkpoints
|
|
264
|
+
RECENT_CHECKPOINTS: (state?.checkpoints || []).slice(-5).map(cp => ({
|
|
265
|
+
timestamp: cp.timestamp,
|
|
266
|
+
feature: cp.feature,
|
|
267
|
+
phase: cp.phase,
|
|
268
|
+
note: cp.note,
|
|
269
|
+
validation: cp.validation
|
|
270
|
+
})),
|
|
271
|
+
|
|
272
|
+
// DevOps config
|
|
273
|
+
DEVOPS_ENABLED: config?.devops ? true : false,
|
|
274
|
+
DEVOPS_ORG: config?.devops?.organization || '',
|
|
275
|
+
DEVOPS_PROJECT: config?.devops?.project || '',
|
|
276
|
+
DEVOPS_BOARDS_ENABLED: config?.devops?.boards?.enabled || false,
|
|
277
|
+
DEVOPS_AREA_PATH: config?.devops?.boards?.areaPath || '',
|
|
278
|
+
DEVOPS_WIKI_ENABLED: config?.devops?.wiki?.enabled || false,
|
|
279
|
+
DEVOPS_WIKI_NAME: config?.devops?.wiki?.wikiName || '',
|
|
280
|
+
DEVOPS_BUILD_PIPELINE: config?.devops?.pipelines?.buildPipeline || '',
|
|
281
|
+
DEVOPS_RELEASE_PIPELINE: config?.devops?.pipelines?.releasePipeline || '',
|
|
282
|
+
|
|
283
|
+
// Version
|
|
284
|
+
MORPH_VERSION: '3.0.0-hierarchical'
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return renderTemplate(template, data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate feature-specific CONTEXT-{feature}.md
|
|
292
|
+
* @param {string} projectPath - Root path of the project
|
|
293
|
+
* @param {string} featureName - Feature name
|
|
294
|
+
* @returns {Promise<string>} Generated CONTEXT-{feature}.md content
|
|
295
|
+
*/
|
|
296
|
+
export async function generateFeatureContext(projectPath, featureName) {
|
|
297
|
+
const [state, agents] = await Promise.all([
|
|
298
|
+
loadState(projectPath),
|
|
299
|
+
loadAgents(projectPath)
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
// Find feature in state (handle both array and object format)
|
|
303
|
+
let feature;
|
|
304
|
+
if (Array.isArray(state?.features)) {
|
|
305
|
+
feature = state.features.find(f => f.name === featureName);
|
|
306
|
+
} else if (state?.features && typeof state.features === 'object') {
|
|
307
|
+
feature = state.features[featureName];
|
|
308
|
+
if (feature) {
|
|
309
|
+
feature = { name: featureName, ...feature };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!feature) {
|
|
314
|
+
throw new Error(`Feature "${featureName}" not found in state.json`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Load feature outputs
|
|
318
|
+
const outputsDir = path.join(projectPath, `.morph/project/outputs/${featureName}`);
|
|
319
|
+
let decisions = [];
|
|
320
|
+
let tasks = [];
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const decisionsPath = path.join(outputsDir, 'decisions.md');
|
|
324
|
+
const decisionsContent = await fs.readFile(decisionsPath, 'utf8');
|
|
325
|
+
// Parse decisions.md (simplified - just extract ADR headers)
|
|
326
|
+
const adrMatches = decisionsContent.matchAll(/##\s+ADR-(\d+):\s+(.+)/g);
|
|
327
|
+
decisions = Array.from(adrMatches).map(match => ({
|
|
328
|
+
id: match[1],
|
|
329
|
+
title: match[2],
|
|
330
|
+
status: 'Accepted',
|
|
331
|
+
date: new Date().toISOString().split('T')[0],
|
|
332
|
+
context: '',
|
|
333
|
+
decision: ''
|
|
334
|
+
}));
|
|
335
|
+
} catch (err) {
|
|
336
|
+
// No decisions.md yet
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const tasksPath = path.join(outputsDir, 'tasks.json');
|
|
341
|
+
const tasksContent = await fs.readFile(tasksPath, 'utf8');
|
|
342
|
+
tasks = JSON.parse(tasksContent).tasks || [];
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// No tasks.json yet
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Load template
|
|
348
|
+
const templatePath = path.join(projectPath, 'content/.morph/templates/CONTEXT-FEATURE.md');
|
|
349
|
+
const template = await fs.readFile(templatePath, 'utf8');
|
|
350
|
+
|
|
351
|
+
// Build active agents list with hierarchy
|
|
352
|
+
const activeAgentIds = feature.activeAgents || [];
|
|
353
|
+
const activeAgents = activeAgentIds
|
|
354
|
+
.filter(id => agents?.agents[id])
|
|
355
|
+
.map(id => ({ id, ...agents.agents[id] }));
|
|
356
|
+
|
|
357
|
+
// Group by tier and build squads
|
|
358
|
+
const teamLead = activeAgents.find(a => a.tier === 1 && a.role === 'orchestrator');
|
|
359
|
+
const domainLeaders = activeAgents.filter(a => a.tier === 2 && a.role === 'domain-leader');
|
|
360
|
+
const specialists = activeAgents.filter(a => a.tier === 3 && a.role === 'specialist');
|
|
361
|
+
const validators = activeAgents.filter(a => a.tier === 4 && a.role === 'validator');
|
|
362
|
+
|
|
363
|
+
const squads = domainLeaders.map(leader => ({
|
|
364
|
+
leader: {
|
|
365
|
+
id: leader.id,
|
|
366
|
+
title: leader.title
|
|
367
|
+
},
|
|
368
|
+
members: specialists
|
|
369
|
+
.filter(s => s.relationships?.reports_to === leader.id)
|
|
370
|
+
.map(s => ({
|
|
371
|
+
id: s.id,
|
|
372
|
+
title: s.title
|
|
373
|
+
}))
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
// Build data object
|
|
377
|
+
const data = {
|
|
378
|
+
FEATURE_NAME: feature.name,
|
|
379
|
+
PHASE: feature.phase,
|
|
380
|
+
WORKFLOW_TYPE: feature.workflow || 'standard',
|
|
381
|
+
STATUS: feature.status,
|
|
382
|
+
COMPLEXITY: feature.complexity || 'medium',
|
|
383
|
+
DESCRIPTION: feature.description || '',
|
|
384
|
+
TIMESTAMP: new Date().toISOString(),
|
|
385
|
+
|
|
386
|
+
// Phase tracking
|
|
387
|
+
PHASE_DESCRIPTION: `Current phase: ${feature.phase}`,
|
|
388
|
+
COMPLETED_PHASES: [], // TODO: track phase history
|
|
389
|
+
UPCOMING_PHASES: [], // TODO: calculate remaining phases
|
|
390
|
+
|
|
391
|
+
// Tasks
|
|
392
|
+
TASKS_TOTAL: feature.tasks?.total || tasks.length,
|
|
393
|
+
TASKS_COMPLETED: feature.tasks?.completed || tasks.filter(t => t.status === 'completed').length,
|
|
394
|
+
TASKS_IN_PROGRESS: tasks.filter(t => t.status === 'in_progress').length,
|
|
395
|
+
TASKS_BLOCKED: tasks.filter(t => t.status === 'blocked').length,
|
|
396
|
+
TASKS_PROGRESS: feature.tasks?.total > 0
|
|
397
|
+
? Math.round((feature.tasks.completed / feature.tasks.total) * 100)
|
|
398
|
+
: 0,
|
|
399
|
+
CURRENT_TASK: tasks.find(t => t.status === 'in_progress') || null,
|
|
400
|
+
|
|
401
|
+
// Active agents
|
|
402
|
+
ACTIVE_AGENTS_COUNT: activeAgents.length,
|
|
403
|
+
TEAM_LEAD: teamLead ? {
|
|
404
|
+
id: teamLead.id,
|
|
405
|
+
title: teamLead.title
|
|
406
|
+
} : null,
|
|
407
|
+
SQUADS: squads,
|
|
408
|
+
VALIDATORS: validators.map(v => ({
|
|
409
|
+
id: v.id,
|
|
410
|
+
title: v.title
|
|
411
|
+
})),
|
|
412
|
+
|
|
413
|
+
// Outputs (handle both object and array format)
|
|
414
|
+
OUTPUTS: (Array.isArray(feature.outputs)
|
|
415
|
+
? feature.outputs.map(output => ({
|
|
416
|
+
type: output,
|
|
417
|
+
generated: true,
|
|
418
|
+
path: `.morph/project/outputs/${featureName}/${output}`,
|
|
419
|
+
generatedAt: null
|
|
420
|
+
}))
|
|
421
|
+
: Object.entries(feature.outputs || {}).map(([type, data]) => ({
|
|
422
|
+
type,
|
|
423
|
+
generated: data.created || false,
|
|
424
|
+
path: data.path || `.morph/project/outputs/${featureName}/${type}`,
|
|
425
|
+
generatedAt: data.createdAt || null
|
|
426
|
+
}))
|
|
427
|
+
),
|
|
428
|
+
|
|
429
|
+
// Decisions
|
|
430
|
+
DECISIONS: decisions,
|
|
431
|
+
|
|
432
|
+
// Validation
|
|
433
|
+
VALIDATION_RESULTS: feature.lastValidation?.results || null,
|
|
434
|
+
VALIDATION_PASSED: feature.lastValidation?.passed || false,
|
|
435
|
+
VALIDATION_TIMESTAMP: feature.lastValidation?.timestamp || null,
|
|
436
|
+
|
|
437
|
+
// Tasks list
|
|
438
|
+
TASKS: tasks,
|
|
439
|
+
|
|
440
|
+
// Checkpoints
|
|
441
|
+
CHECKPOINTS: (state?.checkpoints || [])
|
|
442
|
+
.filter(cp => cp.feature === featureName)
|
|
443
|
+
.map(cp => ({
|
|
444
|
+
timestamp: cp.timestamp,
|
|
445
|
+
phase: cp.phase,
|
|
446
|
+
tasksCompleted: cp.tasksCompleted,
|
|
447
|
+
tasksTotal: cp.tasksTotal,
|
|
448
|
+
note: cp.note,
|
|
449
|
+
validation: cp.validation
|
|
450
|
+
})),
|
|
451
|
+
|
|
452
|
+
// Related standards
|
|
453
|
+
RELATED_STANDARDS: [], // TODO: link standards based on activeAgents
|
|
454
|
+
|
|
455
|
+
// File paths
|
|
456
|
+
CURRENT_TASK_ID: tasks.find(t => t.status === 'in_progress')?.id || '',
|
|
457
|
+
PROPOSAL_PATH: `.morph/project/outputs/${featureName}/proposal.md`,
|
|
458
|
+
SPEC_PATH: `.morph/project/outputs/${featureName}/spec.md`,
|
|
459
|
+
CONTRACTS_PATH: `.morph/project/outputs/${featureName}/contracts.cs`,
|
|
460
|
+
TASKS_PATH: `.morph/project/outputs/${featureName}/tasks.json`,
|
|
461
|
+
DECISIONS_PATH: `.morph/project/outputs/${featureName}/decisions.md`,
|
|
462
|
+
RECAP_PATH: `.morph/project/outputs/${featureName}/recap.md`,
|
|
463
|
+
|
|
464
|
+
// Version
|
|
465
|
+
MORPH_VERSION: '3.0.0-hierarchical',
|
|
466
|
+
FEATURE_ID: feature.id || featureName
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
return renderTemplate(template, data);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Write generated context to file
|
|
474
|
+
* @param {string} projectPath - Root path of the project
|
|
475
|
+
* @param {string} content - Generated content
|
|
476
|
+
* @param {string} filename - Output filename
|
|
477
|
+
* @returns {Promise<string>} Path to written file
|
|
478
|
+
*/
|
|
479
|
+
export async function writeContext(projectPath, content, filename = 'CONTEXT.md') {
|
|
480
|
+
const contextDir = path.join(projectPath, '.morph/project/context');
|
|
481
|
+
await fs.mkdir(contextDir, { recursive: true });
|
|
482
|
+
|
|
483
|
+
const outputPath = path.join(contextDir, filename);
|
|
484
|
+
await fs.writeFile(outputPath, content, 'utf8');
|
|
485
|
+
|
|
486
|
+
return outputPath;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Generate and write both project and feature context files
|
|
491
|
+
* @param {string} projectPath - Root path of the project
|
|
492
|
+
* @param {string} featureName - Optional feature name (if null, only generates project context)
|
|
493
|
+
* @returns {Promise<Object>} Paths to generated files
|
|
494
|
+
*/
|
|
495
|
+
export async function generateContext(projectPath, featureName = null) {
|
|
496
|
+
const results = {};
|
|
497
|
+
|
|
498
|
+
// Always generate project context
|
|
499
|
+
const projectContext = await generateProjectContext(projectPath);
|
|
500
|
+
results.projectContext = await writeContext(projectPath, projectContext, 'CONTEXT.md');
|
|
501
|
+
|
|
502
|
+
// Generate feature context if requested
|
|
503
|
+
if (featureName) {
|
|
504
|
+
const featureContext = await generateFeatureContext(projectPath, featureName);
|
|
505
|
+
results.featureContext = await writeContext(
|
|
506
|
+
projectPath,
|
|
507
|
+
featureContext,
|
|
508
|
+
`CONTEXT-${featureName}.md`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return results;
|
|
513
|
+
}
|