@abarcenas/ai-workflow-template 1.34.0 → 1.39.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.
Files changed (34) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +238 -0
  2. package/.agents/instructions/learned-knowledge.instructions.md +91 -0
  3. package/.husky/post-commit +10 -0
  4. package/.husky/post-merge +0 -1
  5. package/.opencode/agents/architect.agent.md +1 -0
  6. package/.opencode/agents/coder.agent.md +1 -0
  7. package/.opencode/agents/deployer.agent.md +1 -0
  8. package/.opencode/agents/designer.agent.md +1 -0
  9. package/.opencode/agents/e2e-tester.agent.md +1 -0
  10. package/.opencode/agents/implementer.agent.md +1 -0
  11. package/.opencode/agents/orchestrator/feature-pipeline.agent.md +1 -0
  12. package/.opencode/agents/orchestrator/orchestrator.agent.md +1 -0
  13. package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +1 -0
  14. package/.opencode/agents/researcher.agent.md +1 -0
  15. package/.opencode/agents/reviewer.agent.md +1 -0
  16. package/.opencode/agents/tracker.agent.md +2 -0
  17. package/.opencode/agents/unit-tester.agent.md +1 -0
  18. package/README.md +1 -1
  19. package/memory-bank/.vocabulary.json +8 -2
  20. package/package.json +12 -6
  21. package/scripts/knowledgebase-cli.js +228 -0
  22. package/scripts/knowledgebase-cli.test.js +576 -0
  23. package/scripts/knowledgebase-index.js +786 -0
  24. package/scripts/knowledgebase-index.test.js +759 -0
  25. package/scripts/knowledgebase-init.sql +193 -0
  26. package/scripts/mcp-knowledgebase-server.js +330 -0
  27. package/scripts/setup/constants.js +36 -3
  28. package/scripts/setup/hooks.js +2 -2
  29. package/scripts/setup/hooks.test.js +13 -5
  30. package/scripts/setup/index.js +29 -2
  31. package/scripts/setup/index.test.js +36 -1
  32. package/scripts/setup/knowledgebase.js +204 -0
  33. package/scripts/setup/knowledgebase.test.js +248 -0
  34. package/scripts/sync.js +40 -6
