@polymorphism-tech/morph-spec 4.18.0 → 4.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/task-manager.js +12 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/architecture/architecture-expert.md +86 -0
- package/framework/agents/architecture/morph-spec-architect.md +66 -0
- package/framework/agents/architecture/packages-validator.md +77 -0
- package/framework/agents/architecture/security-expert.md +67 -0
- package/framework/agents/backend/ms-agent-expert.md +1 -1
- package/framework/agents/frontend/design-system-validator.md +88 -0
- package/framework/agents/frontend/nextjs-component-validator.md +85 -0
- package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
- package/framework/agents.json +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +39 -0
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
- package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
- package/framework/hooks/shared/activity-logger.js +56 -1
- package/framework/hooks/shared/build-check.js +79 -0
- package/framework/hooks/shared/payload-utils.js +39 -0
- package/framework/hooks/shared/session-snapshot.js +105 -0
- package/framework/phases.json +1 -1
- package/framework/skills/level-0-meta/mcp-registry.json +34 -1
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/ai-agents/production.md +5 -2
- package/framework/standards/ai-agents/setup.md +13 -14
- package/framework/standards/ai-agents/workflows.md +8 -6
- package/framework/standards/backend/database/vector-search-rag.md +1 -1
- package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
- package/package.json +1 -1
- package/src/commands/mcp/mcp-setup.js +21 -0
- package/src/commands/project/init.js +11 -0
- package/src/lib/contracts/sprint-contract-generator.js +137 -0
- package/src/lib/installers/mcp-installer.js +25 -0
- package/src/utils/hooks-installer.js +12 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint Contract Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates sprint contracts from tasks.md + spec.md for the evaluator.
|
|
5
|
+
* Contracts define objective "definition of done" criteria.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a task entry from tasks.md markdown.
|
|
14
|
+
*
|
|
15
|
+
* Expected format: ## T001: Task Title [FR001, FR003]
|
|
16
|
+
*
|
|
17
|
+
* @param {string} content - tasks.md content
|
|
18
|
+
* @param {string} taskId - Task ID (e.g., 'T001')
|
|
19
|
+
* @returns {{ id: string, title: string, frs: string[], description: string }|null}
|
|
20
|
+
*/
|
|
21
|
+
export function parseTaskFromMarkdown(content, taskId) {
|
|
22
|
+
const pattern = new RegExp(
|
|
23
|
+
`##\\s+${taskId}:\\s+(.+?)(?:\\s+\\[([^\\]]+)\\])?\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`,
|
|
24
|
+
'i'
|
|
25
|
+
);
|
|
26
|
+
const match = content.match(pattern);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
|
|
29
|
+
const title = match[1].trim();
|
|
30
|
+
const frString = match[2] || '';
|
|
31
|
+
const description = match[3].trim();
|
|
32
|
+
|
|
33
|
+
const frs = frString
|
|
34
|
+
? frString.split(',').map(fr => fr.trim()).filter(Boolean)
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
return { id: taskId, title, frs, description };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a sprint contract for a task.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectPath - Project root
|
|
44
|
+
* @param {string} featureName - Feature name
|
|
45
|
+
* @param {string} taskId - Task ID
|
|
46
|
+
* @returns {Object|null} Contract object or null if unable to generate
|
|
47
|
+
*/
|
|
48
|
+
export function generateSprintContract(projectPath, featureName, taskId) {
|
|
49
|
+
const featureDir = join(projectPath, '.morph/features', featureName);
|
|
50
|
+
const tasksPath = join(featureDir, '4-tasks/tasks.md');
|
|
51
|
+
|
|
52
|
+
if (!existsSync(tasksPath)) return null;
|
|
53
|
+
|
|
54
|
+
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
55
|
+
const task = parseTaskFromMarkdown(tasksContent, taskId);
|
|
56
|
+
if (!task) return null;
|
|
57
|
+
|
|
58
|
+
// Read spec for FR-based criteria
|
|
59
|
+
const specPath = join(featureDir, '2-design/spec.md');
|
|
60
|
+
let definitionOfDone = [];
|
|
61
|
+
if (task.frs.length > 0 && existsSync(specPath)) {
|
|
62
|
+
const specContent = readFileSync(specPath, 'utf-8');
|
|
63
|
+
for (const fr of task.frs) {
|
|
64
|
+
const frPattern = new RegExp(`##\\s+${fr}[:\\s]([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
|
|
65
|
+
const frMatch = specContent.match(frPattern);
|
|
66
|
+
if (frMatch) {
|
|
67
|
+
const bullets = frMatch[1].match(/^[\s]*[-*]\s+(.+)/gm);
|
|
68
|
+
if (bullets) {
|
|
69
|
+
definitionOfDone.push(...bullets.map(b => b.replace(/^[\s]*[-*]\s+/, '').trim()));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (definitionOfDone.length === 0) {
|
|
76
|
+
definitionOfDone = [task.description || `Complete ${task.title}`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Read stack for build commands
|
|
80
|
+
let stack = '';
|
|
81
|
+
try {
|
|
82
|
+
const configPath = join(projectPath, '.morph/config/config.json');
|
|
83
|
+
if (existsSync(configPath)) {
|
|
84
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
85
|
+
stack = config?.project?.stack || '';
|
|
86
|
+
}
|
|
87
|
+
} catch { /* ignore */ }
|
|
88
|
+
|
|
89
|
+
const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
|
|
90
|
+
const buildCmd = getBuildCommand(stack, hasTsConfig);
|
|
91
|
+
|
|
92
|
+
const contract = {
|
|
93
|
+
task: taskId,
|
|
94
|
+
title: task.title,
|
|
95
|
+
functionalRequirements: task.frs,
|
|
96
|
+
definitionOfDone,
|
|
97
|
+
verification: {
|
|
98
|
+
build: buildCmd || 'echo "No build command configured"',
|
|
99
|
+
test: stack.includes('dotnet') ? `dotnet test --filter ${task.title.replace(/\s+/g, '')}` : 'npm test',
|
|
100
|
+
e2e: null
|
|
101
|
+
},
|
|
102
|
+
maxAttempts: 3,
|
|
103
|
+
generatedAt: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Write contract to disk
|
|
107
|
+
const contractsDir = join(featureDir, '5-implement/contracts');
|
|
108
|
+
mkdirSync(contractsDir, { recursive: true });
|
|
109
|
+
const yamlContent = contractToYaml(contract);
|
|
110
|
+
writeFileSync(join(contractsDir, `${taskId}-contract.yaml`), yamlContent, 'utf-8');
|
|
111
|
+
|
|
112
|
+
return contract;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Simple YAML serializer for contracts (no dependency needed).
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} contract - Contract object
|
|
119
|
+
* @returns {string} YAML string
|
|
120
|
+
*/
|
|
121
|
+
function contractToYaml(contract) {
|
|
122
|
+
const lines = [
|
|
123
|
+
`task: ${contract.task}`,
|
|
124
|
+
`title: "${contract.title}"`,
|
|
125
|
+
`functionalRequirements:`,
|
|
126
|
+
...contract.functionalRequirements.map(fr => ` - ${fr}`),
|
|
127
|
+
`definitionOfDone:`,
|
|
128
|
+
...contract.definitionOfDone.map(d => ` - "${d}"`),
|
|
129
|
+
`verification:`,
|
|
130
|
+
` build: "${contract.verification.build}"`,
|
|
131
|
+
` test: "${contract.verification.test}"`,
|
|
132
|
+
` e2e: ${contract.verification.e2e ? `"${contract.verification.e2e}"` : 'null'}`,
|
|
133
|
+
`maxAttempts: ${contract.maxAttempts}`,
|
|
134
|
+
`generatedAt: "${contract.generatedAt}"`,
|
|
135
|
+
];
|
|
136
|
+
return lines.join('\n') + '\n';
|
|
137
|
+
}
|
|
@@ -148,6 +148,31 @@ export async function installAutoMcps(targetPath, mcpsToInstall) {
|
|
|
148
148
|
return installMcpServers(targetPath, servers);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Install companion skills for MCPs that define them (e.g., Stitch agent skills).
|
|
153
|
+
* Reads the `companionSkills` field from registry entries and runs the install command.
|
|
154
|
+
* @param {Object} mcpsInstalled - Map of mcpName → mcpEntry that were just installed
|
|
155
|
+
* @param {Function} exec - execSync function (for testing)
|
|
156
|
+
* @returns {Array<{name: string, repo: string, success: boolean}>}
|
|
157
|
+
*/
|
|
158
|
+
export function installCompanionSkills(mcpsInstalled, exec = execSync) {
|
|
159
|
+
const results = [];
|
|
160
|
+
|
|
161
|
+
for (const [name, entry] of Object.entries(mcpsInstalled)) {
|
|
162
|
+
const companion = entry.companionSkills;
|
|
163
|
+
if (!companion?.installCommand) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
exec(companion.installCommand, { stdio: 'inherit', timeout: 30000 });
|
|
167
|
+
results.push({ name, repo: companion.repo, success: true });
|
|
168
|
+
} catch {
|
|
169
|
+
results.push({ name, repo: companion.repo, success: false });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
/**
|
|
152
177
|
* Install a single MCP with provided credentials
|
|
153
178
|
* @param {string} targetPath - Project root directory
|
|
@@ -15,7 +15,7 @@ import { homedir } from 'os';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
/** Current hooks schema version — bump when hook definitions change */
|
|
18
|
-
const HOOKS_VERSION = '
|
|
18
|
+
const HOOKS_VERSION = '3.0.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -155,6 +155,17 @@ Otherwise respond: {"ok": true}`
|
|
|
155
155
|
]
|
|
156
156
|
},
|
|
157
157
|
|
|
158
|
+
// === PostToolUse: All — loop detection ===
|
|
159
|
+
{
|
|
160
|
+
event: 'PostToolUse',
|
|
161
|
+
matcher: null,
|
|
162
|
+
hooks: [{
|
|
163
|
+
type: 'command',
|
|
164
|
+
command: 'node framework/hooks/claude-code/post-tool-use/loop-detect.js',
|
|
165
|
+
timeout: 5000
|
|
166
|
+
}]
|
|
167
|
+
},
|
|
168
|
+
|
|
158
169
|
// === Stop ===
|
|
159
170
|
{
|
|
160
171
|
event: 'Stop',
|