@abarcenas/ai-workflow-template 1.45.0 → 1.47.0
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/.agents/config/README.md +43 -0
- package/.agents/config/domain.md +37 -0
- package/.agents/instructions/learned-knowledge.instructions.md +128 -0
- package/.agents/instructions/memory-bank.instructions.md +11 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +74 -0
- package/.agents/skills/grill-me/SKILL.md +7 -0
- package/.agents/skills/grill-with-docs/SKILL.md +7 -0
- package/.agents/skills/grilling/SKILL.md +28 -0
- package/.agents/skills/prototype/LOGIC.md +67 -0
- package/.agents/skills/prototype/SKILL.md +26 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.github/workflows/npm-publish.yml +10 -0
- package/.github/workflows/pr-test.yml +31 -0
- package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
- package/.opencode/agents/scope.agent.md +299 -0
- package/.prettierignore +26 -0
- package/AGENTS.md +46 -3
- package/README.md +29 -2
- package/opencode.mcp.example.json +1 -0
- package/package.json +3 -1
- package/scripts/bump-version.test.js +238 -0
- package/scripts/mcp/playwright-mcp-launcher.js +55 -29
- package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
- package/scripts/mcp-memory-server.js +93 -76
- package/scripts/mcp-memory-server.test.js +398 -0
- package/scripts/memory-cli.test.js +297 -0
- package/scripts/memory-index.js +119 -31
- package/scripts/memory-index.test.js +1173 -0
- package/scripts/normalize-memory.js +26 -1
- package/scripts/normalize-memory.test.js +753 -0
- package/scripts/setup/discover.test.js +124 -2
- package/scripts/setup/hooks.test.js +188 -1
- package/scripts/setup/husky-init.test.js +311 -0
- package/scripts/setup/index.js +35 -5
- package/scripts/setup/index.test.js +279 -0
- package/scripts/setup/knowledgebase.test.js +100 -0
- package/scripts/setup/skills-phase.js +568 -0
- package/scripts/setup/skills-phase.test.js +1274 -0
- package/scripts/setup/sync-phase.js +6 -1
- package/scripts/setup/sync-phase.test.js +180 -2
- package/scripts/setup/ui.test.js +579 -0
- package/scripts/setup/utils.test.js +586 -0
- package/scripts/sync-unit.test.js +1881 -0
- package/scripts/sync.js +816 -239
- package/scripts/sync.test.js +283 -0
- package/scripts/vocab-sync.test.js +88 -0
- package/skills-lock.json +90 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// playwright-mcp-launcher.test.js — Unit tests for the MCP launcher
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// `spawn` is mocked so no MCP server is ever launched. Signal handling and
|
|
6
|
+
// exit-code propagation are exercised through injected fakes (an EventEmitter
|
|
7
|
+
// double for the process and a fake child), plus a direct-run import that
|
|
8
|
+
// verifies the CLI bootstrap branch.
|
|
9
|
+
//
|
|
10
|
+
// Covered scenarios:
|
|
11
|
+
// 1. default args (no env flags)
|
|
12
|
+
// 2. HEADLESS=true adds --headless
|
|
13
|
+
// 3. HEADLESS=false does not
|
|
14
|
+
// 4. VIEWPORT adds --viewport <size>
|
|
15
|
+
// 5. HEADLESS + VIEWPORT together
|
|
16
|
+
// 6. spawn is called as `npx <args>` with inherited stdio + shell
|
|
17
|
+
// 7. exit code 0 is forwarded
|
|
18
|
+
// 8. non-zero exit code is forwarded
|
|
19
|
+
// 9. missing exit code falls back to 0
|
|
20
|
+
// 10. SIGINT / SIGTERM are forwarded to the child
|
|
21
|
+
// 11. launch returns the child
|
|
22
|
+
// 12. direct run auto-launches; imported elsewhere it does not
|
|
23
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
26
|
+
import { EventEmitter } from 'node:events'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
|
|
29
|
+
const cp = vi.hoisted(() => ({ spawn: vi.fn() }))
|
|
30
|
+
|
|
31
|
+
vi.mock('node:child_process', () => ({
|
|
32
|
+
spawn: cp.spawn,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
import { spawn } from 'node:child_process'
|
|
36
|
+
import { buildArgs, launch } from './playwright-mcp-launcher.js'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a fake ChildProcess.
|
|
40
|
+
*
|
|
41
|
+
* @returns {object} Fake child with kill/on and a helper to emit 'exit'.
|
|
42
|
+
*/
|
|
43
|
+
function makeChild() {
|
|
44
|
+
const handlers = {}
|
|
45
|
+
return {
|
|
46
|
+
kill: vi.fn(),
|
|
47
|
+
on: vi.fn((event, handler) => {
|
|
48
|
+
handlers[event] = handler
|
|
49
|
+
}),
|
|
50
|
+
emitExit(code) {
|
|
51
|
+
handlers.exit?.(code)
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('playwright-mcp-launcher', () => {
|
|
57
|
+
let child
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.clearAllMocks()
|
|
61
|
+
child = makeChild()
|
|
62
|
+
spawn.mockReturnValue(child)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.restoreAllMocks()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// ── buildArgs ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
it('buildArgs: returns only the package when no env flags are set', () => {
|
|
72
|
+
expect(buildArgs({})).toEqual(['@playwright/mcp@latest'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('buildArgs: HEADLESS=true adds --headless', () => {
|
|
76
|
+
expect(buildArgs({ HEADLESS: 'true' })).toEqual([
|
|
77
|
+
'@playwright/mcp@latest',
|
|
78
|
+
'--headless',
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('buildArgs: HEADLESS=false does not add --headless', () => {
|
|
83
|
+
expect(buildArgs({ HEADLESS: 'false' })).toEqual(['@playwright/mcp@latest'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('buildArgs: VIEWPORT adds a two-token viewport flag', () => {
|
|
87
|
+
expect(buildArgs({ VIEWPORT: '1280x720' })).toEqual([
|
|
88
|
+
'@playwright/mcp@latest',
|
|
89
|
+
'--viewport',
|
|
90
|
+
'1280x720',
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('buildArgs: combines headless and viewport flags', () => {
|
|
95
|
+
expect(buildArgs({ HEADLESS: 'true', VIEWPORT: '800x600' })).toEqual([
|
|
96
|
+
'@playwright/mcp@latest',
|
|
97
|
+
'--headless',
|
|
98
|
+
'--viewport',
|
|
99
|
+
'800x600',
|
|
100
|
+
])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// ── launch ───────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
it('launch: spawns npx with inherited stdio and shell', () => {
|
|
106
|
+
launch({ env: {}, proc: new EventEmitter(), exit: vi.fn() })
|
|
107
|
+
|
|
108
|
+
expect(spawn).toHaveBeenCalledWith('npx', ['@playwright/mcp@latest'], {
|
|
109
|
+
stdio: 'inherit',
|
|
110
|
+
shell: true,
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('launch: returns the spawned child', () => {
|
|
115
|
+
const returned = launch({
|
|
116
|
+
env: {},
|
|
117
|
+
proc: new EventEmitter(),
|
|
118
|
+
exit: vi.fn(),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(returned).toBe(child)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('launch: forwards exit code 0', () => {
|
|
125
|
+
const exit = vi.fn()
|
|
126
|
+
launch({ env: {}, proc: new EventEmitter(), exit })
|
|
127
|
+
|
|
128
|
+
child.emitExit(0)
|
|
129
|
+
|
|
130
|
+
expect(exit).toHaveBeenCalledWith(0)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('launch: forwards a non-zero exit code', () => {
|
|
134
|
+
const exit = vi.fn()
|
|
135
|
+
launch({ env: {}, proc: new EventEmitter(), exit })
|
|
136
|
+
|
|
137
|
+
child.emitExit(5)
|
|
138
|
+
|
|
139
|
+
expect(exit).toHaveBeenCalledWith(5)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('launch: falls back to 0 when no exit code is provided', () => {
|
|
143
|
+
const exit = vi.fn()
|
|
144
|
+
launch({ env: {}, proc: new EventEmitter(), exit })
|
|
145
|
+
|
|
146
|
+
child.emitExit(undefined)
|
|
147
|
+
|
|
148
|
+
expect(exit).toHaveBeenCalledWith(0)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('launch: forwards SIGINT to the child', () => {
|
|
152
|
+
const proc = new EventEmitter()
|
|
153
|
+
launch({ env: {}, proc, exit: vi.fn() })
|
|
154
|
+
|
|
155
|
+
proc.emit('SIGINT')
|
|
156
|
+
|
|
157
|
+
expect(child.kill).toHaveBeenCalledWith('SIGINT')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('launch: forwards SIGTERM to the child', () => {
|
|
161
|
+
const proc = new EventEmitter()
|
|
162
|
+
launch({ env: {}, proc, exit: vi.fn() })
|
|
163
|
+
|
|
164
|
+
proc.emit('SIGTERM')
|
|
165
|
+
|
|
166
|
+
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// ── Direct-run guard ─────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
it('direct run: auto-launches when argv[1] is this module', async () => {
|
|
172
|
+
const modulePath = fileURLToPath(
|
|
173
|
+
new URL('./playwright-mcp-launcher.js', import.meta.url),
|
|
174
|
+
)
|
|
175
|
+
const originalArgv1 = process.argv[1]
|
|
176
|
+
const intListeners = process.listeners('SIGINT')
|
|
177
|
+
const termListeners = process.listeners('SIGTERM')
|
|
178
|
+
process.argv[1] = modulePath
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
vi.resetModules()
|
|
182
|
+
await import('./playwright-mcp-launcher.js')
|
|
183
|
+
|
|
184
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
185
|
+
'npx',
|
|
186
|
+
expect.arrayContaining(['@playwright/mcp@latest']),
|
|
187
|
+
{ stdio: 'inherit', shell: true },
|
|
188
|
+
)
|
|
189
|
+
} finally {
|
|
190
|
+
process.argv[1] = originalArgv1
|
|
191
|
+
for (const listener of process.listeners('SIGINT')) {
|
|
192
|
+
if (!intListeners.includes(listener)) {
|
|
193
|
+
process.removeListener('SIGINT', listener)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const listener of process.listeners('SIGTERM')) {
|
|
197
|
+
if (!termListeners.includes(listener)) {
|
|
198
|
+
process.removeListener('SIGTERM', listener)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('imported with an empty argv[1]: does not launch', async () => {
|
|
205
|
+
const originalArgv1 = process.argv[1]
|
|
206
|
+
process.argv[1] = ''
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
vi.resetModules()
|
|
210
|
+
await import('./playwright-mcp-launcher.js')
|
|
211
|
+
|
|
212
|
+
expect(spawn).not.toHaveBeenCalled()
|
|
213
|
+
} finally {
|
|
214
|
+
process.argv[1] = originalArgv1
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
CallToolRequestSchema,
|
|
21
21
|
ListToolsRequestSchema,
|
|
22
22
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
+
import { resolve } from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
23
25
|
import { search, rebuildIndex, incrementalUpdate, getFile, getStats } from './memory-index.js';
|
|
24
26
|
|
|
25
27
|
const VALID_FILES = [
|
|
@@ -43,88 +45,92 @@ const server = new Server(
|
|
|
43
45
|
|
|
44
46
|
// ── List tools ───────────────────────────────────────────────────────
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
export async function listTools() {
|
|
49
|
+
return {
|
|
50
|
+
tools: [
|
|
51
|
+
{
|
|
52
|
+
name: 'memory_search',
|
|
53
|
+
description:
|
|
54
|
+
'Semantic search across the project memory bank (activeContext, systemPatterns, progress, decisions, tasks). ' +
|
|
55
|
+
'Returns ranked chunks with source file and similarity score. ' +
|
|
56
|
+
'Use this instead of reading all memory files to save tokens.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
query: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Natural language query about project context, decisions, or status',
|
|
63
|
+
},
|
|
64
|
+
topK: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
default: 5,
|
|
67
|
+
description: 'Number of results to return (1-20)',
|
|
68
|
+
},
|
|
69
|
+
sourceFile: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Optional: filter to a specific memory file (e.g., "activeContext", "systemPatterns")',
|
|
72
|
+
},
|
|
69
73
|
},
|
|
74
|
+
required: ['query'],
|
|
70
75
|
},
|
|
71
|
-
required: ['query'],
|
|
72
76
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
{
|
|
78
|
+
name: 'memory_rebuild',
|
|
79
|
+
description:
|
|
80
|
+
'Full rebuild of the memory vector index from memory-bank/*.md and docs/*.md files. ' +
|
|
81
|
+
'Re-indexes every file regardless of change. ' +
|
|
82
|
+
'Use memory_update instead for faster incremental sync (only changed files). ' +
|
|
83
|
+
'The index is disposable — rebuilding is safe.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {},
|
|
87
|
+
},
|
|
84
88
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
{
|
|
90
|
+
name: 'memory_update',
|
|
91
|
+
description:
|
|
92
|
+
'Incremental update — only re-indexes files whose content changed (SHA-256 content hash). ' +
|
|
93
|
+
'Much faster than memory_rebuild for daily use (~2 seconds vs minutes). ' +
|
|
94
|
+
'Run this after completing tasks, writing to memory-bank, or after git pull.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {},
|
|
98
|
+
},
|
|
95
99
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
{
|
|
101
|
+
name: 'memory_get',
|
|
102
|
+
description:
|
|
103
|
+
'Read the full content of a specific memory file by name. ' +
|
|
104
|
+
'Use when you need the complete file, not just search snippets.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
file: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'File ID without extension. One of: ' + VALID_FILES.join(', '),
|
|
111
|
+
enum: VALID_FILES,
|
|
112
|
+
},
|
|
109
113
|
},
|
|
114
|
+
required: ['file'],
|
|
110
115
|
},
|
|
111
|
-
required: ['file'],
|
|
112
116
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
{
|
|
118
|
+
name: 'memory_stats',
|
|
119
|
+
description: 'Get statistics about the memory index (vector count, file count, database size).',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {},
|
|
123
|
+
},
|
|
120
124
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
server.setRequestHandler(ListToolsRequestSchema, listTools);
|
|
124
130
|
|
|
125
131
|
// ── Handle tool calls ────────────────────────────────────────────────
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
export async function handleToolCall(request) {
|
|
128
134
|
const { name, arguments: args } = request.params;
|
|
129
135
|
|
|
130
136
|
try {
|
|
@@ -227,17 +233,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
227
233
|
isError: true,
|
|
228
234
|
};
|
|
229
235
|
}
|
|
230
|
-
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
server.setRequestHandler(CallToolRequestSchema, handleToolCall);
|
|
231
239
|
|
|
232
240
|
// ── Start server ─────────────────────────────────────────────────────
|
|
233
241
|
|
|
234
|
-
async function main() {
|
|
242
|
+
export async function main() {
|
|
235
243
|
const transport = new StdioServerTransport();
|
|
236
244
|
await server.connect(transport);
|
|
237
245
|
console.error('[memory-bank-mcp] Server started via stdio transport');
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
export function bootstrap() {
|
|
249
|
+
return main().catch((err) => {
|
|
250
|
+
console.error('[memory-bank-mcp] Fatal error:', err);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const isDirectRun =
|
|
256
|
+
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
257
|
+
|
|
258
|
+
if (isDirectRun) {
|
|
259
|
+
bootstrap();
|
|
260
|
+
}
|