specweave 0.4.0 → 0.4.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.
Files changed (62) hide show
  1. package/CLAUDE.md +501 -28
  2. package/README.md +99 -57
  3. package/bin/specweave.js +16 -0
  4. package/dist/adapters/adapter-base.d.ts +21 -0
  5. package/dist/adapters/adapter-base.d.ts.map +1 -1
  6. package/dist/adapters/adapter-base.js +28 -0
  7. package/dist/adapters/adapter-base.js.map +1 -1
  8. package/dist/adapters/adapter-interface.d.ts +41 -0
  9. package/dist/adapters/adapter-interface.d.ts.map +1 -1
  10. package/dist/adapters/claude/adapter.d.ts +36 -0
  11. package/dist/adapters/claude/adapter.d.ts.map +1 -1
  12. package/dist/adapters/claude/adapter.js +135 -0
  13. package/dist/adapters/claude/adapter.js.map +1 -1
  14. package/dist/adapters/copilot/adapter.d.ts +25 -0
  15. package/dist/adapters/copilot/adapter.d.ts.map +1 -1
  16. package/dist/adapters/copilot/adapter.js +112 -0
  17. package/dist/adapters/copilot/adapter.js.map +1 -1
  18. package/dist/adapters/cursor/adapter.d.ts +36 -0
  19. package/dist/adapters/cursor/adapter.d.ts.map +1 -1
  20. package/dist/adapters/cursor/adapter.js +140 -0
  21. package/dist/adapters/cursor/adapter.js.map +1 -1
  22. package/dist/adapters/generic/adapter.d.ts +25 -0
  23. package/dist/adapters/generic/adapter.d.ts.map +1 -1
  24. package/dist/adapters/generic/adapter.js +111 -0
  25. package/dist/adapters/generic/adapter.js.map +1 -1
  26. package/dist/cli/commands/init.d.ts.map +1 -1
  27. package/dist/cli/commands/init.js +48 -1
  28. package/dist/cli/commands/init.js.map +1 -1
  29. package/dist/cli/commands/plugin.d.ts +37 -0
  30. package/dist/cli/commands/plugin.d.ts.map +1 -0
  31. package/dist/cli/commands/plugin.js +296 -0
  32. package/dist/cli/commands/plugin.js.map +1 -0
  33. package/dist/core/plugin-detector.d.ts +96 -0
  34. package/dist/core/plugin-detector.d.ts.map +1 -0
  35. package/dist/core/plugin-detector.js +349 -0
  36. package/dist/core/plugin-detector.js.map +1 -0
  37. package/dist/core/plugin-loader.d.ts +111 -0
  38. package/dist/core/plugin-loader.d.ts.map +1 -0
  39. package/dist/core/plugin-loader.js +319 -0
  40. package/dist/core/plugin-loader.js.map +1 -0
  41. package/dist/core/plugin-manager.d.ts +144 -0
  42. package/dist/core/plugin-manager.d.ts.map +1 -0
  43. package/dist/core/plugin-manager.js +393 -0
  44. package/dist/core/plugin-manager.js.map +1 -0
  45. package/dist/core/schemas/plugin-manifest.schema.json +253 -0
  46. package/dist/core/types/plugin.d.ts +252 -0
  47. package/dist/core/types/plugin.d.ts.map +1 -0
  48. package/dist/core/types/plugin.js +48 -0
  49. package/dist/core/types/plugin.js.map +1 -0
  50. package/dist/integrations/jira/jira-mapper.d.ts +2 -2
  51. package/dist/integrations/jira/jira-mapper.js +2 -2
  52. package/package.json +13 -9
  53. package/src/adapters/adapter-base.ts +33 -0
  54. package/src/adapters/adapter-interface.ts +46 -0
  55. package/src/adapters/claude/adapter.ts +164 -0
  56. package/src/adapters/copilot/adapter.ts +138 -0
  57. package/src/adapters/cursor/adapter.ts +170 -0
  58. package/src/adapters/generic/adapter.ts +137 -0
  59. package/src/commands/specweave.increment.md +48 -4
  60. package/src/hooks/post-increment-plugin-detect.sh +142 -0
  61. package/src/hooks/post-task-completion.sh +53 -11
  62. package/src/hooks/pre-task-plugin-detect.sh +96 -0
@@ -11,6 +11,7 @@ import * as path from 'path';
11
11
  import fs from 'fs-extra';