@@ -0,0 +1,248 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // knowledgebase.test.js — Unit tests for the knowledgebase registration module
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // Tests registerKnowledgebase() from knowledgebase.js — the Phase 6 setup
6
+ // module that registers the consumer project in the centralized knowledgebase.
7
+ //
8
+ // Covered scenarios (6 tests per T17 requirements):
9
+ // 1. skipKnowledgebase flag → early return with action='skipped'
10
+ // 2. No DATABASE_URL → graceful skip with warning message
11
+ // 3. Missing project name → graceful skip
12
+ // 4. Successful index → spawn exits 0, parses chunk counts
13
+ // 5. CLI failure → non-zero exit, action='failed'
14
+ // 6. Spawn error event → ENOENT, graceful handling
15
+ // ═════════════════════════════════════════════════════════════════════════════
16
+
17
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
18
+
19
+ // ── Mocks (vitest hoists these to the top of the module) ──────────────────
20
+
21
+ vi.mock('child_process', () => ({
22
+ spawn: vi.fn(),
23
+ }))
24
+
25
+ vi.mock('./ui.js', () => ({
26
+ verbose: vi.fn(),
27
+ }))
28
+
29
+ vi.mock('./utils.js', () => ({
30
+ resolvePackageRoot: vi.fn(() => '/fake/package/root'),
31
+ logInfo: vi.fn(),
32
+ logWarn: vi.fn(),
33
+ logError: vi.fn(),
34
+ }))
35
+
36
+ import { spawn } from 'child_process'
37
+ import { registerKnowledgebase } from './knowledgebase.js'
38
+
39
+ // ── Test helpers ──────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * Creates a mock child process that mimics a spawned Node.js script.
43
+ *
44
+ * Supports two modes controlled by `emitError`:
45
+ * - false (default): registers stdout/stderr 'data' handlers and a 'close'
46
+ * handler, then fires them asynchronously on a microtask so the calling
47
+ * code's event listeners are registered before the events fire.
48
+ * - true: registers and fires an 'error' handler instead (for ENOENT etc.)
49
+ *
50
+ * @param {object} opts
51
+ * @param {number} [opts.exitCode=0] - Exit code for the 'close' event
52
+ * @param {string} [opts.stdoutData=''] - Data emitted via stdout 'data'
53
+ * @param {string} [opts.stderrData=''] - Data emitted via stderr 'data'
54
+ * @param {boolean} [opts.emitError=false] - If true, emit 'error' instead
55
+ * @returns {object} Mock child process with .stdout.on, .stderr.on, .on
56
+ */
57
+ function createMockChild({
58
+ exitCode = 0,
59
+ stdoutData = '',
60
+ stderrData = '',
61
+ emitError = false,
62
+ } = {}) {
63
+ const stdoutHandlers = []
64
+ const stderrHandlers = []
65
+ let closeHandler = null
66
+ let errorHandler = null
67
+
68
+ const child = {
69
+ stdout: {
70
+ on: (event, handler) => {
71
+ if (event === 'data') stdoutHandlers.push(handler)
72
+ },
73
+ },
74
+ stderr: {
75
+ on: (event, handler) => {
76
+ if (event === 'data') stderrHandlers.push(handler)
77
+ },
78
+ },
79
+ on: (event, handler) => {
80
+ if (event === 'close') closeHandler = handler
81
+ if (event === 'error') errorHandler = handler
82
+ },
83
+ }
84
+
85
+ // Fire events on a microtask so handlers are registered first
86
+ Promise.resolve().then(() => {
87
+ if (emitError) {
88
+ if (errorHandler) {
89
+ errorHandler(new Error('ENOENT: no such file or directory'))
90
+ }
91
+ } else {
92
+ if (stdoutData) {
93
+ stdoutHandlers.forEach((h) => h(Buffer.from(stdoutData)))
94
+ }
95
+ if (stderrData) {
96
+ stderrHandlers.forEach((h) => h(Buffer.from(stderrData)))
97
+ }
98
+ if (closeHandler) closeHandler(exitCode)
99
+ }
100
+ })
101
+
102
+ return child
103
+ }
104
+
105
+ /**
106
+ * Builds a minimal context object accepted by registerKnowledgebase().
107
+ *
108
+ * Defaults provide a valid DATABASE_URL and consumerPackageJson.name so that
109
+ * tests can focus on one axis of variation at a time without repeating
110
+ * setup code.
111
+ *
112
+ * @param {object} [overrides] - Properties to override on the default context.
113
+ * @returns {object} Context object.
114
+ */
115
+ function makeContext(overrides = {}) {
116
+ return {
117
+ flags: {},
118
+ verbose: false,
119
+ consumerRoot: '/fake/consumer',
120
+ consumerPackageJson: { name: 'test-consumer' },
121
+ ...overrides,
122
+ }
123
+ }
124
+
125
+ // ── Tests ─────────────────────────────────────────────────────────────────
126
+
127
+ describe('registerKnowledgebase', () => {
128
+ beforeEach(() => {
129
+ vi.clearAllMocks()
130
+ // Provide a valid DATABASE_URL by default so only tests that explicitly
131
+ // delete it exercise the missing-URL path.
132
+ process.env.DATABASE_URL = 'postgres://localhost:5432/knowledgebase'
133
+ })
134
+
135
+ afterEach(() => {
136
+ delete process.env.DATABASE_URL
137
+ })
138
+
139
+ // ── 1. Skip flag ───────────────────────────────────────────────────────
140
+
141
+ it('returns action="skipped" when flags.skipKnowledgebase is true', async () => {
142
+ const ctx = makeContext({ flags: { skipKnowledgebase: true } })
143
+
144
+ const result = await registerKnowledgebase(ctx)
145
+
146
+ expect(result).toEqual({
147
+ action: 'skipped',
148
+ message: expect.stringContaining('skip-knowledgebase'),
149
+ })
150
+ // No child process should be spawned when the flag short-circuits
151
+ expect(spawn).not.toHaveBeenCalled()
152
+ })
153
+
154
+ // ── 2. Missing DATABASE_URL ───────────────────────────────────────────
155
+
156
+ it('returns action="skipped" with warning when DATABASE_URL is not set', async () => {
157
+ delete process.env.DATABASE_URL
158
+ const ctx = makeContext()
159
+
160
+ const result = await registerKnowledgebase(ctx)
161
+
162
+ expect(result).toEqual({
163
+ action: 'skipped',
164
+ message: expect.stringContaining('DATABASE_URL'),
165
+ })
166
+ expect(spawn).not.toHaveBeenCalled()
167
+ })
168
+
169
+ // ── 3. Missing project name ───────────────────────────────────────────
170
+
171
+ it('returns action="skipped" when consumerPackageJson has no name field', async () => {
172
+ const ctx = makeContext({ consumerPackageJson: {} })
173
+
174
+ const result = await registerKnowledgebase(ctx)
175
+
176
+ expect(result).toEqual({
177
+ action: 'skipped',
178
+ message: expect.stringContaining('project name'),
179
+ })
180
+ expect(spawn).not.toHaveBeenCalled()
181
+ })
182
+
183
+ // ── 4. Successful index ───────────────────────────────────────────────
184
+
185
+ it('returns action="indexed" with parsed chunk counts on successful spawn', async () => {
186
+ vi.mocked(spawn).mockImplementation(() =>
187
+ createMockChild({
188
+ exitCode: 0,
189
+ stdoutData: [
190
+ '[knowledgebase]',
191
+ 'Indexed 5 new, updated 3, skipped 2 chunks',
192
+ 'from project "test-consumer"',
193
+ ].join(' '),
194
+ }),
195
+ )
196
+
197
+ const ctx = makeContext()
198
+ const result = await registerKnowledgebase(ctx)
199
+
200
+ expect(result).toEqual({
201
+ action: 'indexed',
202
+ message: expect.stringContaining('8 chunks indexed'),
203
+ chunks_indexed: 8, // 5 new + 3 updated
204
+ chunks_skipped: 2,
205
+ })
206
+ expect(spawn).toHaveBeenCalledTimes(1)
207
+ })
208
+
209
+ // ── 5. CLI failure (non-zero exit) ────────────────────────────────────
210
+
211
+ it('returns action="failed" when the CLI exits with a non-zero code', async () => {
212
+ vi.mocked(spawn).mockImplementation(() =>
213
+ createMockChild({
214
+ exitCode: 1,
215
+ stderrData: 'Error: connection refused',
216
+ }),
217
+ )
218
+
219
+ const ctx = makeContext()
220
+ const result = await registerKnowledgebase(ctx)
221
+
222
+ expect(result).toEqual({
223
+ action: 'failed',
224
+ message: expect.stringContaining('exit code 1'),
225
+ })
226
+ expect(spawn).toHaveBeenCalledTimes(1)
227
+ })
228
+
229
+ // ── 6. Spawn error event (ENOENT) ─────────────────────────────────────
230
+
231
+ it('gracefully handles spawn error (ENOENT) returning action="failed"', async () => {
232
+ vi.mocked(spawn).mockImplementation(() =>
233
+ createMockChild({ emitError: true }),
234
+ )
235
+
236
+ const ctx = makeContext()
237
+
238
+ // The function must not throw — the spawn 'error' event resolves the
239
+ // internal promise with success=false, and registerKnowledgebase
240
+ // converts that into a graceful 'failed' result.
241
+ const result = await registerKnowledgebase(ctx)
242
+
243
+ expect(result.action).toBe('failed')
244
+ expect(result.message).toContain('exit code -1')
245
+ expect(result.message).toContain('ENOENT')
246
+ expect(spawn).toHaveBeenCalledTimes(1)
247
+ })
248
+ })
package/scripts/sync.js CHANGED
@@ -9,7 +9,9 @@ const sourceAgentsDir = resolve(packageRoot, '.agents')
9
9
  const consumerRoot = process.env.INIT_CWD ? resolve(process.env.INIT_CWD) : process.cwd()
