@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,193 @@
1
+ -- ═══════════════════════════════════════════════════════════════════════════
2
+ -- knowledgebase-init.sql — Manual Database Provisioning Script
3
+ -- ═══════════════════════════════════════════════════════════════════════════
4
+ --
5
+ -- Purpose:
6
+ -- Creates the PostgreSQL + pgvector schema for the centralized
7
+ -- cross-project knowledgebase. This is the MANUAL fallback when
8
+ -- auto-provision via knowledgebase-index.js fails due to insufficient
9
+ -- superuser privileges for CREATE EXTENSION vector.
10
+ --
11
+ -- Usage:
12
+ -- psql $DATABASE_URL -f scripts/knowledgebase-init.sql
13
+ --
14
+ -- Auto-provision fallback:
15
+ -- The core engine (knowledgebase-index.js) attempts the same DDL
16
+ -- programmatically via pg.Pool.query(). If it fails on CREATE EXTENSION
17
+ -- (common on managed PostgreSQL where you lack superuser), the error
18
+ -- message directs you to run this script manually.
19
+ --
20
+ -- Requirements:
21
+ -- - PostgreSQL 16+ with pgvector extension installed
22
+ -- - A user with CREATEROLE or superuser for CREATE EXTENSION
23
+ -- - A database already created (e.g. createdb knowledgebase)
24
+ --
25
+ -- Schema:
26
+ -- Two tables with HNSW vector index for cosine similarity search.
27
+ -- See docs/adr-knowledgebase-pgvector.md §2 for full design rationale.
28
+ --
29
+ -- ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ -- ══ 1. Extension ══════════════════════════════════════════════════════════
32
+ -- Requires superuser privileges. On managed PostgreSQL (Supabase, Neon,
33
+ -- Aiven, etc.), pgvector is typically pre-installed. If you see:
34
+ -- ERROR: permission denied to create extension "vector"
35
+ -- ask your database admin to run: CREATE EXTENSION IF NOT EXISTS vector;
36
+
37
+ CREATE EXTENSION IF NOT EXISTS vector;
38
+
39
+ -- ══ 2. Projects table ═════════════════════════════════════════════════════
40
+ -- Each consumer project is registered once. The project_id is derived from
41
+ -- the consumer's package.json "name" field, which is stable across sessions
42
+ -- and unique within an organization.
43
+
44
+ CREATE TABLE IF NOT EXISTS projects (
45
+ id TEXT PRIMARY KEY, -- package.json "name" field (e.g. "my-consumer-app")
46
+ name TEXT NOT NULL, -- human-readable display name
47
+ first_indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
48
+ last_indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
49
+ );
50
+
51
+ -- ══ 3. Knowledge chunks table ════════════════════════════════════════════
52
+ -- Each row represents one session's "New knowledge" bullet set from a
53
+ -- learned-knowledge.instructions.md file. Content is embedded using
54
+ -- all-MiniLM-L6-v2 (384 dimensions) for cosine similarity search.
55
+ --
56
+ -- The uq_knowledge_chunk constraint ensures idempotency: re-indexing the
57
+ -- same project + session + content produces no duplicates.
58
+
59
+ CREATE TABLE IF NOT EXISTS knowledge_chunks (
60
+ id BIGSERIAL PRIMARY KEY,
61
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
62
+ session_date DATE NOT NULL, -- from ## Session: heading
63
+ session_title TEXT, -- full heading text (e.g. "Session: 2026-07-24 — Feature Pipeline")
64
+ pipeline TEXT, -- from **Pipeline:** field
65
+ coverage TEXT, -- from **Coverage:** field
66
+ tdd_iterations INTEGER NOT NULL DEFAULT 0, -- from **TDD Iterations:** field
67
+ content TEXT NOT NULL, -- the "New knowledge" bullet text
68
+ content_hash TEXT NOT NULL, -- SHA-256(content + session_date + pipeline)
69
+ embedding VECTOR(384), -- from all-MiniLM-L6-v2 (384d)
70
+ indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
71
+
72
+ -- Idempotency: same project + session date + content hash → skip silently
73
+ CONSTRAINT uq_knowledge_chunk UNIQUE (project_id, session_date, content_hash)
74
+ );
75
+
76
+ -- ══ 4. Indexes ═══════════════════════════════════════════════════════════
77
+
78
+ -- ── 4a. HNSW index for approximate nearest neighbor (ANN) search ─────────
79
+ -- Enables fast cosine similarity lookups (~1ms for 10k vectors).
80
+ -- Uses vector_cosine_ops operator class for the <=> (cosine distance) operator.
81
+ -- Parameters:
82
+ -- m = 16 — max connections per element (higher = more accurate but slower build)
83
+ -- ef_construction = 64 — dynamic candidate list size during build
84
+
85
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_embedding
86
+ ON knowledge_chunks
87
+ USING hnsw (embedding vector_cosine_ops)
88
+ WITH (m = 16, ef_construction = 64);
89
+
90
+ -- ── 4b. B-tree indexes for structured queries and filters ────────────────
91
+
92
+ -- Filter knowledge by project (used by knowledgebase_search tool)
93
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_project_id
94
+ ON knowledge_chunks (project_id);
95
+
96
+ -- Temporal queries: "show me knowledge from the last month"
97
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_session_date
98
+ ON knowledge_chunks (session_date DESC);
99
+
100
+ -- Filter by pipeline type (e.g. "TDD Infrastructure Bootstrap", "Feature Pipeline")
101
+ CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_pipeline
102
+ ON knowledge_chunks (pipeline);
103
+
104
+ -- Recently indexed projects (used by knowledgebase_list tool)
105
+ CREATE INDEX IF NOT EXISTS idx_projects_last_indexed
106
+ ON projects (last_indexed_at DESC);
107
+
108
+ -- ══ 5. Match function ════════════════════════════════════════════════════
109
+ -- Convenience function for cosine similarity search with optional
110
+ -- project_id filtering. Returns results ordered by similarity descending.
111
+ --
112
+ -- Usage examples:
113
+ --
114
+ -- -- Find top 5 chunks similar to a query embedding
115
+ -- SELECT * FROM match_knowledge('[0.01, 0.02, ...]'::vector(384), 0.7, 5);
116
+ --
117
+ -- -- Filter to a specific project
118
+ -- SELECT * FROM match_knowledge('[0.01, 0.02, ...]'::vector(384), 0.7, 5, 'my-app');
119
+ --
120
+ -- -- Use with knowledgebase-index.js generated embeddings
121
+ -- -- (the core engine builds embedding vector from query text)
122
+
123
+ CREATE OR REPLACE FUNCTION match_knowledge(
124
+ query_embedding VECTOR(384),
125
+ match_threshold DOUBLE PRECISION, -- minimum similarity (0.0 to 1.0); 0.7 is a good default
126
+ match_count INTEGER, -- max results to return (1-20 recommended)
127
+ filter_project_id TEXT DEFAULT NULL -- optional: restrict to a single project
128
+ )
129
+ RETURNS TABLE(
130
+ id BIGINT,
131
+ project_id TEXT,
132
+ session_date DATE,
133
+ session_title TEXT,
134
+ pipeline TEXT,
135
+ coverage TEXT,
136
+ tdd_iterations INTEGER,
137
+ content TEXT,
138
+ content_hash TEXT,
139
+ similarity DOUBLE PRECISION,
140
+ indexed_at TIMESTAMPTZ
141
+ )
142
+ LANGUAGE SQL
143
+ STABLE
144
+ AS $$
145
+ SELECT
146
+ id,
147
+ project_id,
148
+ session_date,
149
+ session_title,
150
+ pipeline,
151
+ coverage,
152
+ tdd_iterations,
153
+ content,
154
+ content_hash,
155
+ 1 - (embedding <=> query_embedding) AS similarity,
156
+ indexed_at
157
+ FROM knowledge_chunks
158
+ WHERE 1 - (embedding <=> query_embedding) > match_threshold
159
+ AND (filter_project_id IS NULL OR project_id = filter_project_id)
160
+ ORDER BY embedding <=> query_embedding
161
+ LIMIT match_count;
162
+ $$;
163
+
164
+ -- ══ 6. OpenAI upgrade path ═══════════════════════════════════════════════
165
+ -- The default embedding model is Xenova/all-MiniLM-L6-v2 (384 dimensions).
166
+ -- To upgrade to OpenAI text-embedding-3-small (1536 dimensions):
167
+ --
168
+ -- 1. Set OPENAI_API_KEY in your .env file
169
+ -- 2. Resize the embedding column (requires dropping and recreating the
170
+ -- HNSW index first — HNSW indexes cannot be built on expression columns)
171
+ -- 3. Rebuild the HNSW index for the new dimension
172
+ --
173
+ -- -- Step 1: Drop the old HNSW index (required before ALTER COLUMN TYPE)
174
+ -- DROP INDEX IF EXISTS idx_knowledge_chunks_embedding;
175
+ --
176
+ -- -- Step 2: Change column type to 1536 dimensions
177
+ -- ALTER TABLE knowledge_chunks ALTER COLUMN embedding TYPE VECTOR(1536);
178
+ --
179
+ -- -- Step 3: Rebuild the HNSW index
180
+ -- CREATE INDEX idx_knowledge_chunks_embedding
181
+ -- ON knowledge_chunks
182
+ -- USING hnsw (embedding vector_cosine_ops)
183
+ -- WITH (m = 16, ef_construction = 64);
184
+ --
185
+ -- ═══════════════════════════════════════════════════════════════════════════
186
+
187
+ -- ══ Done ═════════════════════════════════════════════════════════════════
188
+ -- Verify installation by running:
189
+ -- \dt knowledge_chunks projects
190
+ -- \di idx_*
191
+ -- \df match_knowledge
192
+ -- SELECT * FROM match_knowledge('[0,0,0,0]'::vector(384), 0.0, 1);
193
+ -- (the last query should return 0 rows — confirming the function works)
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mcp-knowledgebase-server.js — MCP server exposing knowledgebase tools to AI agents.
5
+ *
6
+ * Provides: knowledgebase_search, knowledgebase_index, knowledgebase_stats, knowledgebase_list
7
+ * Uses stdio transport — OpenCode spawns this as a child process.
8
+ *
9
+ * Configuration (opencode.json):
10
+ * "knowledgebase": {
11
+ * "type": "local",
12
+ * "command": ["node", "scripts/mcp-knowledgebase-server.js"],
13
+ * "enabled": true
14
+ * }
15
+ */
16
+
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import {
20
+ CallToolRequestSchema,
21
+ ListToolsRequestSchema,
22
+ } from '@modelcontextprotocol/sdk/types.js';
23
+ import {
24
+ search,
25
+ upsertChunks,
26
+ chunkLearnedKnowledge,
27
+ getStats,
28
+ listProjects,
29
+ getPool,
30
+ closePool,
31
+ } from './knowledgebase-index.js';
32
+ import { readFileSync } from 'node:fs';
33
+ import { resolve } from 'node:path';
34
+
35
+ const server = new Server(
36
+ {
37
+ name: 'knowledgebase',
38
+ version: '1.0.0',
39
+ },
40
+ {
41
+ capabilities: {
42
+ tools: {},
43
+ },
44
+ }
45
+ );
46
+
47
+ // ── List tools ───────────────────────────────────────────────────────
48
+
49
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
50
+ tools: [
51
+ {
52
+ name: 'knowledgebase_search',
53
+ description:
54
+ 'Semantic search across learned knowledge from all indexed projects. ' +
55
+ 'Use this when planning tasks, encountering errors, or seeking patterns from past pipeline sessions.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ query: {
60
+ type: 'string',
61
+ description:
62
+ 'Natural language query about patterns, conventions, gotchas, or agent behavior ' +
63
+ '(e.g. "how to handle verbose logging in child processes")',
64
+ },
65
+ projectId: {
66
+ type: 'string',
67
+ description: 'Optional: filter to a specific project by its package.json name',
68
+ },
69
+ threshold: {
70
+ type: 'number',
71
+ default: 0.6,
72
+ description: 'Minimum similarity threshold (0.0–1.0). Higher values return fewer but more relevant results.',
73
+ },
74
+ limit: {
75
+ type: 'number',
76
+ default: 5,
77
+ description: 'Number of results to return (1–50)',
78
+ },
79
+ },
80
+ required: ['query'],
81
+ },
82
+ },
83
+ {
84
+ name: 'knowledgebase_index',
85
+ description:
86
+ 'Index or re-index a project\'s learned knowledge content. ' +
87
+ 'Use after adding new sessions to learned-knowledge.instructions.md. ' +
88
+ 'Idempotent — unchanged sessions are skipped.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ projectId: {
93
+ type: 'string',
94
+ description: 'Project identifier (from package.json "name")',
95
+ },
96
+ content: {
97
+ type: 'string',
98
+ description:
99
+ 'Optional: markdown content of learned-knowledge.instructions.md to index. ' +
100
+ 'If omitted, reads from .agents/instructions/learned-knowledge.instructions.md',
101
+ },
102
+ },
103
+ required: ['projectId'],
104
+ },
105
+ },
106
+ {
107
+ name: 'knowledgebase_stats',
108
+ description:
109
+ 'Get statistics about the centralized knowledgebase, ' +
110
+ 'including total projects, chunks, database size, and last sync timestamp.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {},
114
+ },
115
+ },
116
+ {
117
+ name: 'knowledgebase_list',
118
+ description:
119
+ 'List all projects currently indexed in the knowledgebase. ' +
120
+ 'Useful for discovering which projects have shared knowledge.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {},
124
+ },
125
+ },
126
+ ],
127
+ }));
128
+
129
+ // ── Handle tool calls ────────────────────────────────────────────────
130
+
131
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
132
+ const { name, arguments: args } = request.params;
133
+
134
+ try {
135
+ switch (name) {
136
+ case 'knowledgebase_search': {
137
+ const results = await search(args.query, {
138
+ project_id: args.projectId || undefined,
139
+ threshold: args.threshold || 0.6,
140
+ limit: args.limit || 5,
141
+ });
142
+
143
+ if (results.length === 0) {
144
+ const pool = await getPool();
145
+ if (!pool) {
146
+ return {
147
+ content: [{
148
+ type: 'text',
149
+ text: 'Knowledgebase not available: DATABASE_URL is not configured. ' +
150
+ 'Set DATABASE_URL in your .env file to enable cross-project knowledge search.',
151
+ }],
152
+ };
153
+ }
154
+ return {
155
+ content: [{
156
+ type: 'text',
157
+ text: 'No results found. The knowledgebase may be empty.',
158
+ }],
159
+ };
160
+ }
161
+
162
+ const formatted = results
163
+ .map((r, i) =>
164
+ `### Result ${i + 1} (similarity: ${r.similarity})\n` +
165
+ `**Project:** \`${r.project_id}\`\n` +
166
+ `**Date:** ${r.session_date}\n` +
167
+ `**Pipeline:** ${r.pipeline || 'N/A'}\n\n` +
168
+ `${r.content}`
169
+ )
170
+ .join('\n\n---\n\n');
171
+
172
+ return {
173
+ content: [{ type: 'text', text: formatted }],
174
+ };
175
+ }
176
+
177
+ case 'knowledgebase_index': {
178
+ const projectId = args.projectId;
179
+ if (!projectId) {
180
+ return {
181
+ content: [{
182
+ type: 'text',
183
+ text: 'Error: projectId is required for knowledgebase_index.',
184
+ }],
185
+ isError: true,
186
+ };
187
+ }
188
+
189
+ let content = args.content;
190
+ if (!content) {
191
+ try {
192
+ content = readFileSync(
193
+ resolve(
194
+ process.cwd(),
195
+ '.agents/instructions/learned-knowledge.instructions.md'
196
+ ),
197
+ 'utf8'
198
+ );
199
+ } catch {
200
+ return {
201
+ content: [{
202
+ type: 'text',
203
+ text: 'No content provided and the default file ' +
204
+ '.agents/instructions/learned-knowledge.instructions.md ' +
205
+ 'could not be read. Provide the "content" parameter or ensure the file exists.',
206
+ }],
207
+ isError: true,
208
+ };
209
+ }
210
+ }
211
+
212
+ const chunks = chunkLearnedKnowledge(content, projectId);
213
+ if (chunks.length === 0) {
214
+ return {
215
+ content: [{
216
+ type: 'text',
217
+ text: `No valid knowledge chunks found for project "${projectId}". ` +
218
+ 'Ensure the content contains "## Session:" headers with ' +
219
+ '"**New knowledge:**" sections.',
220
+ }],
221
+ };
222
+ }
223
+
224
+ const result = await upsertChunks(chunks);
225
+ return {
226
+ content: [{
227
+ type: 'text',
228
+ text:
229
+ `Indexed ${result.inserted} chunks, updated ${result.updated}, ` +
230
+ `skipped ${result.skipped} for project "${projectId}".`,
231
+ }],
232
+ };
233
+ }
234
+
235
+ case 'knowledgebase_stats': {
236
+ const stats = await getStats();
237
+
238
+ if (stats.total_chunks === 0 && stats.total_projects === 0) {
239
+ const pool = await getPool();
240
+ if (!pool) {
241
+ return {
242
+ content: [{
243
+ type: 'text',
244
+ text: 'Knowledgebase not available: DATABASE_URL is not configured.',
245
+ }],
246
+ };
247
+ }
248
+ }
249
+
250
+ return {
251
+ content: [{
252
+ type: 'text',
253
+ text:
254
+ `**Knowledgebase Stats**\n` +
255
+ `- Total projects: ${stats.total_projects}\n` +
256
+ `- Total chunks: ${stats.total_chunks}\n` +
257
+ `- Database size: ${stats.db_size}\n` +
258
+ `- Last sync: ${stats.last_sync || 'Never'}`,
259
+ }],
260
+ };
261
+ }
262
+
263
+ case 'knowledgebase_list': {
264
+ const projects = await listProjects();
265
+
266
+ if (projects.length === 0) {
267
+ const pool = await getPool();
268
+ if (!pool) {
269
+ return {
270
+ content: [{
271
+ type: 'text',
272
+ text: 'Knowledgebase not available: DATABASE_URL is not configured.',
273
+ }],
274
+ };
275
+ }
276
+ return {
277
+ content: [{
278
+ type: 'text',
279
+ text: 'No projects indexed yet.',
280
+ }],
281
+ };
282
+ }
283
+
284
+ const formatted = projects
285
+ .map((p) =>
286
+ `- ${p.name || p.project_id} (${p.chunk_count} chunks, ` +
287
+ `last indexed: ${p.last_indexed || 'Never'})`
288
+ )
289
+ .join('\n');
290
+
291
+ return {
292
+ content: [{
293
+ type: 'text',
294
+ text: `**Indexed Projects:**\n${formatted}`,
295
+ }],
296
+ };
297
+ }
298
+
299
+ default:
300
+ throw new Error(`Unknown tool: ${name}`);
301
+ }
302
+ } catch (err) {
303
+ return {
304
+ content: [{
305
+ type: 'text',
306
+ text: `Error: ${err.message}`,
307
+ }],
308
+ isError: true,
309
+ };
310
+ }
311
+ });
312
+
313
+ // ── Start server ─────────────────────────────────────────────────────
314
+
315
+ async function main() {
316
+ const transport = new StdioServerTransport();
317
+ await server.connect(transport);
318
+ console.error('[knowledgebase-mcp] Server started via stdio transport');
319
+ }
320
+
321
+ process.on('SIGTERM', () => {
322
+ console.error('[knowledgebase-mcp] SIGTERM received, closing pool');
323
+ closePool().catch(() => {});
324
+ process.exit(0);
325
+ });
326
+
327
+ main().catch((err) => {
328
+ console.error('[knowledgebase-mcp] Fatal error:', err);
329
+ process.exit(1);
330
+ });
@@ -41,8 +41,14 @@ export const HOOK_MERGE_SEPARATOR = '# --- @abarcenas/ai-workflow-template ---'
41
41
  export const TEMPLATE_HOOKS = {
42
42
  'pre-commit': {
43
43
  source: '.husky/pre-commit',
44
- content:
45
- '# Managed by @abarcenas/ai-workflow-template setup\n\nnode scripts/bump-version.js\nnode scripts/validate-memory-schema.js\n',
44
+ content: [
45
+ '# Managed by @abarcenas/ai-workflow-template setup',
46
+ '#!/bin/sh',
47
+ '',
48
+ 'node scripts/bump-version.js',
49
+ 'node scripts/validate-memory-schema.js',
50
+ '',
51
+ ].join('\n'),
46
52
  description:
47
53
  'Auto-bump version + validate memory-bank schema before commits',
48
54
  },
@@ -51,7 +57,6 @@ export const TEMPLATE_HOOKS = {
51
57
  content: [
52
58
  '# Managed by @abarcenas/ai-workflow-template setup',
53
59
  '#!/bin/sh',
54
- '. "$(dirname "$0")/_/husky.sh"',
55
60
  '',
56
61
  '# After git pull/merge, check if memory-bank files changed',
57
62
  '# and update the vector index incrementally',
@@ -65,6 +70,26 @@ export const TEMPLATE_HOOKS = {
65
70
  description:
66
71
  'Auto-update memory bank vector index after pull/merge',
67
72
  },
73
+ 'post-commit': {
74
+ source: '.husky/post-commit',
75
+ content: [
76
+ '# Managed by @abarcenas/ai-workflow-template setup',
77
+ '#!/bin/sh',
78
+ '',
79
+ '# After commit, check if learned knowledge file changed',
80
+ '# and sync to the centralized knowledgebase',
81
+ '',
82
+ 'CHANGED=$(git diff HEAD~1 --name-only 2>/dev/null | grep ".agents/instructions/learned-knowledge.instructions.md")',
83
+ '',
84
+ 'if [ -n "$CHANGED" ]; then',
85
+ ' echo "[knowledgebase] Learned knowledge file changed. Syncing to knowledgebase..."',
86
+ ' node scripts/knowledgebase-cli.js sync 2>/dev/null || echo "[knowledgebase] Sync skipped (DATABASE_URL not configured or service unavailable)"',
87
+ 'fi',
88
+ '',
89
+ ].join('\n'),
90
+ description:
91
+ 'Auto-sync learned knowledge to centralized knowledgebase after commits',
92
+ },
68
93
  }
69
94
 
70
95
  // ── CI/CD environment detection ───────────────────────────────────────────────
@@ -113,6 +138,7 @@ export const SUPPORTED_FLAGS = {
113
138
  '--skip-hooks': 'skipHooks',
114
139
  '--skip-prepare': 'skipPrepare',
115
140
  '--skip-sync': 'skipSync',
141
+ '--skip-knowledgebase': 'skipKnowledgebase',
116
142
  '--help': 'help',
117
143
  '-h': 'help',
118
144
  '--version': 'version',
@@ -222,5 +248,12 @@ export const MEMORY_BANK_STUBS = {
222
248
  - **To Build**: {{what remains}}
223
249
  - **Status**: {{overall project status}}
224
250
  - **Known Issues**: {{list known issues}}
251
+ `,
252
+ 'learned-knowledge.instructions.md': `# Learned Knowledge
253
+
254
+ Cross-session knowledge accumulated by agent pipelines. Each session records discoveries, patterns, gotchas, and agent tuning notes for future reference.
255
+
256
+ ---
257
+
225
258
  `,
226
259
  }
@@ -52,8 +52,8 @@ function buildHookContent(hookName) {
52
52
  const template = TEMPLATE_HOOKS[hookName]
53
53
  // The `content` field is always a string in our constants (pre-composed).
54
54
  const raw = template.content
55
- // Marker is first line, then content.
56
- return HOOK_MARKER + '\n' + raw
55
+ // Template content already has the marker on its first line.
56
+ return raw
57
57
  }
58
58
 
59
59
  // ── Hook file path helper ────────────────────────────────────────────────────
@@ -66,15 +66,16 @@ function cleanupTempDirs() {
66
66
  }
67
67
 
68
68
  /**
69
- * Replicates the internal `buildHookContent()` logic so we can assert on
70
- * expected file content without exporting a private function.
69
+ * Returns the expected hook file content as produced by `buildHookContent()`.
71
70
  *
72
- * @param {string} hookName - Key from `TEMPLATE_HOOKS` ('pre-commit' | 'post-merge')
71
+ * The template content in `TEMPLATE_HOOKS` already includes `HOOK_MARKER`
72
+ * as its first line, so this just returns the raw content directly.
73
+ *
74
+ * @param {string} hookName - Key from `TEMPLATE_HOOKS` ('pre-commit' | 'post-merge' | 'post-commit')
73
75
  * @returns {string} The full hook file content as written by hooks.js.
74
76
  */
75
77
  function expectedContent(hookName) {
76
- const raw = TEMPLATE_HOOKS[hookName].content
77
- return HOOK_MARKER + '\n' + raw
78
+ return TEMPLATE_HOOKS[hookName].content
78
79
  }
79
80
 
80
81
  /**
@@ -169,6 +170,7 @@ describe('installHooks', () => {
169
170
  existingHooks: {
170
171
  'pre-commit': { exists: true, isManaged: true },
171
172
  'post-merge': { exists: true, isManaged: true },
173
+ 'post-commit': { exists: true, isManaged: true },
172
174
  },
173
175
  })
174
176
  const huskyDir = join(ctx.consumerRoot, '.husky')
@@ -203,6 +205,7 @@ describe('installHooks', () => {
203
205
  existingHooks: {
204
206
  'pre-commit': { exists: true, isManaged: false },
205
207
  'post-merge': { exists: true, isManaged: false },
208
+ 'post-commit': { exists: true, isManaged: false },
206
209
  },
207
210
  })
208
211
  const huskyDir = join(ctx.consumerRoot, '.husky')
@@ -240,6 +243,7 @@ describe('installHooks', () => {
240
243
  existingHooks: {
241
244
  'pre-commit': { exists: true, isManaged: false },
242
245
  'post-merge': { exists: true, isManaged: false },
246
+ 'post-commit': { exists: true, isManaged: false },
243
247
  },
244
248
  })
245
249
  const huskyDir = join(ctx.consumerRoot, '.husky')
@@ -400,12 +404,14 @@ describe('installHooks', () => {
400
404
  existingHooks: {
401
405
  'pre-commit': { exists: true, isManaged: false },
402
406
  'post-merge': { exists: true, isManaged: false },
407
+ 'post-commit': { exists: true, isManaged: false },
403
408
  },
404
409
  })
405
410
  const huskyDir = join(ctx.consumerRoot, '.husky')
406
411
  mkdirSync(huskyDir)
407
412
  writeFileSync(join(huskyDir, 'pre-commit'), '# orig\n')
408
413
  writeFileSync(join(huskyDir, 'post-merge'), '# orig\n')
414
+ writeFileSync(join(huskyDir, 'post-commit'), '# orig\n')
409
415
 
410
416
  const results = await installHooks(ctx)
411
417
 
@@ -414,6 +420,7 @@ describe('installHooks', () => {
414
420
  }
415
421
  expect(existsSync(join(huskyDir, 'pre-commit.bak'))).toBe(true)
416
422
  expect(existsSync(join(huskyDir, 'post-merge.bak'))).toBe(true)
423
+ expect(existsSync(join(huskyDir, 'post-commit.bak'))).toBe(true)
417
424
  })
418
425
 
419
426
  // ── 12. Partial existingHooks map ─────────────────────────────────────
@@ -424,6 +431,7 @@ describe('installHooks', () => {
424
431
  existingHooks: {
425
432
  'pre-commit': { exists: true, isManaged: true },
426
433
  // post-merge absent → treated as exists=false, isManaged=false
434
+ // post-commit absent → treated as exists=false, isManaged=false
427
435
  },
428
436
  })
429
437
  const huskyDir = join(ctx.consumerRoot, '.husky')