12
12
  import { AdapterBase } from '../adapter-base.js';
13
13
  import { AdapterOptions, AdapterFile } from '../adapter-interface.js';
14
+ import type { Plugin } from '../../core/types/plugin.js';
14
15
 
15
16
  export class CopilotAdapter extends AdapterBase {
16
17
  name = 'copilot';
@@ -178,4 +179,141 @@ Note: For better automation, consider Claude Code (full) or Cursor (semi).
178
179
  Copilot is best for simple projects or when already using VS Code + Copilot.
179
180
  `;
180
181
  }
182
+
183
+ /**
184
+ * Check if Copilot adapter supports plugins
185
+ *
186
+ * Copilot has plugin support via AGENTS.md compilation (same as Cursor)
187
+ *
188
+ * @returns boolean True for Copilot
189
+ */
190
+ supportsPlugins(): boolean {
191
+ return true;
192
+ }
193
+
194
+ /**
195
+ * Compile and install a plugin for Copilot
196
+ *
197
+ * Copilot uses AGENTS.md compilation (same as Cursor)
198
+ *
199
+ * @param plugin Plugin to install
200
+ */
201
+ async compilePlugin(plugin: Plugin): Promise<void> {
202
+ const projectPath = process.cwd();
203
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
204
+
205
+ console.log(`\nšŸ“¦ Compiling plugin for Copilot: ${plugin.manifest.name}`);
206
+
207
+ if (!(await fs.pathExists(agentsMdPath))) {
208
+ throw new Error('AGENTS.md not found. Run specweave init first.');
209
+ }
210
+
211
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
212
+
213
+ const pluginMarker = `<!-- Plugin: ${plugin.manifest.name} -->`;
214
+ if (agentsMd.includes(pluginMarker)) {
215
+ console.log(` ā„¹ļø Plugin ${plugin.manifest.name} already compiled to AGENTS.md`);
216
+ return;
217
+ }
218
+
219
+ let pluginSection = `\n\n${pluginMarker}\n\n`;
220
+ pluginSection += `# Plugin: ${plugin.manifest.name}\n\n`;
221
+ pluginSection += `${plugin.manifest.description}\n\n`;
222
+
223
+ if (plugin.skills.length > 0) {
224
+ pluginSection += `## Skills\n\n`;
225
+ for (const skill of plugin.skills) {
226
+ const skillContent = await fs.readFile(path.join(skill.path, 'SKILL.md'), 'utf-8');
227
+ const contentWithoutFrontmatter = skillContent.replace(/^---\n[\s\S]+?\n---\n/, '');
228
+ pluginSection += `### ${skill.name}\n\n${contentWithoutFrontmatter}\n\n`;
229
+ }
230
+ }
231
+
232
+ if (plugin.agents.length > 0) {
233
+ pluginSection += `## Agents\n\n`;
234
+ for (const agent of plugin.agents) {
235
+ const agentContent = await fs.readFile(path.join(agent.path, 'AGENT.md'), 'utf-8');
236
+ pluginSection += `### ${agent.name}\n\n${agentContent}\n\n`;
237
+ }
238
+ }
239
+
240
+ if (plugin.commands.length > 0) {
241
+ pluginSection += `## Commands\n\n`;
242
+ for (const command of plugin.commands) {
243
+ const commandContent = await fs.readFile(command.path, 'utf-8');
244
+ const contentWithoutFrontmatter = commandContent.replace(/^---\n[\s\S]+?\n---\n/, '');
245
+ pluginSection += `### /${command.name}\n\n${contentWithoutFrontmatter}\n\n`;
246
+ }
247
+ }
248
+
249
+ pluginSection += `<!-- End Plugin: ${plugin.manifest.name} -->\n`;
250
+
251
+ agentsMd += pluginSection;
252
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
253
+
254
+ console.log(` āœ“ Compiled to AGENTS.md`);
255
+ console.log(` āœ“ ${plugin.skills.length} skills, ${plugin.agents.length} agents, ${plugin.commands.length} commands`);
256
+ console.log(`\nāœ… Plugin ${plugin.manifest.name} compiled for Copilot!`);
257
+ }
258
+
259
+ /**
260
+ * Unload a plugin from Copilot
261
+ */
262
+ async unloadPlugin(pluginName: string): Promise<void> {
263
+ const projectPath = process.cwd();
264
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
265
+
266
+ console.log(`\nšŸ—‘ļø Unloading plugin from Copilot: ${pluginName}`);
267
+
268
+ if (!(await fs.pathExists(agentsMdPath))) {
269
+ console.warn(`āš ļø AGENTS.md not found`);
270
+ return;
271
+ }
272
+
273
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
274
+
275
+ const startMarker = `<!-- Plugin: ${pluginName} -->`;
276
+ const endMarker = `<!-- End Plugin: ${pluginName} -->`;
277
+
278
+ const startIndex = agentsMd.indexOf(startMarker);
279
+ if (startIndex === -1) {
280
+ console.warn(`āš ļø Plugin ${pluginName} not found in AGENTS.md`);
281
+ return;
282
+ }
283
+
284
+ const endIndex = agentsMd.indexOf(endMarker, startIndex);
285
+ if (endIndex === -1) {
286
+ console.warn(`āš ļø Plugin ${pluginName} section malformed`);
287
+ return;
288
+ }
289
+
290
+ agentsMd = agentsMd.slice(0, startIndex) + agentsMd.slice(endIndex + endMarker.length);
291
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
292
+
293
+ console.log(` āœ“ Removed from AGENTS.md`);
294
+ console.log(`\nāœ… Plugin ${pluginName} unloaded!`);
295
+ }
296
+
297
+ /**
298
+ * Get list of installed plugins for Copilot
299
+ */
300
+ async getInstalledPlugins(): Promise<string[]> {
301
+ const projectPath = process.cwd();
302
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
303
+
304
+ if (!(await fs.pathExists(agentsMdPath))) {
305
+ return [];
306
+ }
307
+
308
+ const agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
309
+ const pluginMarkerRegex = /<!-- Plugin: (specweave-[a-z0-9-]+) -->/g;
310
+ const matches = agentsMd.matchAll(pluginMarkerRegex);
311
+
312
+ const plugins: string[] = [];
313
+ for (const match of matches) {
314
+ plugins.push(match[1]);
315
+ }
316
+
317
+ return plugins;
318
+ }
181
319
  }
