@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,300 +1,300 @@
1
- /**
2
- * Claude Settings Manager
3
- *
4
- * Extends the original hooks-installer with broader settings management:
5
- * permissions, env vars, and managed entry tracking.
6
- *
7
- * Re-exports installClaudeHooks for backward compatibility.
8
- */
9
-
10
- import { join } from 'path';
11
- import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
12
- import { existsSync } from 'fs';
13
-
14
- const MORPH_MANAGED_KEY = '_morph_managed';
15
-
16
- // ============================================================================
17
- // Re-export from hooks-installer (v2 registry-based)
18
- // ============================================================================
19
-
20
- export { installClaudeHooks, installGlobalStatusline, getHooksVersion } from './hooks-installer.js';
21
- export { installAgents } from './agents-installer.js';
22
-
23
- // ============================================================================
24
- // Extended settings management
25
- // ============================================================================
26
-
27
- /**
28
- * Install permission rules into .claude/settings.local.json
29
- * @param {string} targetPath - Project root directory
30
- * @param {Array<string>} permissions - Permission patterns to add
31
- * @returns {Promise<Object>} Summary of changes
32
- */
33
- export async function installPermissions(targetPath, permissions) {
34
- const settings = await loadSettings(targetPath);
35
-
36
- settings.permissions = settings.permissions || [];
37
-
38
- const added = [];
39
- for (const perm of permissions) {
40
- if (!settings.permissions.includes(perm)) {
41
- settings.permissions.push(perm);
42
- added.push(perm);
43
- }
44
- }
45
-
46
- // Track in _morph_managed
47
- settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
48
- settings[MORPH_MANAGED_KEY].permissions_added =
49
- settings[MORPH_MANAGED_KEY].permissions_added || [];
50
- for (const perm of added) {
51
- if (!settings[MORPH_MANAGED_KEY].permissions_added.includes(perm)) {
52
- settings[MORPH_MANAGED_KEY].permissions_added.push(perm);
53
- }
54
- }
55
- settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
56
-
57
- await saveSettings(targetPath, settings);
58
-
59
- return { added, total: settings.permissions.length };
60
- }
61
-
62
- /**
63
- * Install environment variables into .claude/settings.local.json
64
- * @param {string} targetPath - Project root directory
65
- * @param {Object} envVars - Key-value pairs to add to env
66
- * @returns {Promise<Object>} Summary of changes
67
- */
68
- export async function installEnvVars(targetPath, envVars) {
69
- const settings = await loadSettings(targetPath);
70
-
71
- settings.env = settings.env || {};
72
-
73
- const added = [];
74
- for (const [key, value] of Object.entries(envVars)) {
75
- if (!(key in settings.env)) {
76
- settings.env[key] = value;
77
- added.push(key);
78
- }
79
- }
80
-
81
- // Track in _morph_managed
82
- settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
83
- settings[MORPH_MANAGED_KEY].env_added =
84
- settings[MORPH_MANAGED_KEY].env_added || [];
85
- for (const key of added) {
86
- if (!settings[MORPH_MANAGED_KEY].env_added.includes(key)) {
87
- settings[MORPH_MANAGED_KEY].env_added.push(key);
88
- }
89
- }
90
- settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
91
-
92
- await saveSettings(targetPath, settings);
93
-
94
- return { added, total: Object.keys(settings.env).length };
95
- }
96
-
97
- /**
98
- * Remove only morph-spec managed entries from settings.
99
- * Creates backup before modifying.
100
- * @param {string} targetPath - Project root directory
101
- * @returns {Promise<Object>} Summary of what was removed
102
- */
103
- export async function resetMorphSettings(targetPath) {
104
- const claudeDir = join(targetPath, '.claude');
105
- const settingsPath = join(claudeDir, 'settings.local.json');
106
-
107
- if (!existsSync(settingsPath)) {
108
- return { removed: false, reason: 'file not found' };
109
- }
110
-
111
- const settings = await loadSettings(targetPath);
112
- const managed = settings[MORPH_MANAGED_KEY];
113
-
114
- if (!managed) {
115
- return { removed: false, reason: 'no _morph_managed section found' };
116
- }
117
-
118
- // Backup before modifying
119
- const backupPath = settingsPath + '.backup';
120
- await copyFile(settingsPath, backupPath);
121
-
122
- // Remove managed permissions
123
- if (managed.permissions_added && Array.isArray(settings.permissions)) {
124
- settings.permissions = settings.permissions.filter(
125
- p => !managed.permissions_added.includes(p)
126
- );
127
- if (settings.permissions.length === 0) {
128
- delete settings.permissions;
129
- }
130
- }
131
-
132
- // Remove managed env vars
133
- if (managed.env_added && settings.env) {
134
- for (const key of managed.env_added) {
135
- delete settings.env[key];
136
- }
137
- if (Object.keys(settings.env).length === 0) {
138
- delete settings.env;
139
- }
140
- }
141
-
142
- // Remove managed hooks (v2 format: identified by path pattern)
143
- if (settings.hooks) {
144
- const morphPattern = /framework\/hooks\/claude-code\//;
145
- const oldPattern = /agent-teams\/dispatch\.js/;
146
-
147
- for (const [event, entries] of Object.entries(settings.hooks)) {
148
- if (!Array.isArray(entries)) continue;
149
-
150
- settings.hooks[event] = entries.filter(entry => {
151
- if (!Array.isArray(entry.hooks)) return true;
152
- const nonMorphHooks = entry.hooks.filter(h =>
153
- typeof h.command !== 'string' ||
154
- (!morphPattern.test(h.command) && !oldPattern.test(h.command))
155
- );
156
- return nonMorphHooks.length > 0;
157
- });
158
-
159
- if (settings.hooks[event].length === 0) {
160
- delete settings.hooks[event];
161
- }
162
- }
163
- if (Object.keys(settings.hooks).length === 0) {
164
- delete settings.hooks;
165
- }
166
- }
167
-
168
- // Remove managed MCP servers from .mcp.json (primary location)
169
- if (managed.mcps_installed) {
170
- const mcpJson = await loadMcpJson(targetPath);
171
- if (mcpJson.mcpServers) {
172
- for (const mcpName of managed.mcps_installed) {
173
- delete mcpJson.mcpServers[mcpName];
174
- }
175
- if (Object.keys(mcpJson.mcpServers).length === 0) {
176
- delete mcpJson.mcpServers;
177
- }
178
- // Write back (or remove file if empty)
179
- if (Object.keys(mcpJson).length === 0) {
180
- const mcpJsonPath = join(targetPath, '.mcp.json');
181
- if (existsSync(mcpJsonPath)) {
182
- const { unlink } = await import('fs/promises');
183
- await unlink(mcpJsonPath);
184
- }
185
- } else {
186
- await saveMcpJson(targetPath, mcpJson);
187
- }
188
- }
189
-
190
- // Also clean legacy entries from settings.local.json
191
- if (settings.mcpServers) {
192
- for (const mcpName of managed.mcps_installed) {
193
- delete settings.mcpServers[mcpName];
194
- }
195
- if (Object.keys(settings.mcpServers).length === 0) {
196
- delete settings.mcpServers;
197
- }
198
- }
199
- }
200
-
201
- // Remove the managed marker
202
- delete settings[MORPH_MANAGED_KEY];
203
-
204
- await saveSettings(targetPath, settings);
205
-
206
- return {
207
- removed: true,
208
- backupPath,
209
- permissionsRemoved: managed.permissions_added || [],
210
- envRemoved: managed.env_added || [],
211
- hooksRemoved: managed.hooks_added || [],
212
- mcpsRemoved: managed.mcps_installed || [],
213
- previouslyGenerated: managed.generated_at
214
- };
215
- }
216
-
217
- /**
218
- * Install MCP server entries into .mcp.json at the project root.
219
- * Claude Code reads MCP servers from .mcp.json (not settings.local.json).
220
- * Tracks installed names in settings.local.json _morph_managed.mcps_installed for safe reset.
221
- *
222
- * @param {string} targetPath - Project root directory
223
- * @param {Object} servers - Map of server name → config { type?, command, args, env? }
224
- * @returns {Promise<Object>} Summary of changes { added, skipped, total }
225
- */
226
- export async function installMcpServers(targetPath, servers) {
227
- const mcpJson = await loadMcpJson(targetPath);
228
-
229
- mcpJson.mcpServers = mcpJson.mcpServers || {};
230
-
231
- const added = [];
232
- const skipped = [];
233
-
234
- for (const [name, config] of Object.entries(servers)) {
235
- if (mcpJson.mcpServers[name]) {
236
- skipped.push(name);
237
- } else {
238
- mcpJson.mcpServers[name] = config;
239
- added.push(name);
240
- }
241
- }
242
-
243
- await saveMcpJson(targetPath, mcpJson);
244
-
245
- // Track in settings.local.json _morph_managed
246
- const settings = await loadSettings(targetPath);
247
- settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
248
- settings[MORPH_MANAGED_KEY].mcps_installed =
249
- settings[MORPH_MANAGED_KEY].mcps_installed || [];
250
- for (const name of added) {
251
- if (!settings[MORPH_MANAGED_KEY].mcps_installed.includes(name)) {
252
- settings[MORPH_MANAGED_KEY].mcps_installed.push(name);
253
- }
254
- }
255
- settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
256
-
257
- await saveSettings(targetPath, settings);
258
-
259
- return { added, skipped, total: Object.keys(mcpJson.mcpServers).length };
260
- }
261
-
262
- // ============================================================================
263
- // Helpers
264
- // ============================================================================
265
-
266
- async function loadMcpJson(targetPath) {
267
- const mcpJsonPath = join(targetPath, '.mcp.json');
268
- try {
269
- if (!existsSync(mcpJsonPath)) return {};
270
- return JSON.parse(await readFile(mcpJsonPath, 'utf-8'));
271
- } catch {
272
- return {};
273
- }
274
- }
275
-
276
- async function saveMcpJson(targetPath, data) {
277
- const mcpJsonPath = join(targetPath, '.mcp.json');
278
- await writeFile(mcpJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
279
- }
280
-
281
- async function loadSettings(targetPath) {
282
- const settingsPath = join(targetPath, '.claude', 'settings.local.json');
283
- try {
284
- if (!existsSync(settingsPath)) return {};
285
- return JSON.parse(await readFile(settingsPath, 'utf-8'));
286
- } catch {
287
- return {};
288
- }
289
- }
290
-
291
- async function saveSettings(targetPath, settings) {
292
- const claudeDir = join(targetPath, '.claude');
293
- const settingsPath = join(claudeDir, 'settings.local.json');
294
-
295
- if (!existsSync(claudeDir)) {
296
- await mkdir(claudeDir, { recursive: true });
297
- }
298
-
299
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
300
- }
1
+ /**
2
+ * Claude Settings Manager
3
+ *
4
+ * Extends the original hooks-installer with broader settings management:
5
+ * permissions, env vars, and managed entry tracking.
6
+ *
7
+ * Re-exports installClaudeHooks for backward compatibility.
8
+ */
9
+
10
+ import { join } from 'path';
11
+ import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+
14
+ const MORPH_MANAGED_KEY = '_morph_managed';
15
+
16
+ // ============================================================================
17
+ // Re-export from hooks-installer (v2 registry-based)
18
+ // ============================================================================
19
+
20
+ export { installClaudeHooks, installGlobalStatusline, getHooksVersion } from './hooks-installer.js';
21
+ export { installAgents } from './agents-installer.js';
22
+
23
+ // ============================================================================
24
+ // Extended settings management
25
+ // ============================================================================
26
+
27
+ /**
28
+ * Install permission rules into .claude/settings.local.json
29
+ * @param {string} targetPath - Project root directory
30
+ * @param {Array<string>} permissions - Permission patterns to add
31
+ * @returns {Promise<Object>} Summary of changes
32
+ */
33
+ export async function installPermissions(targetPath, permissions) {
34
+ const settings = await loadSettings(targetPath);
35
+
36
+ settings.permissions = settings.permissions || [];
37
+
38
+ const added = [];
39
+ for (const perm of permissions) {
40
+ if (!settings.permissions.includes(perm)) {
41
+ settings.permissions.push(perm);
42
+ added.push(perm);
43
+ }
44
+ }
45
+
46
+ // Track in _morph_managed
47
+ settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
48
+ settings[MORPH_MANAGED_KEY].permissions_added =
49
+ settings[MORPH_MANAGED_KEY].permissions_added || [];
50
+ for (const perm of added) {
51
+ if (!settings[MORPH_MANAGED_KEY].permissions_added.includes(perm)) {
52
+ settings[MORPH_MANAGED_KEY].permissions_added.push(perm);
53
+ }
54
+ }
55
+ settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
56
+
57
+ await saveSettings(targetPath, settings);
58
+
59
+ return { added, total: settings.permissions.length };
60
+ }
61
+
62
+ /**
63
+ * Install environment variables into .claude/settings.local.json
64
+ * @param {string} targetPath - Project root directory
65
+ * @param {Object} envVars - Key-value pairs to add to env
66
+ * @returns {Promise<Object>} Summary of changes
67
+ */
68
+ export async function installEnvVars(targetPath, envVars) {
69
+ const settings = await loadSettings(targetPath);
70
+
71
+ settings.env = settings.env || {};
72
+
73
+ const added = [];
74
+ for (const [key, value] of Object.entries(envVars)) {
75
+ if (!(key in settings.env)) {
76
+ settings.env[key] = value;
77
+ added.push(key);
78
+ }
79
+ }
80
+
81
+ // Track in _morph_managed
82
+ settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
83
+ settings[MORPH_MANAGED_KEY].env_added =
84
+ settings[MORPH_MANAGED_KEY].env_added || [];
85
+ for (const key of added) {
86
+ if (!settings[MORPH_MANAGED_KEY].env_added.includes(key)) {
87
+ settings[MORPH_MANAGED_KEY].env_added.push(key);
88
+ }
89
+ }
90
+ settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
91
+
92
+ await saveSettings(targetPath, settings);
93
+
94
+ return { added, total: Object.keys(settings.env).length };
95
+ }
96
+
97
+ /**
98
+ * Remove only morph-spec managed entries from settings.
99
+ * Creates backup before modifying.
100
+ * @param {string} targetPath - Project root directory
101
+ * @returns {Promise<Object>} Summary of what was removed
102
+ */
103
+ export async function resetMorphSettings(targetPath) {
104
+ const claudeDir = join(targetPath, '.claude');
105
+ const settingsPath = join(claudeDir, 'settings.local.json');
106
+
107
+ if (!existsSync(settingsPath)) {
108
+ return { removed: false, reason: 'file not found' };
109
+ }
110
+
111
+ const settings = await loadSettings(targetPath);
112
+ const managed = settings[MORPH_MANAGED_KEY];
113
+
114
+ if (!managed) {
115
+ return { removed: false, reason: 'no _morph_managed section found' };
116
+ }
117
+
118
+ // Backup before modifying
119
+ const backupPath = settingsPath + '.backup';
120
+ await copyFile(settingsPath, backupPath);
121
+
122
+ // Remove managed permissions
123
+ if (managed.permissions_added && Array.isArray(settings.permissions)) {
124
+ settings.permissions = settings.permissions.filter(
125
+ p => !managed.permissions_added.includes(p)
126
+ );
127
+ if (settings.permissions.length === 0) {
128
+ delete settings.permissions;
129
+ }
130
+ }
131
+
132
+ // Remove managed env vars
133
+ if (managed.env_added && settings.env) {
134
+ for (const key of managed.env_added) {
135
+ delete settings.env[key];
136
+ }
137
+ if (Object.keys(settings.env).length === 0) {
138
+ delete settings.env;
139
+ }
140
+ }
141
+
142
+ // Remove managed hooks (v2 format: identified by path pattern)
143
+ if (settings.hooks) {
144
+ const morphPattern = /framework\/hooks\/claude-code\//;
145
+ const oldPattern = /agent-teams\/dispatch\.js/;
146
+
147
+ for (const [event, entries] of Object.entries(settings.hooks)) {
148
+ if (!Array.isArray(entries)) continue;
149
+
150
+ settings.hooks[event] = entries.filter(entry => {
151
+ if (!Array.isArray(entry.hooks)) return true;
152
+ const nonMorphHooks = entry.hooks.filter(h =>
153
+ typeof h.command !== 'string' ||
154
+ (!morphPattern.test(h.command) && !oldPattern.test(h.command))
155
+ );
156
+ return nonMorphHooks.length > 0;
157
+ });
158
+
159
+ if (settings.hooks[event].length === 0) {
160
+ delete settings.hooks[event];
161
+ }
162
+ }
163
+ if (Object.keys(settings.hooks).length === 0) {
164
+ delete settings.hooks;
165
+ }
166
+ }
167
+
168
+ // Remove managed MCP servers from .mcp.json (primary location)
169
+ if (managed.mcps_installed) {
170
+ const mcpJson = await loadMcpJson(targetPath);
171
+ if (mcpJson.mcpServers) {
172
+ for (const mcpName of managed.mcps_installed) {
173
+ delete mcpJson.mcpServers[mcpName];
174
+ }
175
+ if (Object.keys(mcpJson.mcpServers).length === 0) {
176
+ delete mcpJson.mcpServers;
177
+ }
178
+ // Write back (or remove file if empty)
179
+ if (Object.keys(mcpJson).length === 0) {
180
+ const mcpJsonPath = join(targetPath, '.mcp.json');
181
+ if (existsSync(mcpJsonPath)) {
182
+ const { unlink } = await import('fs/promises');
183
+ await unlink(mcpJsonPath);
184
+ }
185
+ } else {
186
+ await saveMcpJson(targetPath, mcpJson);
187
+ }
188
+ }
189
+
190
+ // Also clean legacy entries from settings.local.json
191
+ if (settings.mcpServers) {
192
+ for (const mcpName of managed.mcps_installed) {
193
+ delete settings.mcpServers[mcpName];
194
+ }
195
+ if (Object.keys(settings.mcpServers).length === 0) {
196
+ delete settings.mcpServers;
197
+ }
198
+ }
199
+ }
200
+
201
+ // Remove the managed marker
202
+ delete settings[MORPH_MANAGED_KEY];
203
+
204
+ await saveSettings(targetPath, settings);
205
+
206
+ return {
207
+ removed: true,
208
+ backupPath,
209
+ permissionsRemoved: managed.permissions_added || [],
210
+ envRemoved: managed.env_added || [],
211
+ hooksRemoved: managed.hooks_added || [],
212
+ mcpsRemoved: managed.mcps_installed || [],
213
+ previouslyGenerated: managed.generated_at
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Install MCP server entries into .mcp.json at the project root.
219
+ * Claude Code reads MCP servers from .mcp.json (not settings.local.json).
220
+ * Tracks installed names in settings.local.json _morph_managed.mcps_installed for safe reset.
221
+ *
222
+ * @param {string} targetPath - Project root directory
223
+ * @param {Object} servers - Map of server name → config { type?, command, args, env? }
224
+ * @returns {Promise<Object>} Summary of changes { added, skipped, total }
225
+ */
226
+ export async function installMcpServers(targetPath, servers) {
227
+ const mcpJson = await loadMcpJson(targetPath);
228
+
229
+ mcpJson.mcpServers = mcpJson.mcpServers || {};
230
+
231
+ const added = [];
232
+ const skipped = [];
233
+
234
+ for (const [name, config] of Object.entries(servers)) {
235
+ if (mcpJson.mcpServers[name]) {
236
+ skipped.push(name);
237
+ } else {
238
+ mcpJson.mcpServers[name] = config;
239
+ added.push(name);
240
+ }
241
+ }
242
+
243
+ await saveMcpJson(targetPath, mcpJson);
244
+
245
+ // Track in settings.local.json _morph_managed
246
+ const settings = await loadSettings(targetPath);
247
+ settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
248
+ settings[MORPH_MANAGED_KEY].mcps_installed =
249
+ settings[MORPH_MANAGED_KEY].mcps_installed || [];
250
+ for (const name of added) {
251
+ if (!settings[MORPH_MANAGED_KEY].mcps_installed.includes(name)) {
252
+ settings[MORPH_MANAGED_KEY].mcps_installed.push(name);
253
+ }
254
+ }
255
+ settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
256
+
257
+ await saveSettings(targetPath, settings);
258
+
259
+ return { added, skipped, total: Object.keys(mcpJson.mcpServers).length };
260
+ }
261
+
262
+ // ============================================================================
263
+ // Helpers
264
+ // ============================================================================
265
+
266
+ async function loadMcpJson(targetPath) {
267
+ const mcpJsonPath = join(targetPath, '.mcp.json');
268
+ try {
269
+ if (!existsSync(mcpJsonPath)) return {};
270
+ return JSON.parse(await readFile(mcpJsonPath, 'utf-8'));
271
+ } catch {
272
+ return {};
273
+ }
274
+ }
275
+
276
+ async function saveMcpJson(targetPath, data) {
277
+ const mcpJsonPath = join(targetPath, '.mcp.json');
278
+ await writeFile(mcpJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
279
+ }
280
+
281
+ async function loadSettings(targetPath) {
282
+ const settingsPath = join(targetPath, '.claude', 'settings.local.json');
283
+ try {
284
+ if (!existsSync(settingsPath)) return {};
285
+ return JSON.parse(await readFile(settingsPath, 'utf-8'));
286
+ } catch {
287
+ return {};
288
+ }
289
+ }
290
+
291
+ async function saveSettings(targetPath, settings) {
292
+ const claudeDir = join(targetPath, '.claude');
293
+ const settingsPath = join(claudeDir, 'settings.local.json');
294
+
295
+ if (!existsSync(claudeDir)) {
296
+ await mkdir(claudeDir, { recursive: true });
297
+ }
298
+
299
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
300
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import { join, dirname, resolve } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { WORKTREE_DIR_NAMES } from '../lib/worktree/dirs.js';
4
5
 
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const frameworkRoot = join(__dirname, '..', '..');
@@ -137,12 +138,12 @@ export async function createDirectoryLink(target, link) {
137
138
  }
138
139
  }
139
140
 
140
- export async function updateGitignore(projectPath) {
141
- const gitignorePath = join(projectPath, '.gitignore');
142
-
143
- const morphRules = [
144
- '',
145
- '# MORPH-SPEC',
141
+ /**
142
+ * The morph-owned block of the installed .gitignore. Exported so the worktree
143
+ * container canary (test/lib/worktree/dirs.test.js) can prove both names are
144
+ * covered without rendering a file.
145
+ */
146
+ export const MORPH_GITIGNORE_ENTRIES = [
146
147
  // Derivable, per-machine state index — rebuilt from the committed
147
148
  // .morph/features/{feature}/feature.json files. Never commit it: doing so
148
149
  // sweeps every feature's accumulated diffs into one commit. The feature
@@ -165,19 +166,23 @@ export async function updateGitignore(projectPath) {
165
166
  '.claude/agents/',
166
167
  '.claude/CLAUDE.md',
167
168
  '.claude/settings.local.json',
168
- '/worktrees/',
169
- // Legacy name (pre-C1). Kept so a project that already has `.worktrees/`
170
- // on disk does not suddenly see it as untracked dirt — which would also
171
- // make `finish`'s clean-root check refuse.
172
- '/.worktrees/',
169
+ // Both container names, anchored at the root. The legacy name (pre-C1) is
170
+ // kept so a project that already has `.worktrees/` on disk does not
171
+ // suddenly see it as untracked dirt — which would also make `finish`'s
172
+ // clean-root check refuse.
173
+ ...WORKTREE_DIR_NAMES.map((d) => `/${d}/`),
173
174
  '.playwright-mcp/',
174
175
  'docs/superpowers/',
175
176
  // Grafo de codebase — artefato derivado, reconstruído pelo hook do
176
177
  // graphify a cada commit. Committá-lo faria de todo commit um diff de
177
178
  // megabytes de JSON gerado.
178
179
  'graphify-out/',
179
- ''
180
- ];
180
+ ];
181
+
182
+ export async function updateGitignore(projectPath) {
183
+ const gitignorePath = join(projectPath, '.gitignore');
184
+
185
+ const morphRules = ['', '# MORPH-SPEC', ...MORPH_GITIGNORE_ENTRIES, ''];
181
186
 
182
187
  let content = '';
183
188
  let hasMorphSection = false;