@polymorphism-tech/morph-spec 8.32.0 → 8.34.2

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 (176) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +26 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +71 -68
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +218 -39
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +37 -4
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -1,217 +1,217 @@
1
- /**
2
- * @fileoverview Project Detector
3
- *
4
- * Scans a project directory for stack, architecture, UI library, and integrations.
5
- * Used by mcp-setup, morph-init, and session-start hooks.
6
- *
7
- * @module detectors/index
8
- */
9
-
10
- import { existsSync, readFileSync, readdirSync } from 'fs';
11
- import { join } from 'path';
12
-
13
- /**
14
- * Detect project structure from filesystem evidence.
15
- *
16
- * @param {string} targetPath - Root path of the project
17
- * @param {Object} [opts] - Options
18
- * @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
19
- * @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
20
- */
21
- export function detectProject(targetPath, opts = {}) {
22
- const structure = {
23
- stack: 'unknown',
24
- architecture: null,
25
- uiLibrary: null,
26
- integrations: [],
27
- hasVercel: false,
28
- };
29
-
30
- try {
31
- // ── Package.json analysis ──────────────────────────────────────────────
32
- // Collect deps from root and common monorepo subfolders
33
- const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
34
- const allDeps = {};
35
- let rootPkg = null;
36
- let scripts = [];
37
- for (const loc of pkgLocations) {
38
- const pkgPath = join(targetPath, loc, 'package.json');
39
- if (!existsSync(pkgPath)) continue;
40
- try {
41
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
42
- if (loc === '.') rootPkg = pkg;
43
- Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
44
- scripts.push(...Object.keys(pkg.scripts || {}));
45
- // Track frontend path for config
46
- if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
47
- structure.frontendPath = loc;
48
- }
49
- } catch { /* invalid JSON */ }
50
- }
51
- const depNames = Object.keys(allDeps);
52
-
53
- if (depNames.length > 0) {
54
-
55
- // Stack detection from deps
56
- if (depNames.includes('next')) {
57
- structure.stack = 'nextjs';
58
- } else if (depNames.includes('react')) {
59
- structure.stack = 'react';
60
- } else if (depNames.includes('vue')) {
61
- structure.stack = 'vue';
62
- } else if (depNames.includes('express') || depNames.includes('fastify') || depNames.includes('hono')) {
63
- structure.stack = 'nodejs';
64
- } else if (rootPkg?.name) {
65
- structure.stack = 'nodejs';
66
- }
67
-
68
- // UI library detection
69
- if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
70
- structure.uiLibrary = 'shadcn';
71
- }
72
-
73
- // Integrations
74
- if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
75
- structure.integrations.push('neon');
76
- }
77
- if (depNames.includes('@neondatabase/auth')) {
78
- structure.integrations.push('neon-auth');
79
- }
80
- if (depNames.includes('stripe')) {
81
- structure.integrations.push('stripe');
82
- }
83
-
84
- // Vercel detection
85
- if (depNames.includes('vercel') || scripts.includes('vercel')) {
86
- structure.hasVercel = true;
87
- }
88
- }
89
-
90
- // ── .csproj detection ──────────────────────────────────────────────────
91
- let hasDotnet = false;
92
- const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
93
-
94
- function checkCsproj(filePath) {
95
- hasDotnet = true;
96
- try {
97
- const content = readFileSync(filePath, 'utf8');
98
- if (content.includes('Hangfire')) {
99
- if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
100
- }
101
- } catch { /* ignore */ }
102
- }
103
-
104
- for (const loc of csprojLocations) {
105
- try {
106
- const dir = join(targetPath, loc);
107
- if (!existsSync(dir)) continue;
108
- const entries = readdirSyncSafe(dir);
109
- // Direct .csproj files
110
- for (const e of entries.filter(f => f.endsWith('.csproj'))) {
111
- checkCsproj(join(dir, e));
112
- }
113
- // One level deeper (e.g., src/backend/MyProject/*.csproj)
114
- for (const sub of entries) {
115
- const subDir = join(dir, sub);
116
- for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
117
- checkCsproj(join(subDir, e));
118
- }
119
- }
120
- } catch { /* ignore */ }
121
- }
122
-
123
- // Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
124
- if (hasDotnet) {
125
- if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
126
- // Monorepo: frontend + .NET backend
127
- structure.stack = `${structure.stack}+dotnet`;
128
- } else if (structure.stack === 'unknown') {
129
- structure.stack = 'dotnet';
130
- }
131
- }
132
-
133
- // ── vercel.json detection ──────────────────────────────────────────────
134
- if (existsSync(join(targetPath, 'vercel.json'))) {
135
- structure.hasVercel = true;
136
- }
137
-
138
- // ── tsconfig.json detection (fallback for stack) ────────────────────────
139
- if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
140
- structure.stack = 'nodejs';
141
- }
142
-
143
- // ── Architecture detection ─────────────────────────────────────────────
144
- try {
145
- const configPath = join(targetPath, '.morph/config/config.json');
146
- if (existsSync(configPath)) {
147
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
148
- if (config.architecture?.style) {
149
- structure.architecture = config.architecture.style;
150
- }
151
- }
152
- } catch { /* ignore */ }
153
-
154
- // ── Docker detection ───────────────────────────────────────────────────
155
- if (existsSync(join(targetPath, 'docker-compose.yml')) ||
156
- existsSync(join(targetPath, 'docker-compose.yaml')) ||
157
- existsSync(join(targetPath, 'Dockerfile'))) {
158
- if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
159
- }
160
-
161
- // ── Azure detection ────────────────────────────────────────────────────
162
- const azureSignals = [
163
- '.azure',
164
- 'azure-pipelines.yml',
165
- 'infra/main.bicep',
166
- 'infra',
167
- ];
168
- const hasAzure = azureSignals.some(signal => {
169
- const p = join(targetPath, signal);
170
- if (!existsSync(p)) return false;
171
- if (signal === 'infra') {
172
- // Check for .bicep files inside infra/
173
- return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
174
- }
175
- return true;
176
- });
177
- if (hasAzure && !structure.integrations.includes('azure')) {
178
- structure.integrations.push('azure');
179
- }
180
- } catch {
181
- // Fail-safe — return defaults
182
- }
183
-
184
- return {
185
- structure,
186
- inferred: {
187
- markdown: opts.generateStandards ? generateInferredMarkdown(structure) : '',
188
- },
189
- };
190
- }
191
-
192
- /**
193
- * Safe readdir that returns [] on error.
194
- * @param {string} dir
195
- * @returns {string[]}
196
- */
197
- function readdirSyncSafe(dir) {
198
- try {
199
- return readdirSync(dir);
200
- } catch {
201
- return [];
202
- }
203
- }
204
-
205
- /**
206
- * Generate a markdown summary from detected structure.
207
- * @param {Object} structure
208
- * @returns {string}
209
- */
210
- function generateInferredMarkdown(structure) {
211
- const lines = [`## Detected Stack: ${structure.stack}`];
212
- if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
213
- if (structure.uiLibrary) lines.push(`UI Library: ${structure.uiLibrary}`);
214
- if (structure.integrations.length > 0) lines.push(`Integrations: ${structure.integrations.join(', ')}`);
215
- if (structure.hasVercel) lines.push(`Vercel: detected`);
216
- return lines.join('\n');
217
- }
1
+ /**
2
+ * @fileoverview Project Detector
3
+ *
4
+ * Scans a project directory for stack, architecture, UI library, and integrations.
5
+ * Used by mcp-setup, morph-init, and session-start hooks.
6
+ *
7
+ * @module detectors/index
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ /**
14
+ * Detect project structure from filesystem evidence.
15
+ *
16
+ * @param {string} targetPath - Root path of the project
17
+ * @param {Object} [opts] - Options
18
+ * @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
19
+ * @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
20
+ */
21
+ export function detectProject(targetPath, opts = {}) {
22
+ const structure = {
23
+ stack: 'unknown',
24
+ architecture: null,
25
+ uiLibrary: null,
26
+ integrations: [],
27
+ hasVercel: false,
28
+ };
29
+
30
+ try {
31
+ // ── Package.json analysis ──────────────────────────────────────────────
32
+ // Collect deps from root and common monorepo subfolders
33
+ const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
34
+ const allDeps = {};
35
+ let rootPkg = null;
36
+ let scripts = [];
37
+ for (const loc of pkgLocations) {
38
+ const pkgPath = join(targetPath, loc, 'package.json');
39
+ if (!existsSync(pkgPath)) continue;
40
+ try {
41
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
42
+ if (loc === '.') rootPkg = pkg;
43
+ Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
44
+ scripts.push(...Object.keys(pkg.scripts || {}));
45
+ // Track frontend path for config
46
+ if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
47
+ structure.frontendPath = loc;
48
+ }
49
+ } catch { /* invalid JSON */ }
50
+ }
51
+ const depNames = Object.keys(allDeps);
52
+
53
+ if (depNames.length > 0) {
54
+
55
+ // Stack detection from deps
56
+ if (depNames.includes('next')) {
57
+ structure.stack = 'nextjs';
58
+ } else if (depNames.includes('react')) {
59
+ structure.stack = 'react';
60
+ } else if (depNames.includes('vue')) {
61
+ structure.stack = 'vue';
62
+ } else if (depNames.includes('express') || depNames.includes('fastify') || depNames.includes('hono')) {
63
+ structure.stack = 'nodejs';
64
+ } else if (rootPkg?.name) {
65
+ structure.stack = 'nodejs';
66
+ }
67
+
68
+ // UI library detection
69
+ if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
70
+ structure.uiLibrary = 'shadcn';
71
+ }
72
+
73
+ // Integrations
74
+ if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
75
+ structure.integrations.push('neon');
76
+ }
77
+ if (depNames.includes('@neondatabase/auth')) {
78
+ structure.integrations.push('neon-auth');
79
+ }
80
+ if (depNames.includes('stripe')) {
81
+ structure.integrations.push('stripe');
82
+ }
83
+
84
+ // Vercel detection
85
+ if (depNames.includes('vercel') || scripts.includes('vercel')) {
86
+ structure.hasVercel = true;
87
+ }
88
+ }
89
+
90
+ // ── .csproj detection ──────────────────────────────────────────────────
91
+ let hasDotnet = false;
92
+ const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
93
+
94
+ function checkCsproj(filePath) {
95
+ hasDotnet = true;
96
+ try {
97
+ const content = readFileSync(filePath, 'utf8');
98
+ if (content.includes('Hangfire')) {
99
+ if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
100
+ }
101
+ } catch { /* ignore */ }
102
+ }
103
+
104
+ for (const loc of csprojLocations) {
105
+ try {
106
+ const dir = join(targetPath, loc);
107
+ if (!existsSync(dir)) continue;
108
+ const entries = readdirSyncSafe(dir);
109
+ // Direct .csproj files
110
+ for (const e of entries.filter(f => f.endsWith('.csproj'))) {
111
+ checkCsproj(join(dir, e));
112
+ }
113
+ // One level deeper (e.g., src/backend/MyProject/*.csproj)
114
+ for (const sub of entries) {
115
+ const subDir = join(dir, sub);
116
+ for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
117
+ checkCsproj(join(subDir, e));
118
+ }
119
+ }
120
+ } catch { /* ignore */ }
121
+ }
122
+
123
+ // Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
124
+ if (hasDotnet) {
125
+ if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
126
+ // Monorepo: frontend + .NET backend
127
+ structure.stack = `${structure.stack}+dotnet`;
128
+ } else if (structure.stack === 'unknown') {
129
+ structure.stack = 'dotnet';
130
+ }
131
+ }
132
+
133
+ // ── vercel.json detection ──────────────────────────────────────────────
134
+ if (existsSync(join(targetPath, 'vercel.json'))) {
135
+ structure.hasVercel = true;
136
+ }
137
+
138
+ // ── tsconfig.json detection (fallback for stack) ────────────────────────
139
+ if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
140
+ structure.stack = 'nodejs';
141
+ }
142
+
143
+ // ── Architecture detection ─────────────────────────────────────────────
144
+ try {
145
+ const configPath = join(targetPath, '.morph/config/config.json');
146
+ if (existsSync(configPath)) {
147
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
148
+ if (config.architecture?.style) {
149
+ structure.architecture = config.architecture.style;
150
+ }
151
+ }
152
+ } catch { /* ignore */ }
153
+
154
+ // ── Docker detection ───────────────────────────────────────────────────
155
+ if (existsSync(join(targetPath, 'docker-compose.yml')) ||
156
+ existsSync(join(targetPath, 'docker-compose.yaml')) ||
157
+ existsSync(join(targetPath, 'Dockerfile'))) {
158
+ if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
159
+ }
160
+
161
+ // ── Azure detection ────────────────────────────────────────────────────
162
+ const azureSignals = [
163
+ '.azure',
164
+ 'azure-pipelines.yml',
165
+ 'infra/main.bicep',
166
+ 'infra',
167
+ ];
168
+ const hasAzure = azureSignals.some(signal => {
169
+ const p = join(targetPath, signal);
170
+ if (!existsSync(p)) return false;
171
+ if (signal === 'infra') {
172
+ // Check for .bicep files inside infra/
173
+ return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
174
+ }
175
+ return true;
176
+ });
177
+ if (hasAzure && !structure.integrations.includes('azure')) {
178
+ structure.integrations.push('azure');
179
+ }
180
+ } catch {
181
+ // Fail-safe — return defaults
182
+ }
183
+
184
+ return {
185
+ structure,
186
+ inferred: {
187
+ markdown: opts.generateStandards ? generateInferredMarkdown(structure) : '',
188
+ },
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Safe readdir that returns [] on error.
194
+ * @param {string} dir
195
+ * @returns {string[]}
196
+ */
197
+ function readdirSyncSafe(dir) {
198
+ try {
199
+ return readdirSync(dir);
200
+ } catch {
201
+ return [];
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Generate a markdown summary from detected structure.
207
+ * @param {Object} structure
208
+ * @returns {string}
209
+ */
210
+ function generateInferredMarkdown(structure) {
211
+ const lines = [`## Detected Stack: ${structure.stack}`];
212
+ if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
213
+ if (structure.uiLibrary) lines.push(`UI Library: ${structure.uiLibrary}`);
214
+ if (structure.integrations.length > 0) lines.push(`Integrations: ${structure.integrations.join(', ')}`);
215
+ if (structure.hasVercel) lines.push(`Vercel: detected`);
216
+ return lines.join('\n');
217
+ }
@@ -21,14 +21,15 @@
21
21
  import { join } from 'path';
22
22
 
23
23
  import { loadEnvFile } from '../../../framework/hooks/shared/e2e-check.js';
24
- import { allocateEnvBlock, resolveFleetConfig } from '../worktree/env-allocator.js';
24
+ import { allocateEnvBlock, portLayout, resolveFleetConfig } from '../worktree/env-allocator.js';
25
25
 
26
26
  /**
27
27
  * Build the environment a `docker compose` invocation should carry.
28
28
  *
29
- * Port variables mirror `buildEnvFileContent()` in env-allocator.js so a
30
- * compose file and a `.morph/worktree.env` describe the same world: `PORT` for
31
- * the web tier, `ASPNETCORE_URLS` on base+1 for a .NET backend.
29
+ * Port variables come from `portLayout()` in env-allocator.js — the SAME
30
+ * function that renders `.morph/worktree.env` — so a compose file and the
31
+ * worktree manifest describe the same world by construction (`PORT`,
32
+ * `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME`).
32
33
  *
33
34
  * `PLAYWRIGHT_BASE_URL` is derived here rather than left to the spec config —
34
35
  * the suite has to be told where the app actually landed, otherwise a feature
@@ -53,14 +54,8 @@ export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
53
54
  const { basePort, blockSize } = resolveFleetConfig(cwd);
54
55
  const block = allocateEnvBlock(featureName, { gitCommonDir, basePort, blockSize });
55
56
 
56
- env.MORPH_PORT_BASE = String(block.base);
57
- env.MORPH_PORT_RANGE = `${block.range[0]}-${block.range[1]}`;
58
- env.PORT = String(block.base);
57
+ Object.assign(env, portLayout(block, { stack: config?.project?.stack ?? '', feature: featureName }));
59
58
  env.PLAYWRIGHT_BASE_URL = `http://localhost:${block.base}`;
60
-
61
- if (/\bdotnet\b/i.test(String(config?.project?.stack ?? ''))) {
62
- env.ASPNETCORE_URLS = `http://localhost:${block.base + 1}`;
63
- }
64
59
  } catch {
65
60
  // No block → default ports. Surfaces later as a port conflict, never here.
66
61
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Reclaim a feature's verification stack — containers, ports, named volumes
3
+ * and the freshness stamp — from the tree that owns the feature.
4
+ *
5
+ * `morph-spec verify` leaves the stack UP on purpose (cold-starting a
6
+ * container per task is what makes the loop unusable), so something has to be
7
+ * the one place that brings it down when the feature closes. That place is
8
+ * this module, shared by `finish` and `delete --with-worktree`.
9
+ *
10
+ * Three things the old root-side teardown got wrong, each pinned by a test:
11
+ * 1. It ran from the ROOT. A compose that exists only in the worktree was
12
+ * never found, and a bind mount resolved to the wrong checkout.
13
+ * 2. It passed a bare `COMPOSE_PROJECT_NAME`. Without the port block
14
+ * (`PORT`, `POSTGRES_PORT`, …) compose resolves a different config than
15
+ * the one that came up, and `down` misses the containers.
16
+ * 3. It never removed volumes or the stamp. Postgres data and a stamp saying
17
+ * "source unchanged since build" outlived the feature.
18
+ *
19
+ * Entirely fail-open: no compose, no docker, or a stubborn container are all
20
+ * non-events for the caller — a stack that refuses to come down must never
21
+ * fail a finish that already merged.
22
+ *
23
+ * @module lib/e2e/stack-teardown
24
+ */
25
+
26
+ import { execFileSync } from 'child_process';
27
+ import { existsSync, readFileSync } from 'fs';
28
+ import { join, resolve } from 'path';
29
+
30
+ import {
31
+ composeProjectName,
32
+ defaultExec,
33
+ deleteStamp,
34
+ resolveComposeFile,
35
+ tearDownStack,
36
+ } from '../../../framework/hooks/shared/e2e-check.js';
37
+ import { buildComposeEnv } from './compose-env.js';
38
+
39
+ /**
40
+ * @param {string} feature
41
+ * @param {object} params
42
+ * @param {string} params.root - primary root (absolute)
43
+ * @param {string|null} [params.worktreePath] - the feature's live worktree, if any
44
+ * @param {boolean} [params.volumes=true] - also remove named volumes (`down -v`)
45
+ * @param {Function} [params._exec] - injectable executor (testing)
46
+ * @returns {{ attempted: boolean, cwd: string, status: 'pass'|'fail'|'skip', stampDeleted: boolean, output?: string }}
47
+ */
48
+ export function tearDownFeatureStack(feature, { root, worktreePath = null, volumes = true, _exec = defaultExec } = {}) {
49
+ const cwd = worktreePath && existsSync(worktreePath) ? worktreePath : root;
50
+ const projectName = composeProjectName(feature);
51
+ const gitCommonDir = resolveGitCommonDir(root);
52
+
53
+ try {
54
+ const config = readConfig(cwd) ?? readConfig(root);
55
+
56
+ // The owning tree's compose first; the root's copy is the fallback for a
57
+ // worktree already half torn down (versioned file, so equivalent).
58
+ let composeCwd = cwd;
59
+ let composeFile = resolveComposeFile(cwd, config);
60
+ if (!composeFile && cwd !== root) {
61
+ composeCwd = root;
62
+ composeFile = resolveComposeFile(root, config);
63
+ }
64
+
65
+ if (!composeFile) {
66
+ // Nothing to bring down — but a stamp left behind would tell the next
67
+ // `up` that a stack it cannot see is still fresh.
68
+ return { attempted: false, cwd, status: 'skip', stampDeleted: deleteStamp(gitCommonDir, projectName) };
69
+ }
70
+
71
+ const env = {
72
+ COMPOSE_PROJECT_NAME: projectName,
73
+ ...buildComposeEnv({ cwd: composeCwd, config, featureName: feature, gitCommonDir }),
74
+ };
75
+ const result = tearDownStack(composeCwd, { composeFile, projectName, env, volumes }, _exec);
76
+
77
+ return {
78
+ attempted: true,
79
+ cwd: composeCwd,
80
+ status: result.status,
81
+ stampDeleted: deleteStamp(gitCommonDir, projectName),
82
+ ...(result.status === 'fail' && result.output ? { output: result.output } : {}),
83
+ };
84
+ } catch {
85
+ // Unconditional even when the teardown itself blew up: the stamp must not
86
+ // outlive the stack it described.
87
+ return { attempted: false, cwd, status: 'fail', stampDeleted: deleteStamp(gitCommonDir, projectName) };
88
+ }
89
+ }
90
+
91
+ /** BOM-tolerant config read; null when absent or malformed. */
92
+ function readConfig(dir) {
93
+ try {
94
+ const path = join(dir, '.morph', 'config', 'config.json');
95
+ if (!existsSync(path)) return null;
96
+ return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /** Same resolution (and same fail-open) as verify-runner's. */
103
+ function resolveGitCommonDir(cwd) {
104
+ try {
105
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
106
+ cwd,
107
+ stdio: 'pipe',
108
+ encoding: 'utf-8',
109
+ windowsHide: true,
110
+ }).trim();
111
+ return common ? resolve(cwd, common) : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
@@ -20,8 +20,9 @@ import { fileURLToPath } from 'url';
20
20
  import { existsSync, readFileSync } from 'fs';
21
21
  import { scanGraph } from '../graph/god-nodes.js';
22
22
  import { renderTemplate } from '../../core/templates/template-renderer.js';
23
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
23
24
 
24
- const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', 'worktrees', '.worktrees']);
25
+ export const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', ...WORKTREE_DIR_NAMES]);
25
26
  const MAX_FILES_PER_SCANNER = 200;
26
27
 
27
28
  /**