@@ -12,6 +12,7 @@ import * as path from 'path';
12
12
  import fs from 'fs-extra';
13
13
  import { AdapterBase } from '../adapter-base.js';
14
14
  import { AdapterOptions, AdapterFile } from '../adapter-interface.js';
15
+ import type { Plugin } from '../../core/types/plugin.js';
15
16
 
16
17
  export class CursorAdapter extends AdapterBase {
17
18
  name = 'cursor';
@@ -219,4 +220,173 @@ Pro tip: Say "act as [role]" to follow AGENTS.md patterns:
219
220
  - "act as DevOps and create infrastructure" (AGENTS.md defines DevOps role)
220
221
  `;
221
222
  }
223
+
224
+ /**
225
+ * Check if Cursor adapter supports plugins
226
+ *
227
+ * Cursor has plugin support via AGENTS.md compilation
228
+ *
229
+ * @returns boolean True for Cursor
230
+ */
231
+ supportsPlugins(): boolean {
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * Compile and install a plugin for Cursor
237
+ *
238
+ * Cursor uses AGENTS.md compilation:
239
+ * - Append skills to AGENTS.md
240
+ * - Append agents to AGENTS.md
241
+ * - Append commands to team commands JSON
242
+ *
243
+ * @param plugin Plugin to install
244
+ */
245
+ async compilePlugin(plugin: Plugin): Promise<void> {
246
+ const projectPath = process.cwd();
247
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
248
+
249
+ console.log(`\nšŸ“¦ Compiling plugin for Cursor: ${plugin.manifest.name}`);
250
+
251
+ // Ensure AGENTS.md exists
252
+ if (!(await fs.pathExists(agentsMdPath))) {
253
+ throw new Error('AGENTS.md not found. Run specweave init first.');
254
+ }
255
+
256
+ // Read current AGENTS.md
257
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
258
+
259
+ // Check if plugin already compiled
260
+ const pluginMarker = `<!-- Plugin: ${plugin.manifest.name} -->`;
261
+ if (agentsMd.includes(pluginMarker)) {
262
+ console.log(` ā„¹ļø Plugin ${plugin.manifest.name} already compiled to AGENTS.md`);
263
+ return;
264
+ }
265
+
266
+ // Generate plugin section for AGENTS.md
267
+ let pluginSection = `\n\n${pluginMarker}\n\n`;
268
+ pluginSection += `# Plugin: ${plugin.manifest.name}\n\n`;
269
+ pluginSection += `${plugin.manifest.description}\n\n`;
270
+
271
+ // Add skills
272
+ if (plugin.skills.length > 0) {
273
+ pluginSection += `## Skills\n\n`;
274
+ for (const skill of plugin.skills) {
275
+ const skillContent = await fs.readFile(path.join(skill.path, 'SKILL.md'), 'utf-8');
276
+ // Remove frontmatter for AGENTS.md
277
+ const contentWithoutFrontmatter = skillContent.replace(/^---\n[\s\S]+?\n---\n/, '');
278
+ pluginSection += `### ${skill.name}\n\n`;
279
+ pluginSection += `${contentWithoutFrontmatter}\n\n`;
280
+ }
281
+ }
282
+
283
+ // Add agents
284
+ if (plugin.agents.length > 0) {
285
+ pluginSection += `## Agents\n\n`;
286
+ for (const agent of plugin.agents) {
287
+ const agentContent = await fs.readFile(path.join(agent.path, 'AGENT.md'), 'utf-8');
288
+ pluginSection += `### ${agent.name}\n\n`;
289
+ pluginSection += `${agentContent}\n\n`;
290
+ }
291
+ }
292
+
293
+ // Add commands
294
+ if (plugin.commands.length > 0) {
295
+ pluginSection += `## Commands\n\n`;
296
+ for (const command of plugin.commands) {
297
+ const commandContent = await fs.readFile(command.path, 'utf-8');
298
+ // Remove frontmatter
299
+ const contentWithoutFrontmatter = commandContent.replace(/^---\n[\s\S]+?\n---\n/, '');
300
+ pluginSection += `### /${command.name}\n\n`;
301
+ pluginSection += `${contentWithoutFrontmatter}\n\n`;
302
+ }
303
+ }
304
+
305
+ pluginSection += `<!-- End Plugin: ${plugin.manifest.name} -->\n`;
306
+
307
+ // Append to AGENTS.md
308
+ agentsMd += pluginSection;
309
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
310
+
311
+ console.log(` āœ“ Compiled to AGENTS.md`);
312
+ console.log(` āœ“ ${plugin.skills.length} skills added`);
313
+ console.log(` āœ“ ${plugin.agents.length} agents added`);
314
+ console.log(` āœ“ ${plugin.commands.length} commands added`);
315
+
316
+ console.log(`\nāœ… Plugin ${plugin.manifest.name} compiled for Cursor!`);
317
+ }
318
+
319
+ /**
320
+ * Unload a plugin from Cursor
321
+ *
322
+ * Removes plugin section from AGENTS.md
323
+ *
324
+ * @param pluginName Name of plugin to unload
325
+ */
326
+ async unloadPlugin(pluginName: string): Promise<void> {
327
+ const projectPath = process.cwd();
328
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
329
+
330
+ console.log(`\nšŸ—‘ļø Unloading plugin from Cursor: ${pluginName}`);
331
+
332
+ if (!(await fs.pathExists(agentsMdPath))) {
333
+ console.warn(`āš ļø AGENTS.md not found`);
334
+ return;
335
+ }
336
+
337
+ // Read AGENTS.md
338
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
339
+
340
+ // Find plugin section
341
+ const startMarker = `<!-- Plugin: ${pluginName} -->`;
342
+ const endMarker = `<!-- End Plugin: ${pluginName} -->`;
343
+
344
+ const startIndex = agentsMd.indexOf(startMarker);
345
+ if (startIndex === -1) {
346
+ console.warn(`āš ļø Plugin ${pluginName} not found in AGENTS.md`);
347
+ return;
348
+ }
349
+
350
+ const endIndex = agentsMd.indexOf(endMarker, startIndex);
351
+ if (endIndex === -1) {
352
+ console.warn(`āš ļø Plugin ${pluginName} section malformed in AGENTS.md`);
353
+ return;
354
+ }
355
+
356
+ // Remove plugin section
357
+ agentsMd = agentsMd.slice(0, startIndex) + agentsMd.slice(endIndex + endMarker.length);
358
+
359
+ // Write back
360
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
361
+
362
+ console.log(` āœ“ Removed from AGENTS.md`);
363
+ console.log(`\nāœ… Plugin ${pluginName} unloaded!`);
364
+ }
365
+
366
+ /**
367
+ * Get list of installed plugins for Cursor
368
+ *
369
+ * Parses AGENTS.md for plugin markers
370
+ *
371
+ * @returns Array of installed plugin names
372
+ */
373
+ async getInstalledPlugins(): Promise<string[]> {
374
+ const projectPath = process.cwd();
375
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
376
+
377
+ if (!(await fs.pathExists(agentsMdPath))) {
378
+ return [];
379
+ }
380
+
381
+ const agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
382
+ const pluginMarkerRegex = /<!-- Plugin: (specweave-[a-z0-9-]+) -->/g;
383
+ const matches = agentsMd.matchAll(pluginMarkerRegex);
384
+
385
+ const plugins: string[] = [];
386
+ for (const match of matches) {
387
+ plugins.push(match[1]);
388
+ }
389
+
390
+ return plugins;
391
+ }
222
392
  }
