@polymorphism-tech/morph-spec 4.3.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +155 -0
- package/bin/morph-spec.js +2 -2
- package/bin/task-manager.cjs +102 -14
- package/package.json +1 -1
- package/src/commands/agents/agents-fuse.js +2 -1
- package/src/commands/project/detect-agents.js +31 -1
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +52 -52
- package/src/commands/project/init.js +8 -1
- package/src/commands/project/update.js +12 -2
- package/src/commands/state/advance-phase.js +19 -4
- package/src/commands/state/state.js +14 -12
- package/src/commands/tasks/task.js +1 -1
- package/src/commands/threads/thread-template.js +1 -1
- package/src/core/state/state-manager.js +19 -15
- package/src/core/workflows/workflow-detector.js +14 -1
- package/src/lib/checkpoints/checkpoint-hooks.js +8 -3
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- package/src/lib/generators/recap-generator.js +30 -10
- package/src/lib/validators/validation-runner.js +8 -24
- package/src/utils/hooks-installer.js +69 -0
|
@@ -129,12 +129,15 @@ Based on detected patterns in your codebase:
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (config?.language === 'javascript') {
|
|
132
|
+
const techLine = config.technologies?.length > 0
|
|
133
|
+
? `\n- **Framework**: ${config.technologies.join(', ')}`
|
|
134
|
+
: '';
|
|
135
|
+
|
|
132
136
|
return `### JavaScript / TypeScript Conventions
|
|
133
137
|
|
|
134
138
|
Based on detected patterns in your codebase:
|
|
135
139
|
|
|
136
|
-
- **Package Manager**: ${config.packageManager}
|
|
137
|
-
- **Framework**: ${config.technologies.join(', ')}
|
|
140
|
+
- **Package Manager**: ${config.packageManager}${techLine}
|
|
138
141
|
|
|
139
142
|
**Recommendation**: Refer to framework standards for JavaScript best practices.`;
|
|
140
143
|
}
|
|
@@ -149,6 +152,37 @@ function generateArchitectureSection(structure) {
|
|
|
149
152
|
const { architecture } = structure;
|
|
150
153
|
|
|
151
154
|
const descriptions = {
|
|
155
|
+
'cli-library': `### CLI / Library Architecture
|
|
156
|
+
|
|
157
|
+
Your project follows a CLI/Library pattern:
|
|
158
|
+
|
|
159
|
+
- ✅ **bin/** entry points detected
|
|
160
|
+
- ✅ **src/** source directory detected
|
|
161
|
+
- ✅ **package.json** present
|
|
162
|
+
|
|
163
|
+
**Key principles**:
|
|
164
|
+
- Commands exposed via bin/
|
|
165
|
+
- Core logic in src/commands/ and src/lib/
|
|
166
|
+
- Public API exported from src/index.js`,
|
|
167
|
+
|
|
168
|
+
'nextjs-app-router': `### Next.js App Router Architecture
|
|
169
|
+
|
|
170
|
+
Your project uses the Next.js App Router pattern:
|
|
171
|
+
|
|
172
|
+
- ✅ **app/** directory detected (React Server Components)
|
|
173
|
+
|
|
174
|
+
**Key principles**:
|
|
175
|
+
- Server Components by default
|
|
176
|
+
- Client Components with 'use client' directive
|
|
177
|
+
- Route handlers in app/api/`,
|
|
178
|
+
|
|
179
|
+
'express-mvc': `### Express MVC Architecture
|
|
180
|
+
|
|
181
|
+
Your project uses Express with MVC pattern:
|
|
182
|
+
|
|
183
|
+
- ✅ **routes/** detected
|
|
184
|
+
- ✅ **controllers/** detected`,
|
|
185
|
+
|
|
152
186
|
'clean-architecture': `### Clean Architecture
|
|
153
187
|
|
|
154
188
|
Your project follows Clean Architecture pattern:
|
|
@@ -203,19 +237,29 @@ function generateRecommendations(structure, config, conversation) {
|
|
|
203
237
|
}
|
|
204
238
|
}
|
|
205
239
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
if (structure?.stack === 'nodejs' || config?.language === 'javascript') {
|
|
241
|
+
if (!structure?.patterns?.includes('Unit Tests')) {
|
|
242
|
+
recommendations.push('No unit tests detected - consider adding test coverage with Jest or Vitest');
|
|
243
|
+
}
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
245
|
+
if (structure?.architecture === 'unknown') {
|
|
246
|
+
recommendations.push('No clear architecture detected - consider organizing code into src/commands/, src/lib/, src/core/');
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// Recommend tests (generic / .NET)
|
|
250
|
+
if (!structure?.patterns?.includes('Unit Tests')) {
|
|
251
|
+
recommendations.push('No unit tests detected - consider adding test project');
|
|
252
|
+
}
|
|
215
253
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
254
|
+
// Recommend DI (only meaningful for C# / .NET)
|
|
255
|
+
if (config?.language === 'csharp' && !structure?.patterns?.includes('Dependency Injection')) {
|
|
256
|
+
recommendations.push('Consider implementing Dependency Injection pattern');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Architecture recommendations (.NET)
|
|
260
|
+
if (structure?.architecture === 'unknown' && structure?.folders?.hasServices) {
|
|
261
|
+
recommendations.push('Service layer detected but no clear architecture - consider Clean Architecture or CQRS');
|
|
262
|
+
}
|
|
219
263
|
}
|
|
220
264
|
|
|
221
265
|
return recommendations;
|
|
@@ -250,14 +294,30 @@ function identifyGaps(structure, config) {
|
|
|
250
294
|
*/
|
|
251
295
|
function filterImportantDependencies(deps) {
|
|
252
296
|
const important = deps.filter(dep => {
|
|
253
|
-
// Framework packages
|
|
297
|
+
// .NET Framework packages
|
|
254
298
|
if (dep.includes('Microsoft.') || dep.includes('System.')) return true;
|
|
255
|
-
// UI libraries
|
|
299
|
+
// Blazor UI libraries
|
|
256
300
|
if (dep.includes('Blazor') || dep.includes('FluentUI') || dep.includes('Mud')) return true;
|
|
257
|
-
//
|
|
301
|
+
// .NET common tools
|
|
258
302
|
if (dep.includes('Hangfire') || dep.includes('Serilog') || dep.includes('AutoMapper')) return true;
|
|
259
|
-
// AI/ML
|
|
303
|
+
// AI/ML (.NET)
|
|
260
304
|
if (dep.includes('Agents') || dep.includes('AI') || dep.includes('OpenAI')) return true;
|
|
305
|
+
// JS: frontend frameworks
|
|
306
|
+
if (['next', 'react', 'react-dom', 'vue', 'nuxt', 'svelte', 'astro'].includes(dep)) return true;
|
|
307
|
+
// JS: backend frameworks
|
|
308
|
+
if (['express', 'fastify', 'hono', 'koa', 'nestjs', '@nestjs/core'].includes(dep)) return true;
|
|
309
|
+
// JS: databases / ORMs
|
|
310
|
+
if (['prisma', '@prisma/client', 'supabase', '@supabase/supabase-js', 'drizzle-orm', 'mongoose', 'typeorm'].includes(dep)) return true;
|
|
311
|
+
// JS: auth
|
|
312
|
+
if (['next-auth', 'clerk', '@clerk/nextjs', 'lucia', 'better-auth'].includes(dep)) return true;
|
|
313
|
+
// JS: validation / schema
|
|
314
|
+
if (['zod', 'yup', 'joi', 'valibot'].includes(dep)) return true;
|
|
315
|
+
// JS: testing
|
|
316
|
+
if (['vitest', 'jest', '@jest/core', 'mocha', 'cypress', 'playwright'].includes(dep)) return true;
|
|
317
|
+
// JS: UI / styling
|
|
318
|
+
if (['tailwindcss', '@mui/material', 'shadcn-ui', 'radix-ui', 'chakra-ui'].includes(dep)) return true;
|
|
319
|
+
// JS: AI
|
|
320
|
+
if (dep.includes('openai') || dep.includes('anthropic') || dep.includes('langchain') || dep.includes('ai')) return true;
|
|
261
321
|
return false;
|
|
262
322
|
});
|
|
263
323
|
|
|
@@ -97,9 +97,11 @@ async function detectStack(projectPath) {
|
|
|
97
97
|
]
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
const globIgnore = ['stacks/**', 'framework/**', 'test/**', 'node_modules/**', '.morph/**'];
|
|
101
|
+
|
|
100
102
|
for (const [stack, globs] of Object.entries(patterns)) {
|
|
101
103
|
for (const pattern of globs) {
|
|
102
|
-
const files = await glob(pattern, { cwd: projectPath, nodir: true });
|
|
104
|
+
const files = await glob(pattern, { cwd: projectPath, nodir: true, ignore: globIgnore });
|
|
103
105
|
if (files.length > 0) {
|
|
104
106
|
return stack;
|
|
105
107
|
}
|
|
@@ -113,7 +115,10 @@ async function detectStack(projectPath) {
|
|
|
113
115
|
* Detect architecture pattern
|
|
114
116
|
*/
|
|
115
117
|
async function detectArchitecture(projectPath) {
|
|
118
|
+
const globIgnore = ['stacks/**', 'framework/**', 'test/**', 'node_modules/**', '.morph/**'];
|
|
119
|
+
|
|
116
120
|
const checks = [
|
|
121
|
+
// .NET: Clean Architecture
|
|
117
122
|
{
|
|
118
123
|
pattern: 'clean-architecture',
|
|
119
124
|
indicators: [
|
|
@@ -122,13 +127,15 @@ async function detectArchitecture(projectPath) {
|
|
|
122
127
|
existsSync(join(projectPath, 'src', 'Infrastructure'))
|
|
123
128
|
]
|
|
124
129
|
},
|
|
130
|
+
// .NET: CQRS
|
|
125
131
|
{
|
|
126
132
|
pattern: 'cqrs',
|
|
127
133
|
indicators: [
|
|
128
|
-
await glob('**/Commands/**/*.cs', { cwd: projectPath }).then(f => f.length > 0),
|
|
129
|
-
await glob('**/Queries/**/*.cs', { cwd: projectPath }).then(f => f.length > 0)
|
|
134
|
+
await glob('**/Commands/**/*.cs', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0),
|
|
135
|
+
await glob('**/Queries/**/*.cs', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0)
|
|
130
136
|
]
|
|
131
137
|
},
|
|
138
|
+
// .NET: MVC
|
|
132
139
|
{
|
|
133
140
|
pattern: 'mvc',
|
|
134
141
|
indicators: [
|
|
@@ -136,6 +143,31 @@ async function detectArchitecture(projectPath) {
|
|
|
136
143
|
existsSync(join(projectPath, 'Models')),
|
|
137
144
|
existsSync(join(projectPath, 'Views'))
|
|
138
145
|
]
|
|
146
|
+
},
|
|
147
|
+
// JS: CLI / Library (bin + src + lib)
|
|
148
|
+
{
|
|
149
|
+
pattern: 'cli-library',
|
|
150
|
+
indicators: [
|
|
151
|
+
existsSync(join(projectPath, 'bin')),
|
|
152
|
+
existsSync(join(projectPath, 'src')),
|
|
153
|
+
existsSync(join(projectPath, 'package.json'))
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
// JS: Next.js App Router
|
|
157
|
+
{
|
|
158
|
+
pattern: 'nextjs-app-router',
|
|
159
|
+
indicators: [
|
|
160
|
+
existsSync(join(projectPath, 'app')),
|
|
161
|
+
await glob('app/**/*.{js,ts,jsx,tsx}', { cwd: projectPath, ignore: globIgnore }).then(f => f.length > 0)
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
// JS: Express MVC
|
|
165
|
+
{
|
|
166
|
+
pattern: 'express-mvc',
|
|
167
|
+
indicators: [
|
|
168
|
+
existsSync(join(projectPath, 'src', 'routes')) || existsSync(join(projectPath, 'routes')),
|
|
169
|
+
existsSync(join(projectPath, 'src', 'controllers')) || existsSync(join(projectPath, 'controllers'))
|
|
170
|
+
]
|
|
139
171
|
}
|
|
140
172
|
];
|
|
141
173
|
|
|
@@ -176,47 +208,43 @@ async function detectUILibrary(projectPath) {
|
|
|
176
208
|
*/
|
|
177
209
|
async function detectPatterns(projectPath) {
|
|
178
210
|
const patterns = [];
|
|
211
|
+
// Ignore template/vendor directories; keep test/ accessible for JS test detection
|
|
212
|
+
const globIgnore = ['stacks/**', 'framework/**', 'test/fixtures/**', 'node_modules/**', '.morph/**'];
|
|
179
213
|
|
|
180
|
-
//
|
|
181
|
-
const
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: 'Hangfire Jobs',
|
|
204
|
-
glob: '**/*Job.cs'
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
name: 'AI Agents',
|
|
208
|
-
glob: '**/*Agent.cs'
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: 'Unit Tests',
|
|
212
|
-
glob: '**/*.Tests/**/*.cs'
|
|
213
|
-
}
|
|
214
|
+
// C# / .NET patterns
|
|
215
|
+
const dotnetChecks = [
|
|
216
|
+
{ name: 'Repository Pattern', glob: '**/*Repository.cs' },
|
|
217
|
+
{ name: 'Service Layer', glob: '**/*Service.cs' },
|
|
218
|
+
{ name: 'DTOs', glob: '**/*Dto.cs' },
|
|
219
|
+
{ name: 'Entity Framework', glob: '**/Migrations/**/*.cs' },
|
|
220
|
+
{ name: 'Dependency Injection', glob: '**/DependencyInjection.cs' },
|
|
221
|
+
{ name: 'Hangfire Jobs', glob: '**/*Job.cs' },
|
|
222
|
+
{ name: 'AI Agents', glob: '**/*Agent.cs' },
|
|
223
|
+
{ name: 'Unit Tests', glob: '**/*.Tests/**/*.cs' }
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
// JavaScript / TypeScript patterns
|
|
227
|
+
const jsChecks = [
|
|
228
|
+
{ name: 'Service Layer', glob: 'src/**/*Service.js' },
|
|
229
|
+
{ name: 'Service Layer', glob: 'src/**/*Service.ts' },
|
|
230
|
+
{ name: 'Repository Pattern', glob: 'src/**/*Repository.js' },
|
|
231
|
+
{ name: 'Repository Pattern', glob: 'src/**/*Repository.ts' },
|
|
232
|
+
{ name: 'API Routes', glob: 'src/**/routes/**/*.{js,ts}' },
|
|
233
|
+
{ name: 'API Routes', glob: 'src/commands/**/*.{js,ts}' },
|
|
234
|
+
{ name: 'Unit Tests', glob: 'test/**/*.test.{js,ts}' },
|
|
235
|
+
{ name: 'Unit Tests', glob: 'src/**/*.test.{js,ts}' },
|
|
236
|
+
{ name: 'Unit Tests', glob: 'src/**/*.spec.{js,ts}' }
|
|
214
237
|
];
|
|
215
238
|
|
|
216
|
-
|
|
217
|
-
|
|
239
|
+
const allChecks = [...dotnetChecks, ...jsChecks];
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
|
|
242
|
+
for (const { name, glob: pattern } of allChecks) {
|
|
243
|
+
if (seen.has(name)) continue;
|
|
244
|
+
const files = await glob(pattern, { cwd: projectPath, nodir: true, ignore: globIgnore });
|
|
218
245
|
if (files.length > 0) {
|
|
219
246
|
patterns.push(name);
|
|
247
|
+
seen.add(name);
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
250
|
|
|
@@ -75,18 +75,38 @@ export async function generateRecap(projectPath, featureName, options = {}) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function getTasksSummary(feature) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// v2: feature.tasks is an array of task objects
|
|
79
|
+
if (Array.isArray(feature.tasks)) {
|
|
80
|
+
const tasks = feature.tasks;
|
|
81
|
+
const completed = tasks.filter(t => t.status === 'completed');
|
|
82
|
+
const pending = tasks.filter(t => t.status === 'pending');
|
|
83
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
84
|
+
return {
|
|
85
|
+
total: tasks.length,
|
|
86
|
+
completed: completed.length,
|
|
87
|
+
pending: pending.length,
|
|
88
|
+
inProgress: inProgress.length,
|
|
89
|
+
percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
|
|
90
|
+
items: tasks
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// v3: feature.tasks is a counter object {total, completed, inProgress, pending}
|
|
95
|
+
// Use feature.taskList for individual items if available
|
|
96
|
+
const counters = feature.tasks || {};
|
|
97
|
+
const items = feature.taskList || [];
|
|
98
|
+
const total = counters.total || items.length || 0;
|
|
99
|
+
const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
|
|
100
|
+
const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
|
|
101
|
+
const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
|
|
82
102
|
|
|
83
103
|
return {
|
|
84
|
-
total
|
|
85
|
-
completed:
|
|
86
|
-
pending:
|
|
87
|
-
inProgress:
|
|
88
|
-
percentage:
|
|
89
|
-
items
|
|
104
|
+
total,
|
|
105
|
+
completed: completedCount,
|
|
106
|
+
pending: pendingCount,
|
|
107
|
+
inProgress: inProgressCount,
|
|
108
|
+
percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
|
|
109
|
+
items
|
|
90
110
|
};
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -187,39 +187,23 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
case 'blazor': {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return await validateBlazorPatterns(projectPath, options);
|
|
193
|
-
} catch {
|
|
194
|
-
return null; // Validator may not exist or have different export
|
|
195
|
-
}
|
|
190
|
+
const { validateBlazorPatterns } = await import('./blazor/blazor-validator.js');
|
|
191
|
+
return await validateBlazorPatterns(projectPath, options);
|
|
196
192
|
}
|
|
197
193
|
|
|
198
194
|
case 'blazor-concurrency': {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return await analyzeConcurrency(projectPath, options);
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
195
|
+
const { analyzeConcurrency } = await import('./blazor/blazor-concurrency-analyzer.js');
|
|
196
|
+
return await analyzeConcurrency(projectPath, options);
|
|
205
197
|
}
|
|
206
198
|
|
|
207
199
|
case 'blazor-state': {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return await validateState(projectPath, options);
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
200
|
+
const { validateState } = await import('./blazor/blazor-state-validator.js');
|
|
201
|
+
return await validateState(projectPath, options);
|
|
214
202
|
}
|
|
215
203
|
|
|
216
204
|
case 'css': {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return await validateCss(projectPath, options);
|
|
220
|
-
} catch {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
205
|
+
const { validateCss } = await import('./css/css-validator.js');
|
|
206
|
+
return await validateCss(projectPath, options);
|
|
223
207
|
}
|
|
224
208
|
|
|
225
209
|
case 'contract-compliance': {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Hooks Installer
|
|
3
|
+
*
|
|
4
|
+
* Merges the MORPH-SPEC agent-teams PostToolUse hook into
|
|
5
|
+
* .claude/settings.local.json without overwriting existing config.
|
|
6
|
+
*
|
|
7
|
+
* Called by `morph-spec init` and `morph-spec update`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
|
|
14
|
+
const DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Install or update the MORPH-SPEC PostToolUse hook in .claude/settings.local.json.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} targetPath - Project root directory
|
|
20
|
+
* @returns {boolean} true if the hook was newly added, false if it was already present
|
|
21
|
+
*/
|
|
22
|
+
export async function installClaudeHooks(targetPath) {
|
|
23
|
+
const claudeDir = join(targetPath, '.claude');
|
|
24
|
+
const settingsPath = join(claudeDir, 'settings.local.json');
|
|
25
|
+
|
|
26
|
+
// Read existing settings or start empty
|
|
27
|
+
let settings = {};
|
|
28
|
+
if (existsSync(settingsPath)) {
|
|
29
|
+
try {
|
|
30
|
+
settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
31
|
+
} catch {
|
|
32
|
+
// Malformed JSON — start fresh to avoid corrupting the file
|
|
33
|
+
settings = {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Ensure hooks.PostToolUse array exists
|
|
38
|
+
settings.hooks = settings.hooks || {};
|
|
39
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
40
|
+
|
|
41
|
+
// Check if dispatch.js is already registered (avoid duplicate entries)
|
|
42
|
+
const alreadyRegistered = settings.hooks.PostToolUse.some(entry =>
|
|
43
|
+
Array.isArray(entry.hooks) &&
|
|
44
|
+
entry.hooks.some(h => h.command === DISPATCH_COMMAND)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (alreadyRegistered) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add the agent-teams dispatch hook
|
|
52
|
+
settings.hooks.PostToolUse.push({
|
|
53
|
+
matcher: 'Bash',
|
|
54
|
+
hooks: [
|
|
55
|
+
{
|
|
56
|
+
type: 'command',
|
|
57
|
+
command: DISPATCH_COMMAND
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Ensure .claude directory exists
|
|
63
|
+
if (!existsSync(claudeDir)) {
|
|
64
|
+
await mkdir(claudeDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
68
|
+
return true;
|
|
69
|
+
}
|