@polymorphism-tech/morph-spec 8.33.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 +3 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +25 -1
  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 +5 -2
  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 +21 -4
  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 +7 -0
  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,209 +1,209 @@
1
- /**
2
- * Settings Generator
3
- *
4
- * Generates .claude/settings.local.json for projects based on detected
5
- * stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
6
- * to track managed entries for safe reset.
7
- */
8
-
9
- import { join } from 'path';
10
- import { existsSync } from 'fs';
11
- import { readFile, writeFile, mkdir } from 'fs/promises';
12
-
13
- const MORPH_MANAGED_KEY = '_morph_managed';
14
-
15
- /**
16
- * Generate and merge settings for a project
17
- * @param {string} targetPath - Project root directory
18
- * @param {Object} detectedConfig - Output from detectClaudeConfig()
19
- * @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
20
- * @returns {Promise<Object>} Generated settings summary
21
- */
22
- export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
23
- const claudeDir = join(targetPath, '.claude');
24
- const settingsPath = join(claudeDir, 'settings.local.json');
25
-
26
- // Read existing settings
27
- const existing = await readSettingsSafe(settingsPath);
28
-
29
- // Build stack-based permissions
30
- const stack = morphConfig?.project?.architecture || 'unknown';
31
- const permissions = buildPermissions(stack, detectedConfig);
32
-
33
- // Build generated settings
34
- const generated = {};
35
-
36
- if (permissions.length > 0) {
37
- generated.permissions = permissions;
38
- }
39
-
40
- // Track what we manage
41
- const managed = {
42
- permissions_added: permissions,
43
- mcps_detected: detectedConfig.mcpServers.map(s => s.name),
44
- plugins_detected: detectedConfig.plugins.map(p => p.name),
45
- generated_at: new Date().toISOString()
46
- };
47
-
48
- // Merge without overwriting user config
49
- const merged = mergeSettings(existing, generated);
50
- merged[MORPH_MANAGED_KEY] = managed;
51
-
52
- // Ensure directory exists
53
- if (!existsSync(claudeDir)) {
54
- await mkdir(claudeDir, { recursive: true });
55
- }
56
-
57
- await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
58
-
59
- return {
60
- settingsPath,
61
- permissionsAdded: permissions.length,
62
- managed
63
- };
64
- }
65
-
66
- /**
67
- * Build permission rules based on detected stack
68
- * @param {string} stack - Detected project stack
69
- * @param {Object} detectedConfig - Detected Claude config
70
- * @returns {Array<string>} Permission patterns
71
- */
72
- export function buildPermissions(stack, detectedConfig = {}) {
73
- const permissions = [];
74
-
75
- // Stack-based permissions
76
- const stackPermissions = {
77
- nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
78
- typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
79
- nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
80
- react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
81
- vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
82
- dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
83
- python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
84
- go: ['Bash(go:*)']
85
- };
86
-
87
- if (stackPermissions[stack]) {
88
- permissions.push(...stackPermissions[stack]);
89
- }
90
-
91
- // morph-spec CLI always needs node/npx
92
- if (!permissions.includes('Bash(npx:*)')) {
93
- permissions.push('Bash(npx:*)');
94
- }
95
-
96
- return permissions;
97
- }
98
-
99
- /**
100
- * Deep merge settings without overwriting user entries
101
- * @param {Object} existing - Current settings.local.json
102
- * @param {Object} generated - Generated settings to merge
103
- * @returns {Object} Merged settings
104
- */
105
- export function mergeSettings(existing, generated) {
106
- const result = { ...existing };
107
-
108
- for (const [key, value] of Object.entries(generated)) {
109
- if (key === MORPH_MANAGED_KEY) continue;
110
-
111
- if (Array.isArray(value) && Array.isArray(result[key])) {
112
- // Merge arrays without duplicates
113
- const set = new Set(result[key]);
114
- for (const item of value) {
115
- set.add(item);
116
- }
117
- result[key] = [...set];
118
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
119
- // Recursively merge objects
120
- result[key] = mergeSettings(result[key] || {}, value);
121
- } else if (!(key in result)) {
122
- // Only add new keys, never overwrite existing
123
- result[key] = value;
124
- }
125
- }
126
-
127
- return result;
128
- }
129
-
130
- /**
131
- * Remove only morph-spec managed entries from settings
132
- * @param {string} settingsPath - Path to settings.local.json
133
- * @returns {Promise<Object>} Summary of what was removed
134
- */
135
- export async function resetMorphSettings(settingsPath) {
136
- if (!existsSync(settingsPath)) {
137
- return { removed: false, reason: 'file not found' };
138
- }
139
-
140
- const settings = await readSettingsSafe(settingsPath);
141
- const managed = settings[MORPH_MANAGED_KEY];
142
-
143
- if (!managed) {
144
- return { removed: false, reason: 'no _morph_managed section found' };
145
- }
146
-
147
- // Remove managed permissions
148
- if (managed.permissions_added && Array.isArray(settings.permissions)) {
149
- settings.permissions = settings.permissions.filter(
150
- p => !managed.permissions_added.includes(p)
151
- );
152
- if (settings.permissions.length === 0) {
153
- delete settings.permissions;
154
- }
155
- }
156
-
157
- // Remove the managed marker itself
158
- delete settings[MORPH_MANAGED_KEY];
159
-
160
- // Write back
161
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
162
-
163
- return {
164
- removed: true,
165
- permissionsRemoved: managed.permissions_added || [],
166
- previouslyGenerated: managed.generated_at
167
- };
168
- }
169
-
170
- /**
171
- * Save integrations.json with detected config
172
- * @param {string} targetPath - Project root directory
173
- * @param {Object} detectedConfig - Output from detectClaudeConfig()
174
- */
175
- export async function saveIntegrations(targetPath, detectedConfig) {
176
- const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
177
- const integrationsDir = join(targetPath, '.morph', 'config');
178
-
179
- if (!existsSync(integrationsDir)) {
180
- await mkdir(integrationsDir, { recursive: true });
181
- }
182
-
183
- const integrations = {
184
- version: '1.0.0',
185
- detectedAt: new Date().toISOString(),
186
- plugins: detectedConfig.plugins,
187
- mcpServers: detectedConfig.mcpServers,
188
- superpowers: detectedConfig.superpowers,
189
- customSkills: detectedConfig.customSkills.length,
190
- customCommands: detectedConfig.customCommands.length,
191
- hooks: detectedConfig.hooks.length
192
- };
193
-
194
- await writeFile(integrationsPath, JSON.stringify(integrations, null, 2) + '\n', 'utf-8');
195
- return integrationsPath;
196
- }
197
-
198
- // ============================================================================
199
- // Helpers
200
- // ============================================================================
201
-
202
- async function readSettingsSafe(settingsPath) {
203
- try {
204
- if (!existsSync(settingsPath)) return {};
205
- return JSON.parse(await readFile(settingsPath, 'utf-8'));
206
- } catch {
207
- return {};
208
- }
209
- }
1
+ /**
2
+ * Settings Generator
3
+ *
4
+ * Generates .claude/settings.local.json for projects based on detected
5
+ * stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
6
+ * to track managed entries for safe reset.
7
+ */
8
+
9
+ import { join } from 'path';
10
+ import { existsSync } from 'fs';
11
+ import { readFile, writeFile, mkdir } from 'fs/promises';
12
+
13
+ const MORPH_MANAGED_KEY = '_morph_managed';
14
+
15
+ /**
16
+ * Generate and merge settings for a project
17
+ * @param {string} targetPath - Project root directory
18
+ * @param {Object} detectedConfig - Output from detectClaudeConfig()
19
+ * @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
20
+ * @returns {Promise<Object>} Generated settings summary
21
+ */
22
+ export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
23
+ const claudeDir = join(targetPath, '.claude');
24
+ const settingsPath = join(claudeDir, 'settings.local.json');
25
+
26
+ // Read existing settings
27
+ const existing = await readSettingsSafe(settingsPath);
28
+
29
+ // Build stack-based permissions
30
+ const stack = morphConfig?.project?.architecture || 'unknown';
31
+ const permissions = buildPermissions(stack, detectedConfig);
32
+
33
+ // Build generated settings
34
+ const generated = {};
35
+
36
+ if (permissions.length > 0) {
37
+ generated.permissions = permissions;
38
+ }
39
+
40
+ // Track what we manage
41
+ const managed = {
42
+ permissions_added: permissions,
43
+ mcps_detected: detectedConfig.mcpServers.map(s => s.name),
44
+ plugins_detected: detectedConfig.plugins.map(p => p.name),
45
+ generated_at: new Date().toISOString()
46
+ };
47
+
48
+ // Merge without overwriting user config
49
+ const merged = mergeSettings(existing, generated);
50
+ merged[MORPH_MANAGED_KEY] = managed;
51
+
52
+ // Ensure directory exists
53
+ if (!existsSync(claudeDir)) {
54
+ await mkdir(claudeDir, { recursive: true });
55
+ }
56
+
57
+ await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
58
+
59
+ return {
60
+ settingsPath,
61
+ permissionsAdded: permissions.length,
62
+ managed
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Build permission rules based on detected stack
68
+ * @param {string} stack - Detected project stack
69
+ * @param {Object} detectedConfig - Detected Claude config
70
+ * @returns {Array<string>} Permission patterns
71
+ */
72
+ export function buildPermissions(stack, detectedConfig = {}) {
73
+ const permissions = [];
74
+
75
+ // Stack-based permissions
76
+ const stackPermissions = {
77
+ nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
78
+ typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
79
+ nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
80
+ react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
81
+ vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
82
+ dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
83
+ python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
84
+ go: ['Bash(go:*)']
85
+ };
86
+
87
+ if (stackPermissions[stack]) {
88
+ permissions.push(...stackPermissions[stack]);
89
+ }
90
+
91
+ // morph-spec CLI always needs node/npx
92
+ if (!permissions.includes('Bash(npx:*)')) {
93
+ permissions.push('Bash(npx:*)');
94
+ }
95
+
96
+ return permissions;
97
+ }
98
+
99
+ /**
100
+ * Deep merge settings without overwriting user entries
101
+ * @param {Object} existing - Current settings.local.json
102
+ * @param {Object} generated - Generated settings to merge
103
+ * @returns {Object} Merged settings
104
+ */
105
+ export function mergeSettings(existing, generated) {
106
+ const result = { ...existing };
107
+
108
+ for (const [key, value] of Object.entries(generated)) {
109
+ if (key === MORPH_MANAGED_KEY) continue;
110
+
111
+ if (Array.isArray(value) && Array.isArray(result[key])) {
112
+ // Merge arrays without duplicates
113
+ const set = new Set(result[key]);
114
+ for (const item of value) {
115
+ set.add(item);
116
+ }
117
+ result[key] = [...set];
118
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
119
+ // Recursively merge objects
120
+ result[key] = mergeSettings(result[key] || {}, value);
121
+ } else if (!(key in result)) {
122
+ // Only add new keys, never overwrite existing
123
+ result[key] = value;
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Remove only morph-spec managed entries from settings
132
+ * @param {string} settingsPath - Path to settings.local.json
133
+ * @returns {Promise<Object>} Summary of what was removed
134
+ */
135
+ export async function resetMorphSettings(settingsPath) {
136
+ if (!existsSync(settingsPath)) {
137
+ return { removed: false, reason: 'file not found' };
138
+ }
139
+
140
+ const settings = await readSettingsSafe(settingsPath);
141
+ const managed = settings[MORPH_MANAGED_KEY];
142
+
143
+ if (!managed) {
144
+ return { removed: false, reason: 'no _morph_managed section found' };
145
+ }
146
+
147
+ // Remove managed permissions
148
+ if (managed.permissions_added && Array.isArray(settings.permissions)) {
149
+ settings.permissions = settings.permissions.filter(
150
+ p => !managed.permissions_added.includes(p)
151
+ );
152
+ if (settings.permissions.length === 0) {
153
+ delete settings.permissions;
154
+ }
155
+ }
156
+
157
+ // Remove the managed marker itself
158
+ delete settings[MORPH_MANAGED_KEY];
159
+
160
+ // Write back
161
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
162
+
163
+ return {
164
+ removed: true,
165
+ permissionsRemoved: managed.permissions_added || [],
166
+ previouslyGenerated: managed.generated_at
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Save integrations.json with detected config
172
+ * @param {string} targetPath - Project root directory
173
+ * @param {Object} detectedConfig - Output from detectClaudeConfig()
174
+ */
175
+ export async function saveIntegrations(targetPath, detectedConfig) {
176
+ const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
177
+ const integrationsDir = join(targetPath, '.morph', 'config');
178
+
179
+ if (!existsSync(integrationsDir)) {
180
+ await mkdir(integrationsDir, { recursive: true });
181
+ }
182
+
183
+ const integrations = {
184
+ version: '1.0.0',
185
+ detectedAt: new Date().toISOString(),
186
+ plugins: detectedConfig.plugins,
187
+ mcpServers: detectedConfig.mcpServers,
188
+ superpowers: detectedConfig.superpowers,
189
+ customSkills: detectedConfig.customSkills.length,
190
+ customCommands: detectedConfig.customCommands.length,
191
+ hooks: detectedConfig.hooks.length
192
+ };
193
+
194
+ await writeFile(integrationsPath, JSON.stringify(integrations, null, 2) + '\n', 'utf-8');
195
+ return integrationsPath;
196
+ }
197
+
198
+ // ============================================================================
199
+ // Helpers
200
+ // ============================================================================
201
+
202
+ async function readSettingsSafe(settingsPath) {
203
+ try {
204
+ if (!existsSync(settingsPath)) return {};
205
+ return JSON.parse(await readFile(settingsPath, 'utf-8'));
206
+ } catch {
207
+ return {};
208
+ }
209
+ }
@@ -17,9 +17,10 @@
17
17
  */
18
18
  import { writeFileSync, existsSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
20
21
 
21
22
  export const ALWAYS_IGNORED = [
22
- '.git/', '.morph/', 'artifacts/', 'worktrees/', '.worktrees/',
23
+ '.git/', '.morph/', 'artifacts/', ...WORKTREE_DIR_NAMES.map((d) => `${d}/`),
23
24
  'coverage/', 'graphify-out/', 'dist/', 'out/',
24
25
  ];
25
26