@@ -12,6 +12,7 @@ import * as path from 'path';
12
12
  import fs from 'fs-extra';
13
13
  import { AdapterBase } from '../adapter-base.js';
14
14
  import { AdapterOptions, AdapterFile } from '../adapter-interface.js';
15
+ import type { Plugin } from '../../core/types/plugin.js';
15
16
 
16
17
  export class GenericAdapter extends AdapterBase {
17
18
  name = 'generic';
@@ -160,4 +161,140 @@ You're ready to build with SpecWeave using ANY AI tool!
160
161
  Remember: AGENTS.md is the universal standard - it works everywhere!
161
162
  `;
162
163
  }
164
+
165
+ /**
166
+ * Check if Generic adapter supports plugins
167
+ *
168
+ * Generic has manual plugin support via AGENTS.md
169
+ *
170
+ * @returns boolean True
171
+ */
172
+ supportsPlugins(): boolean {
173
+ return true;
174
+ }
175
+
176
+ /**
177
+ * Compile and install a plugin for Generic adapter
178
+ *
179
+ * Appends plugin content to AGENTS.md for manual copy-paste workflows
180
+ *
181
+ * @param plugin Plugin to install
182
+ */
183
+ async compilePlugin(plugin: Plugin): Promise<void> {
184
+ const projectPath = process.cwd();
185
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
186
+
187
+ console.log(`\nšŸ“¦ Adding plugin to AGENTS.md: ${plugin.manifest.name}`);
188
+
189
+ if (!(await fs.pathExists(agentsMdPath))) {
190
+ throw new Error('AGENTS.md not found. Run specweave init first.');
191
+ }
192
+
193
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
194
+
195
+ const pluginMarker = `<!-- Plugin: ${plugin.manifest.name} -->`;
196
+ if (agentsMd.includes(pluginMarker)) {
197
+ console.log(` ā„¹ļø Plugin ${plugin.manifest.name} already in AGENTS.md`);
198
+ return;
199
+ }
200
+
201
+ let pluginSection = `\n\n${pluginMarker}\n\n`;
202
+ pluginSection += `# Plugin: ${plugin.manifest.name}\n\n`;
203
+ pluginSection += `${plugin.manifest.description}\n\n`;
204
+
205
+ if (plugin.skills.length > 0) {
206
+ pluginSection += `## Skills\n\n`;
207
+ for (const skill of plugin.skills) {
208
+ const skillContent = await fs.readFile(path.join(skill.path, 'SKILL.md'), 'utf-8');
209
+ const contentWithoutFrontmatter = skillContent.replace(/^---\n[\s\S]+?\n---\n/, '');
210
+ pluginSection += `### ${skill.name}\n\n${contentWithoutFrontmatter}\n\n`;
211
+ }
212
+ }
213
+
214
+ if (plugin.agents.length > 0) {
215
+ pluginSection += `## Agents\n\n`;
216
+ for (const agent of plugin.agents) {
217
+ const agentContent = await fs.readFile(path.join(agent.path, 'AGENT.md'), 'utf-8');
218
+ pluginSection += `### ${agent.name}\n\n${agentContent}\n\n`;
219
+ }
220
+ }
221
+
222
+ if (plugin.commands.length > 0) {
223
+ pluginSection += `## Workflows (Manual)\n\n`;
224
+ for (const command of plugin.commands) {
225
+ const commandContent = await fs.readFile(command.path, 'utf-8');
226
+ const contentWithoutFrontmatter = commandContent.replace(/^---\n[\s\S]+?\n---\n/, '');
227
+ pluginSection += `### ${command.name.replace('specweave.', '')}\n\n${contentWithoutFrontmatter}\n\n`;
228
+ }
229
+ }
230
+
231
+ pluginSection += `<!-- End Plugin: ${plugin.manifest.name} -->\n`;
232
+
233
+ agentsMd += pluginSection;
234
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
235
+
236
+ console.log(` āœ“ Added to AGENTS.md (copy-paste workflows)`);
237
+ console.log(`\nāœ… Plugin ${plugin.manifest.name} available for manual use!`);
238
+ }
239
+
240
+ /**
241
+ * Unload a plugin from Generic adapter
242
+ */
243
+ async unloadPlugin(pluginName: string): Promise<void> {
244
+ const projectPath = process.cwd();
245
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
246
+
247
+ console.log(`\nšŸ—‘ļø Removing plugin from AGENTS.md: ${pluginName}`);
248
+
249
+ if (!(await fs.pathExists(agentsMdPath))) {
250
+ console.warn(`āš ļø AGENTS.md not found`);
251
+ return;
252
+ }
253
+
254
+ let agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
255
+
256
+ const startMarker = `<!-- Plugin: ${pluginName} -->`;
257
+ const endMarker = `<!-- End Plugin: ${pluginName} -->`;
258
+
259
+ const startIndex = agentsMd.indexOf(startMarker);
260
+ if (startIndex === -1) {
261
+ console.warn(`āš ļø Plugin ${pluginName} not found in AGENTS.md`);
262
+ return;
263
+ }
264
+
265
+ const endIndex = agentsMd.indexOf(endMarker, startIndex);
266
+ if (endIndex === -1) {
267
+ console.warn(`āš ļø Plugin ${pluginName} section malformed`);
268
+ return;
269
+ }
270
+
271
+ agentsMd = agentsMd.slice(0, startIndex) + agentsMd.slice(endIndex + endMarker.length);
272
+ await fs.writeFile(agentsMdPath, agentsMd, 'utf-8');
273
+
274
+ console.log(` āœ“ Removed from AGENTS.md`);
275
+ console.log(`\nāœ… Plugin ${pluginName} removed!`);
276
+ }
277
+
278
+ /**
279
+ * Get list of installed plugins for Generic adapter
280
+ */
281
+ async getInstalledPlugins(): Promise<string[]> {
282
+ const projectPath = process.cwd();
283
+ const agentsMdPath = path.join(projectPath, 'AGENTS.md');
284
+
285
+ if (!(await fs.pathExists(agentsMdPath))) {
286
+ return [];
287
+ }
288
+
289
+ const agentsMd = await fs.readFile(agentsMdPath, 'utf-8');
290
+ const pluginMarkerRegex = /<!-- Plugin: (specweave-[a-z0-9-]+) -->/g;
291
+ const matches = agentsMd.matchAll(pluginMarkerRegex);
292
+
293
+ const plugins: string[] = [];
294
+ for (const match of matches) {
295
+ plugins.push(match[1]);
296
+ }
297
+
298
+ return plugins;
299
+ }
163
300
  }
