@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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/bin/task-manager.js +12 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/agents/architecture/architecture-expert.md +86 -0
  7. package/framework/agents/architecture/morph-spec-architect.md +66 -0
  8. package/framework/agents/architecture/packages-validator.md +77 -0
  9. package/framework/agents/architecture/security-expert.md +67 -0
  10. package/framework/agents/backend/ms-agent-expert.md +1 -1
  11. package/framework/agents/frontend/design-system-validator.md +88 -0
  12. package/framework/agents/frontend/nextjs-component-validator.md +85 -0
  13. package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
  14. package/framework/agents.json +1 -1
  15. package/framework/hooks/claude-code/post-tool-use/dispatch.js +39 -0
  16. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
  17. package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
  18. package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
  19. package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
  20. package/framework/hooks/shared/activity-logger.js +56 -1
  21. package/framework/hooks/shared/build-check.js +79 -0
  22. package/framework/hooks/shared/payload-utils.js +39 -0
  23. package/framework/hooks/shared/session-snapshot.js +105 -0
  24. package/framework/phases.json +1 -1
  25. package/framework/skills/level-0-meta/mcp-registry.json +34 -1
  26. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  27. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  28. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
  29. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
  30. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
  31. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
  32. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
  33. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  34. package/framework/standards/ai-agents/production.md +5 -2
  35. package/framework/standards/ai-agents/setup.md +13 -14
  36. package/framework/standards/ai-agents/workflows.md +8 -6
  37. package/framework/standards/backend/database/vector-search-rag.md +1 -1
  38. package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
  39. package/package.json +1 -1
  40. package/src/commands/mcp/mcp-setup.js +21 -0
  41. package/src/commands/project/init.js +11 -0
  42. package/src/lib/contracts/sprint-contract-generator.js +137 -0
  43. package/src/lib/installers/mcp-installer.js +25 -0
  44. 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 = '2.14.0';
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',