10
10
  const targetAgentsDir = resolve(consumerRoot, '.agents')
11
11
  const manifestPath = resolve(consumerRoot, '.agents-sync-manifest.json')
12
- const excludedRelativePaths = new Set([])
12
+ const excludedRelativePaths = new Set([
13
+ 'instructions/learned-knowledge.instructions.md',
14
+ ])
13
15
  const args = new Set(process.argv.slice(2))
14
16
  const forceOverwrite = args.has('--force')
15
17
  const dryRun = args.has('--dry-run')
@@ -77,7 +79,7 @@ function syncFile(sourceFile, targetFile) {
77
79
  copyFileSync(sourceFile, targetFile)
78
80
  }
79
81
 
80
- const rootFiles = ['AGENTS.md', 'opencode.mcp.example.json', 'skills-lock.json']
82
+ const rootFiles = ['AGENTS.md', 'opencode.json', 'skills-lock.json']
81
83
 
82
84
  if (!existsSync(sourceAgentsDir)) {
83
85
  console.warn('[ai-workflow-template] No .agents directory found to sync.')
@@ -180,6 +182,11 @@ const scriptsToSync = [
180
182
  'validate-memory-schema.js',
181
183
  'mcp-memory-server.js',
182
184
  'mcp/playwright-mcp-launcher.js',
185
+ // Knowledgebase scripts
186
+ 'knowledgebase-cli.js',
187
+ 'knowledgebase-index.js',
188
+ 'mcp-knowledgebase-server.js',
189
+ 'knowledgebase-init.sql',
183
190
  ]
184
191
 
185
192
  const sourceScriptsDir = resolve(packageRoot, 'scripts')
@@ -333,14 +340,41 @@ for (const [fileName, fileContent] of Object.entries(memoryBankStubs)) {
333
340
  scaffolded += 1
334
341
  }
335
342
 
336
- /* ── Auto-copy opencode.mcp.json from example ────────────────── */
343
+ /* ── Scaffold .agents/instructions/ directory ───────────────── */
344
+
345
+ const agentInstructionsStubs = {
346
+ 'learned-knowledge.instructions.md': `# Learned Knowledge
347
+
348
+ Cross-session knowledge accumulated by agent pipelines. Each session records discoveries, patterns, gotchas, and agent tuning notes for future reference.
349
+
350
+ ---
351
+
352
+ `,
353
+ }
354
+
355
+ for (const [fileName, fileContent] of Object.entries(agentInstructionsStubs)) {
356
+ const targetFile = resolve(targetAgentsDir, 'instructions', fileName)
357
+ const trackedKey = `__agents-instructions__/${fileName}`
358
+
359
+ if (existsSync(targetFile)) {
360
+ continue
361
+ }
362
+
363
+ ensureParentDirectory(targetFile)
364
+ if (isVerbose) console.error(` … scaffolding: .agents/instructions/${fileName}`)
365
+ writeFileSync(targetFile, fileContent, 'utf8')
366
+ manifest.files[trackedKey] = hashFile(targetFile)
367
+ scaffolded += 1
368
+ }
369
+
370
+ /* ── Auto-copy opencode.mcp.example.json → opencode.json ────── */
337
371
 
338
372
  const exampleMcpFile = resolve(packageRoot, 'opencode.mcp.example.json')
339
- const targetMcpFile = resolve(consumerRoot, 'opencode.mcp.json')
340
- const mcpTrackedKey = '__root__/opencode.mcp.json'
373
+ const targetMcpFile = resolve(consumerRoot, 'opencode.json')
374
+ const mcpTrackedKey = '__root__/opencode.json'
341
375
 
342
376
  if (existsSync(exampleMcpFile) && !existsSync(targetMcpFile)) {
343
- if (isVerbose) console.error(' … scaffolding: opencode.mcp.json from example')
377
+ if (isVerbose) console.error(' … scaffolding: opencode.json from example')
344
378
  copyFileSync(exampleMcpFile, targetMcpFile)
345
379
  manifest.files[mcpTrackedKey] = hashFile(targetMcpFile)
346
380
  scaffolded += 1