@@ -117,7 +117,51 @@ You are helping the user create a new SpecWeave increment with automatic closure
117
117
  - "What's the short name?" (e.g., "user-authentication" for increment 003-user-authentication)
118
118
  - "Priority? (P1/P2/P3)" (default: P1)
119
119
 
120
- ### Step 4: Activate Increment Planning Workflow
120
+ ### Step 4: Detect Suggested Plugins (T-019 - Plugin Auto-Detection)
121
+
122
+ **šŸ”Œ NEW IN v0.4.0**: Auto-detect plugins based on increment description
123
+
124
+ Before planning, analyze the feature description for plugin keywords and suggest relevant plugins:
125
+
126
+ ```bash
127
+ # Example feature descriptions and their plugin suggestions:
128
+ "Deploy to Kubernetes" → kubernetes plugin
129
+ "Add Stripe payments" → payment-processing plugin
130
+ "Create React dashboard" → frontend-stack plugin
131
+ "Build FastAPI backend" → backend-stack plugin
132
+ "Sync with GitHub issues" → github plugin
133
+ "Integrate with Jira" → jira plugin
134
+ ```
135
+
136
+ **Detection Logic**:
137
+ 1. Extract keywords from feature description
138
+ 2. Match against plugin triggers (from manifest.json)
139
+ 3. Check if plugin already enabled
140
+ 4. Suggest new plugins only
141
+
142
+ **Output Format**:
143
+ ```
144
+ šŸ’” Plugin Detection
145
+
146
+ Analyzing feature: "Add authentication with NextJS and Stripe"
147
+
148
+ Suggested plugins:
149
+ āœ… frontend-stack (NextJS detected)
150
+ āœ… payment-processing (Stripe detected)
151
+
152
+ Would you like to enable these plugins? (Y/n)
153
+ ```
154
+
155
+ **If user confirms**:
156
+ - Enable plugins via PluginManager
157
+ - Plugins become available for increment planning
158
+ - Skills/agents from plugins can be used immediately
159
+
160
+ **If user declines**:
161
+ - Continue without plugins
162
+ - User can enable later: `specweave plugin enable <name>`
163
+
164
+ ### Step 5: Activate Increment Planning Workflow
121
165
 
