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.
- package/CLAUDE.md +501 -28
- package/README.md +99 -57
- package/bin/specweave.js +16 -0
- package/dist/adapters/adapter-base.d.ts +21 -0
- package/dist/adapters/adapter-base.d.ts.map +1 -1
- package/dist/adapters/adapter-base.js +28 -0
- package/dist/adapters/adapter-base.js.map +1 -1
- package/dist/adapters/adapter-interface.d.ts +41 -0
- package/dist/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/adapters/claude/adapter.d.ts +36 -0
- package/dist/adapters/claude/adapter.d.ts.map +1 -1
- package/dist/adapters/claude/adapter.js +135 -0
- package/dist/adapters/claude/adapter.js.map +1 -1
- package/dist/adapters/copilot/adapter.d.ts +25 -0
- package/dist/adapters/copilot/adapter.d.ts.map +1 -1
- package/dist/adapters/copilot/adapter.js +112 -0
- package/dist/adapters/copilot/adapter.js.map +1 -1
- package/dist/adapters/cursor/adapter.d.ts +36 -0
- package/dist/adapters/cursor/adapter.d.ts.map +1 -1
- package/dist/adapters/cursor/adapter.js +140 -0
- package/dist/adapters/cursor/adapter.js.map +1 -1
- package/dist/adapters/generic/adapter.d.ts +25 -0
- package/dist/adapters/generic/adapter.d.ts.map +1 -1
- package/dist/adapters/generic/adapter.js +111 -0
- package/dist/adapters/generic/adapter.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +48 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/plugin.d.ts +37 -0
- package/dist/cli/commands/plugin.d.ts.map +1 -0
- package/dist/cli/commands/plugin.js +296 -0
- package/dist/cli/commands/plugin.js.map +1 -0
- package/dist/core/plugin-detector.d.ts +96 -0
- package/dist/core/plugin-detector.d.ts.map +1 -0
- package/dist/core/plugin-detector.js +349 -0
- package/dist/core/plugin-detector.js.map +1 -0
- package/dist/core/plugin-loader.d.ts +111 -0
- package/dist/core/plugin-loader.d.ts.map +1 -0
- package/dist/core/plugin-loader.js +319 -0
- package/dist/core/plugin-loader.js.map +1 -0
- package/dist/core/plugin-manager.d.ts +144 -0
- package/dist/core/plugin-manager.d.ts.map +1 -0
- package/dist/core/plugin-manager.js +393 -0
- package/dist/core/plugin-manager.js.map +1 -0
- package/dist/core/schemas/plugin-manifest.schema.json +253 -0
- package/dist/core/types/plugin.d.ts +252 -0
- package/dist/core/types/plugin.d.ts.map +1 -0
- package/dist/core/types/plugin.js +48 -0
- package/dist/core/types/plugin.js.map +1 -0
- package/dist/integrations/jira/jira-mapper.d.ts +2 -2
- package/dist/integrations/jira/jira-mapper.js +2 -2
- package/package.json +13 -9
- package/src/adapters/adapter-base.ts +33 -0
- package/src/adapters/adapter-interface.ts +46 -0
- package/src/adapters/claude/adapter.ts +164 -0
- package/src/adapters/copilot/adapter.ts +138 -0
- package/src/adapters/cursor/adapter.ts +170 -0
- package/src/adapters/generic/adapter.ts +137 -0
- package/src/commands/specweave.increment.md +48 -4
- package/src/hooks/post-increment-plugin-detect.sh +142 -0
- package/src/hooks/post-task-completion.sh +53 -11
- 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:
|
|
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
|
|
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
|
|
205
|
+
Wait for the skill to complete. Do NOT continue to Step 7 until the increment-planner skill returns.
|
|
162
206
|
|
|
163
|
-
### Step
|
|
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
|