122
166
  **🚨 CRITICAL - YOU MUST USE THE SKILL TOOL:**
123
167
 
@@ -149,7 +193,7 @@ The increment-planner skill will:
149
193
  - āœ… Quality gates enforced
150
194
  - āŒ Direct file writing bypasses entire workflow
151
195
 
152
- ### Step 5: Skill Tool Invocation (MANDATORY)
196
+ ### Step 6: Skill Tool Invocation (MANDATORY)
153
197
 
154
198
  **BEFORE PROCEEDING, USE THE SKILL TOOL:**
155
199
 
@@ -158,9 +202,9 @@ You must literally call the Skill tool like this:
158
202
  Skill(command: "increment-planner")
159
203
  ```
160
204
 
161
- Wait for the skill to complete. Do NOT continue to Step 6 until the increment-planner skill returns.
205
+ Wait for the skill to complete. Do NOT continue to Step 7 until the increment-planner skill returns.
162
206
 
163
- ### Step 6: Alternative Approach (ONLY IF SKILL FAILS)
207
+ ### Step 7: Alternative Approach (ONLY IF SKILL FAILS)
164
208
 
165
209
  **Only use this if Skill tool is unavailable or fails:**
166
210
 
@@ -0,0 +1,142 @@
1
+ #!/bin/bash
2
+
3
+ #
4
+ # Post-Increment Plugin Detection Hook (T-021)
5
+ #
6
+ # Runs AFTER increment completion to suggest plugins based on git diff
7
+ # This is Phase 4 of the 4-phase plugin detection system
8
+ #
9
+ # Trigger: After `/specweave.done` completes an increment
10
+ # Output: Plugin suggestions for NEXT increment
11
+ #
12
+
13
+ # Exit on error
14
+ set -e
15
+
16
+ # Get project root (assumes hook runs from project directory)
17
+ PROJECT_ROOT=$(pwd)
18
+
19
+ # Colors for output
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[1;33m'
22
+ CYAN='\033[0;36m'
23
+ GRAY='\033[0;37m'
24
+ MAGENTA='\033[0;35m'
25
+ NC='\033[0m' # No Color
26
+
27
+ # Check if SpecWeave is installed
28
+ if ! command -v specweave &> /dev/null; then
29
+ # Silent exit if SpecWeave not installed (development mode)
30
+ exit 0
31
+ fi
32
+
33
+ # Check if git repository exists
34
+ if [ ! -d ".git" ]; then
35
+ # Not a git repo, skip detection
36
+ exit 0
37
+ fi
38
+
39
+ # Get completed increment number (passed as argument)
40
+ INCREMENT_NUM="$1"
41
+
42
+ if [ -z "$INCREMENT_NUM" ]; then
43
+ # No increment number provided, skip
44
+ exit 0
45
+ fi
46
+
47
+ # Get git diff since last increment tag (or HEAD if no tags)
48
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~10")
49
+ GIT_DIFF=$(git diff --name-only "$LAST_TAG"..HEAD 2>/dev/null || echo "")
50
+
51
+ if [ -z "$GIT_DIFF" ]; then
52
+ # No changes detected, skip
53
+ exit 0
54
+ fi
55
+
56
+ # Also check package.json changes for new dependencies
57
+ PACKAGE_JSON_CHANGED=false
58
+ if echo "$GIT_DIFF" | grep -q "package.json"; then
59
+ PACKAGE_JSON_CHANGED=true
60
+ fi
61
+
62
+ # Keyword-based plugin detection from file changes
63
+ declare -A PLUGIN_PATTERNS=(
64
+ ["specweave-github"]=".github/|github-"
65
+ ["specweave-kubernetes"]="kubernetes/|k8s/|helm/|\.yaml$"
66
+ ["specweave-frontend-stack"]="components/|pages/|app/|\.tsx$|\.jsx$"
67
+ ["specweave-backend-stack"]="api/|server/|routes/|controllers/"
68
+ ["specweave-ml-ops"]="models/|training/|\.ipynb$|\.h5$|\.pkl$"
69
+ ["specweave-payment-processing"]="payments/|billing/|stripe"
70
+ ["specweave-figma"]="\.figma|design/|mockups/"
71
+ ["specweave-observability"]="prometheus/|grafana/|monitoring/"
72
+ ["specweave-diagrams"]="diagrams/|\.mmd$|architecture/"
73
+ )
74
+
75
+ # Dependency-based detection (if package.json changed)
76
+ declare -A DEPENDENCY_PLUGINS=(
77
+ ["specweave-frontend-stack"]="react|vue|angular|next"
78
+ ["specweave-backend-stack"]="express|fastapi|django|nestjs"
79
+ ["specweave-payment-processing"]="stripe|paypal"
80
+ ["specweave-ml-ops"]="tensorflow|pytorch|scikit-learn"
81
+ ["specweave-observability"]="prometheus-client|@opentelemetry"
82
+ )
83
+
84
+ # Detect plugins based on file patterns
85
+ SUGGESTED_PLUGINS=()
86
+
87
+ # Check file patterns
88
+ for plugin in "${!PLUGIN_PATTERNS[@]}"; do
89
+ pattern="${PLUGIN_PATTERNS[$plugin]}"
90
+
91
+ if echo "$GIT_DIFF" | grep -qi -E "$pattern"; then
92
+ # Check if plugin is already enabled
93
+ if ! specweave plugin list --enabled 2>/dev/null | grep -q "$plugin"; then
94
+ if [[ ! " ${SUGGESTED_PLUGINS[@]} " =~ " ${plugin} " ]]; then
95
+ SUGGESTED_PLUGINS+=("$plugin")
96
+ fi
97
+ fi
98
+ fi
99
+ done
100
+
101
+ # Check package.json dependencies if it changed
102
+ if [ "$PACKAGE_JSON_CHANGED" = true ] && [ -f "package.json" ]; then
103
+ DEPENDENCIES=$(cat package.json | grep -A 100 '"dependencies"' | grep -A 100 '"devDependencies"' || echo "")
104
+
105
+ for plugin in "${!DEPENDENCY_PLUGINS[@]}"; do
106
+ keywords="${DEPENDENCY_PLUGINS[$plugin]}"
107
+
108
+ if echo "$DEPENDENCIES" | grep -qi -E "$keywords"; then
109
+ if ! specweave plugin list --enabled 2>/dev/null | grep -q "$plugin"; then
110
+ if [[ ! " ${SUGGESTED_PLUGINS[@]} " =~ " ${plugin} " ]]; then
111
+ SUGGESTED_PLUGINS+=("$plugin")
112
+ fi
113
+ fi
114
+ fi
115
+ done
116
+ fi
117
+
118
+ # Output suggestions if any found
119
+ if [ ${#SUGGESTED_PLUGINS[@]} -gt 0 ]; then
120
+ echo ""
121
+ echo -e "${MAGENTA}══════════════════════════════════════════════════════${NC}"
122
+ echo -e "${CYAN}šŸ’” Plugin Suggestions for Next Increment${NC}"
123
+ echo -e "${MAGENTA}══════════════════════════════════════════════════════${NC}"
124
+ echo ""
125
+ echo -e "${GRAY}Increment ${INCREMENT_NUM} introduced changes that suggest:${NC}"
126
+ echo ""
127
+
128
+ for plugin in "${SUGGESTED_PLUGINS[@]}"; do
129
+ echo -e " ✨ ${GREEN}${plugin}${NC}"
130
+ done
131
+
132
+ echo ""
133
+ echo -e "${YELLOW}Consider enabling these plugins for your next increment:${NC}"
134
+ echo -e "${CYAN} specweave plugin enable <name>${NC}"
135
+ echo ""
136
+ echo -e "${GRAY}Plugins will auto-activate skills and agents for better support${NC}"
137
+ echo -e "${MAGENTA}══════════════════════════════════════════════════════${NC}"
138
+ echo ""
139
+ fi
140
+
141
+ # Always exit successfully (non-blocking)
142
+ exit 0