@abarcenas/ai-workflow-template 1.23.0 → 1.26.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 (33) hide show
  1. package/.agents/instructions/agent-skill.instructions.md +18 -20
  2. package/.agents/instructions/agent.instructions.md +111 -570
  3. package/.agents/instructions/ai-prompt-engineering-safety.instructions.md +3 -3
  4. package/.agents/instructions/instructions.instructions.md +6 -7
  5. package/.agents/instructions/learned-knowledge.instructions.md +17 -41
  6. package/.agents/instructions/memory-bank.instructions.md +125 -17
  7. package/.agents/instructions/memory-schema.instructions.md +75 -0
  8. package/.agents/instructions/prompt.instructions.md +25 -30
  9. package/.agents/instructions/task-implementation.instructions.md +12 -5
  10. package/.agents/skills/architecture-blueprint-generator/SKILL.md +63 -18
  11. package/.agents/skills/context-map/SKILL.md +27 -4
  12. package/.agents/skills/conventional-commit/SKILL.md +2 -2
  13. package/.agents/skills/github-issues/SKILL.md +2 -2
  14. package/.agents/skills/graphify-framework-aware/SKILL.md +133 -0
  15. package/.agents/skills/graphify-framework-aware/references/framework-registry.md +95 -0
  16. package/.agents/skills/graphify-framework-aware/scripts/fw-exclude.py +425 -0
  17. package/.agents/skills/memory-search/SKILL.md +69 -0
  18. package/.agents/skills/microsoft-docs/SKILL.md +1 -1
  19. package/.agents/skills/polygot-test-agent/SKILL.md +0 -1
  20. package/.agents/skills/prompt-builder/SKILL.md +15 -14
  21. package/.husky/post-merge +10 -0
  22. package/.husky/pre-commit +2 -0
  23. package/AGENTS.md +54 -0
  24. package/README.md +177 -3
  25. package/memory-bank/.vocabulary.json +52 -0
  26. package/opencode.mcp.example.json +8 -1
  27. package/package.json +25 -5
  28. package/scripts/mcp-memory-server.js +243 -0
  29. package/scripts/memory-cli.js +110 -0
  30. package/scripts/memory-index.js +512 -0
  31. package/scripts/normalize-memory.js +349 -0
  32. package/scripts/sync.test.js +12 -0
  33. package/scripts/validate-memory-schema.js +190 -0
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * normalize-memory.js — Onboarding script for memory-bank vectorization.
5
+ *
6
+ * Performs one-time normalization of existing documentation:
7
+ * 1. Adds YAML frontmatter to memory-bank/*.md and docs/*.md files missing it
8
+ * 2. Auto-suggests tags and entities from .vocabulary.json
9
+ * 3. Migrates plan/*.md to docs/ (if plan/ directory has content)
10
+ * 4. Validates heading structure (memory-bank files)
11
+ *
12
+ * Idempotent — safe to run multiple times. Files already normalized are skipped.
13
+ *
14
+ * Usage:
15
+ * node scripts/normalize-memory.js # Normalize (creates .normalized marker)
16
+ * node scripts/normalize-memory.js --force # Re-normalize all files
17
+ * node scripts/normalize-memory.js --dry-run # Show what would change
18
+ */
19
+
20
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
21
+ import { basename, dirname, extname, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const packageRoot = resolve(__dirname, '..');
26
+ const consumerRoot = process.env.INIT_CWD ? resolve(process.env.INIT_CWD) : process.cwd();
27
+
28
+ const MEMORY_BANK_DIR = resolve(consumerRoot, 'memory-bank');
29
+ const DOCS_DIR = resolve(consumerRoot, 'docs');
30
+ const PLAN_DIR = resolve(consumerRoot, 'plan');
31
+ const VOCAB_PATH = resolve(MEMORY_BANK_DIR, '.vocabulary.json');
32
+ const MARKER_FILE = resolve(MEMORY_BANK_DIR, '.normalized');
33
+ const SOURCE_VOCAB_PATH = resolve(packageRoot, 'memory-bank', '.vocabulary.json');
34
+
35
+ const args = new Set(process.argv.slice(2));
36
+ const force = args.has('--force');
37
+ const dryRun = args.has('--dry-run');
38
+
39
+ // ── Load vocabulary ──────────────────────────────────────────────────
40
+
41
+ function loadVocabulary() {
42
+ const vocabPath = existsSync(VOCAB_PATH) ? VOCAB_PATH : SOURCE_VOCAB_PATH;
43
+ if (!existsSync(vocabPath)) {
44
+ console.warn('[normalize-memory] No .vocabulary.json found. Tags will not be auto-suggested.');
45
+ return null;
46
+ }
47
+ return JSON.parse(readFileSync(vocabPath, 'utf8'));
48
+ }
49
+
50
+ function getAllTags(vocab) {
51
+ if (!vocab || !vocab.tags) return [];
52
+ return Object.values(vocab.tags).flat();
53
+ }
54
+
55
+ // ── Parse existing frontmatter ───────────────────────────────────────
56
+
57
+ function parseFrontmatter(content) {
58
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
59
+ if (!match) return { frontmatter: null, body: content };
60
+
61
+ const raw = match[1];
62
+ const fm = {};
63
+ for (const line of raw.split('\n')) {
64
+ const kv = line.match(/^(\w[\w-]*):\s*(.*)/);
65
+ if (kv) {
66
+ fm[kv[1]] = kv[2].trim();
67
+ }
68
+ }
69
+ return { frontmatter: fm, body: content.slice(match[0].length) };
70
+ }
71
+
72
+ // ── Extract H1 title ─────────────────────────────────────────────────
73
+
74
+ function extractTitle(content) {
75
+ const match = content.match(/^# (.+)$/m);
76
+ return match ? match[1].trim() : basename(content, extname(content));
77
+ }
78
+
79
+ // ── Auto-suggest tags ────────────────────────────────────────────────
80
+
81
+ function suggestTags(body, allTags) {
82
+ const lowerBody = body.toLowerCase();
83
+ return allTags.filter(tag => lowerBody.includes(tag.toLowerCase()));
84
+ }
85
+
86
+ function suggestEntities(body, vocab) {
87
+ if (!vocab || !vocab.entity_patterns) return [];
88
+
89
+ const entities = [];
90
+ for (const [entity, patterns] of Object.entries(vocab.entity_patterns)) {
91
+ for (const pattern of patterns) {
92
+ try {
93
+ if (new RegExp(pattern, 'i').test(body)) {
94
+ entities.push(entity);
95
+ break;
96
+ }
97
+ } catch { /* skip invalid regex */ }
98
+ }
99
+ }
100
+ return entities;
101
+ }
102
+
103
+ function inferCategory(fileName, vocab) {
104
+ if (!vocab || !vocab.file_category_map) return 'context';
105
+
106
+ const baseId = basename(fileName, extname(fileName)).replace(/^\./, '');
107
+ if (vocab.file_category_map[baseId]) return vocab.file_category_map[baseId];
108
+
109
+ if (fileName.includes('/tasks/')) return 'task';
110
+ if (fileName.startsWith('docs/') || fileName.includes('/docs/')) return 'decision';
111
+
112
+ return 'context';
113
+ }
114
+
115
+ function getGitMtime(filePath) {
116
+ try {
117
+ // Fallback to filesystem mtime if git not available
118
+ const stats = statSync(filePath);
119
+ const d = stats.mtime;
120
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
121
+ } catch {
122
+ return new Date().toISOString().slice(0, 10);
123
+ }
124
+ }
125
+
126
+ // ── Build frontmatter block ──────────────────────────────────────────
127
+
128
+ function buildFrontmatterYaml(fm) {
129
+ const lines = ['---'];
130
+ for (const [key, value] of Object.entries(fm)) {
131
+ if (Array.isArray(value)) {
132
+ lines.push(`${key}: [${value.join(', ')}]`);
133
+ } else {
134
+ lines.push(`${key}: "${value}"`);
135
+ }
136
+ }
137
+ lines.push('---');
138
+ return lines.join('\n') + '\n';
139
+ }
140
+
141
+ // ── Normalize a single file ──────────────────────────────────────────
142
+
143
+ function normalizeFile(filePath, vocab, allTags, isMemoryBank) {
144
+ const content = readFileSync(filePath, 'utf8');
145
+ const { frontmatter, body } = parseFrontmatter(content);
146
+
147
+ if (frontmatter && !force) {
148
+ // Already has frontmatter — skip unless forced
149
+ return { file: filePath, action: 'skipped', reason: 'already normalized' };
150
+ }
151
+
152
+ const fileName = basename(filePath);
153
+ const fileId = basename(filePath, extname(filePath));
154
+ const title = extractTitle(body);
155
+ const date = getGitMtime(filePath);
156
+ const tags = suggestTags(body, allTags);
157
+ const entities = suggestEntities(body, vocab);
158
+ const category = isMemoryBank ? inferCategory(fileName, vocab) : undefined;
159
+
160
+ const newFm = {
161
+ id: fileId,
162
+ title,
163
+ updated: date,
164
+ tags: tags.length > 0 ? tags : [],
165
+ };
166
+
167
+ if (isMemoryBank) {
168
+ newFm.entities = entities;
169
+ newFm.category = category;
170
+ } else {
171
+ // docs/ files get doc_type instead
172
+ newFm.doc_type = inferDocType(fileId);
173
+ }
174
+
175
+ const newContent = buildFrontmatterYaml(newFm) + '\n' + body;
176
+
177
+ if (!dryRun) {
178
+ writeFileSync(filePath, newContent, 'utf8');
179
+ }
180
+
181
+ return {
182
+ file: filePath,
183
+ action: dryRun ? 'would-normalize' : 'normalized',
184
+ tags: newFm.tags,
185
+ entities: newFm.entities || [],
186
+ category: newFm.category || newFm.doc_type || 'n/a',
187
+ };
188
+ }
189
+
190
+ function inferDocType(fileId) {
191
+ const map = {
192
+ 'tracker-log': 'tracker-log',
193
+ 'orchestrator-log': 'orchestrator-log',
194
+ 'architecture-context': 'architecture',
195
+ 'playwright-mcp-configuration': 'configuration',
196
+ };
197
+ if (map[fileId.replace(/^\./, '')]) return map[fileId.replace(/^\./, '')];
198
+ if (fileId.includes('spike')) return 'spike';
199
+ return 'decision';
200
+ }
201
+
202
+ // ── Collect files ────────────────────────────────────────────────────
203
+
204
+ function collectMdFiles(dir, skipDir = null) {
205
+ if (!existsSync(dir)) return [];
206
+ const files = [];
207
+ const entries = readdirSync(dir, { withFileTypes: true });
208
+ for (const entry of entries) {
209
+ const fullPath = join(dir, entry.name);
210
+ if (entry.isDirectory()) {
211
+ if (entry.name === skipDir) continue;
212
+ if (entry.name.startsWith('.') && entry.name !== '.index') continue; // skip hidden dirs except .index
213
+ files.push(...collectMdFiles(fullPath, skipDir));
214
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
215
+ files.push(fullPath);
216
+ }
217
+ }
218
+ return files;
219
+ }
220
+
221
+ // ── Migrate plan/ to docs/ ───────────────────────────────────────────
222
+
223
+ function migratePlanToDocs(vocab, allTags) {
224
+ if (!existsSync(PLAN_DIR)) return [];
225
+
226
+ const planFiles = collectMdFiles(PLAN_DIR);
227
+ if (planFiles.length === 0) return [];
228
+
229
+ ensureDir(DOCS_DIR);
230
+ const results = [];
231
+
232
+ for (const planFile of planFiles) {
233
+ const fileName = basename(planFile);
234
+ let destPath = join(DOCS_DIR, fileName);
235
+
236
+ // Avoid overwriting existing docs files
237
+ if (existsSync(destPath)) {
238
+ const base = basename(fileName, extname(fileName));
239
+ const ext = extname(fileName);
240
+ destPath = join(DOCS_DIR, `${base}.migrated${ext}`);
241
+ }
242
+
243
+ const content = readFileSync(planFile, 'utf8');
244
+ const { frontmatter, body } = parseFrontmatter(content);
245
+ const title = extractTitle(body);
246
+ const date = getGitMtime(planFile);
247
+ const tags = suggestTags(body, allTags);
248
+
249
+ const fm = {
250
+ id: basename(destPath, extname(destPath)),
251
+ title,
252
+ updated: date,
253
+ tags: tags.length > 0 ? tags : ['migration'],
254
+ doc_type: 'decision',
255
+ };
256
+
257
+ if (frontmatter) {
258
+ // Preserve existing frontmatter but add note
259
+ fm.note = `Migrated from plan/ directory during normalization on ${date}`;
260
+ }
261
+
262
+ const newContent = buildFrontmatterYaml(fm) + '\n' + body;
263
+
264
+ if (!dryRun) {
265
+ writeFileSync(destPath, newContent, 'utf8');
266
+ try { renameSync(planFile, planFile + '.migrated'); } catch { /* ok */ }
267
+ }
268
+
269
+ results.push({
270
+ from: planFile,
271
+ to: destPath,
272
+ action: dryRun ? 'would-migrate' : 'migrated',
273
+ });
274
+ }
275
+
276
+ // Clean up empty plan/ directory
277
+ if (!dryRun) {
278
+ try {
279
+ const remaining = readdirSync(PLAN_DIR).filter(f => !f.endsWith('.migrated'));
280
+ if (remaining.length === 0) {
281
+ rmdirSync(PLAN_DIR);
282
+ console.log('[normalize-memory] Removed empty plan/ directory');
283
+ }
284
+ } catch { /* ignore */ }
285
+ }
286
+
287
+ return results;
288
+ }
289
+
290
+ // ── Main ─────────────────────────────────────────────────────────────
291
+
292
+ function ensureDir(dir) {
293
+ if (!dryRun && !existsSync(dir)) mkdirSync(dir, { recursive: true });
294
+ }
295
+
296
+ function main() {
297
+ // Check if already normalized
298
+ if (existsSync(MARKER_FILE) && !force && !dryRun) {
299
+ console.log('[normalize-memory] Already normalized. Use --force to re-run.');
300
+ process.exit(0);
301
+ }
302
+
303
+ const vocab = loadVocabulary();
304
+ const allTags = getAllTags(vocab);
305
+
306
+ console.log(`[normalize-memory] Mode: ${dryRun ? 'dry-run' : force ? 'force' : 'normal'}`);
307
+
308
+ // Migrate plan/ to docs/
309
+ const migrations = migratePlanToDocs(vocab, allTags);
310
+ if (migrations.length > 0) {
311
+ console.log(`[normalize-memory] Migrated ${migrations.length} files from plan/ to docs/`);
312
+ for (const m of migrations) {
313
+ console.log(` ${m.action}: ${m.from} → ${m.to}`);
314
+ }
315
+ }
316
+
317
+ // Normalize memory-bank files
318
+ const memoryFiles = collectMdFiles(MEMORY_BANK_DIR, '.index');
319
+ console.log(`[normalize-memory] Found ${memoryFiles.length} memory-bank files`);
320
+ for (const file of memoryFiles) {
321
+ const result = normalizeFile(file, vocab, allTags, true);
322
+ if (result.action !== 'skipped') {
323
+ console.log(` ${result.action}: ${basename(file)} (tags: [${result.tags.join(', ')}], category: ${result.category})`);
324
+ }
325
+ }
326
+
327
+ // Normalize docs files (including hidden)
328
+ const docFiles = collectMdFiles(DOCS_DIR);
329
+ console.log(`[normalize-memory] Found ${docFiles.length} docs files`);
330
+ for (const file of docFiles) {
331
+ // Memory-bank files may also exist in docs/ for some projects — skip duplicates
332
+ if (file.includes('/.index/')) continue;
333
+ const result = normalizeFile(file, vocab, allTags, false);
334
+ if (result.action !== 'skipped') {
335
+ console.log(` ${result.action}: ${basename(file)} (tags: [${result.tags.join(', ')}])`);
336
+ }
337
+ }
338
+
339
+ // Create marker file
340
+ if (!dryRun) {
341
+ ensureDir(MEMORY_BANK_DIR);
342
+ writeFileSync(MARKER_FILE, new Date().toISOString() + '\n', 'utf8');
343
+ console.log('[normalize-memory] Created .normalized marker');
344
+ }
345
+
346
+ console.log('[normalize-memory] Done.');
347
+ }
348
+
349
+ main();
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ describe('sync utility functions', () => {
4
+ it('should validate basic assertions work', () => {
5
+ expect(1 + 1).toBe(2)
6
+ })
7
+
8
+ it('should handle string operations', () => {
9
+ const result = 'ai-workflow-template'.split('-').join(' ')
10
+ expect(result).toBe('ai workflow template')
11
+ })
12
+ })
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * validate-memory-schema.js — Pre-commit hook validator.
5
+ *
6
+ * Checks staged memory-bank/*.md files for valid YAML frontmatter.
7
+ * Rejects commits with missing/invalid fields.
8
+ *
9
+ * Reads allowed values from memory-bank/.vocabulary.json at validation time.
10
+ *
11
+ * Usage (in .husky/pre-commit):
12
+ * node scripts/validate-memory-schema.js
13
+ */
14
+
15
+ import { execSync } from 'node:child_process';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { basename, extname, resolve } from 'node:path';
18
+
19
+ const MEMORY_BANK_DIR = resolve(process.cwd(), 'memory-bank');
20
+ const VOCAB_PATH = resolve(MEMORY_BANK_DIR, '.vocabulary.json');
21
+
22
+ // ── Load vocabulary ──────────────────────────────────────────────────
23
+
24
+ function loadVocabulary() {
25
+ if (!existsSync(VOCAB_PATH)) {
26
+ console.error('[validate-memory] ⚠ No .vocabulary.json found at memory-bank/.vocabulary.json');
27
+ console.error('[validate-memory] Schema validation skipped — vocabulary is required for validation.');
28
+ process.exit(0); // Don't block commits if vocab is missing (e.g., template not yet installed)
29
+ }
30
+
31
+ try {
32
+ return JSON.parse(readFileSync(VOCAB_PATH, 'utf8'));
33
+ } catch (err) {
34
+ console.error(`[validate-memory] ⚠ Failed to parse .vocabulary.json: ${err.message}`);
35
+ process.exit(0);
36
+ }
37
+ }
38
+
39
+ // ── Get staged memory-bank files ─────────────────────────────────────
40
+
41
+ function getStagedMemoryFiles() {
42
+ try {
43
+ const output = execSync(
44
+ 'git diff --cached --name-only --diff-filter=ACM',
45
+ { encoding: 'utf8', cwd: process.cwd() }
46
+ );
47
+ return output
48
+ .split('\n')
49
+ .filter(f => f.startsWith('memory-bank/') && f.endsWith('.md') && !f.includes('/.index/'));
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ // ── Parse frontmatter ────────────────────────────────────────────────
56
+
57
+ function parseFrontmatter(content) {
58
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
59
+ if (!match) return null;
60
+
61
+ const raw = match[1];
62
+ const fm = {};
63
+ for (const line of raw.split('\n')) {
64
+ const kv = line.match(/^(\w[\w-]*):\s*(.*)/);
65
+ if (kv) {
66
+ const key = kv[1];
67
+ let value = kv[2].trim();
68
+ // Parse arrays
69
+ if (value.startsWith('[') && value.endsWith(']')) {
70
+ value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
71
+ } else if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
72
+ value = value.slice(1, -1);
73
+ }
74
+ fm[key] = value;
75
+ }
76
+ }
77
+ return fm;
78
+ }
79
+
80
+ // ── Validate ─────────────────────────────────────────────────────────
81
+
82
+ function validateFile(filePath, vocab) {
83
+ const errors = [];
84
+ let content;
85
+
86
+ try {
87
+ content = readFileSync(resolve(process.cwd(), filePath), 'utf8');
88
+ } catch {
89
+ return [`Cannot read file: ${filePath}`];
90
+ }
91
+
92
+ const fm = parseFrontmatter(content);
93
+ if (!fm) {
94
+ errors.push(`Missing YAML frontmatter (file must start with ---...---)`);
95
+ return errors;
96
+ }
97
+
98
+ const fileName = basename(filePath);
99
+ const fileId = basename(filePath, extname(filePath));
100
+
101
+ // Required fields for memory-bank files
102
+ const requiredFields = ['id', 'title', 'updated', 'tags', 'category'];
103
+ for (const field of requiredFields) {
104
+ if (!fm[field]) {
105
+ errors.push(`Missing required field: '${field}'`);
106
+ }
107
+ }
108
+
109
+ // id must match filename
110
+ if (fm.id && fm.id !== fileId) {
111
+ errors.push(`Field 'id' (${fm.id}) does not match filename (${fileId})`);
112
+ }
113
+
114
+ // updated must be ISO date
115
+ if (fm.updated && !/^\d{4}-\d{2}-\d{2}$/.test(fm.updated)) {
116
+ errors.push(`Field 'updated' (${fm.updated}) is not a valid date. Use YYYY-MM-DD format.`);
117
+ }
118
+
119
+ // category must be in allowed list
120
+ if (fm.category && vocab?.categories) {
121
+ const allowed = Object.keys(vocab.categories);
122
+ if (!allowed.includes(fm.category)) {
123
+ errors.push(
124
+ `Invalid category '${fm.category}'. Allowed: ${allowed.join(', ')}`
125
+ );
126
+ }
127
+ }
128
+
129
+ // tags must be from vocabulary (warn only — don't block for new user-defined tags)
130
+ if (fm.tags && Array.isArray(fm.tags) && vocab?.tags) {
131
+ const allTags = Object.values(vocab.tags).flat();
132
+ const unknown = fm.tags.filter(t => !allTags.includes(t));
133
+ if (unknown.length > 0) {
134
+ console.warn(
135
+ `[validate-memory] ⚠ ${filePath}: Unknown tags: [${unknown.join(', ')}]. ` +
136
+ `Consider adding them to memory-bank/.vocabulary.json.`
137
+ );
138
+ }
139
+ }
140
+
141
+ return errors;
142
+ }
143
+
144
+ // ── Main ─────────────────────────────────────────────────────────────
145
+
146
+ function main() {
147
+ const stagedFiles = getStagedMemoryFiles();
148
+
149
+ if (stagedFiles.length === 0) {
150
+ process.exit(0); // No memory-bank files staged — nothing to validate
151
+ }
152
+
153
+ const vocab = loadVocabulary();
154
+ let hasErrors = false;
155
+
156
+ for (const file of stagedFiles) {
157
+ const errors = validateFile(file, vocab);
158
+ if (errors.length > 0) {
159
+ hasErrors = true;
160
+ console.error(`\n❌ ${file}:`);
161
+ for (const err of errors) {
162
+ console.error(` - ${err}`);
163
+ }
164
+ console.error('');
165
+ console.error(' Fix: Add valid YAML frontmatter at the top of the file:');
166
+ console.error(' ---');
167
+ console.error(' id: <filename-slug>');
168
+ console.error(' title: "<H1 heading>"');
169
+ console.error(' updated: YYYY-MM-DD');
170
+ console.error(' tags: [<from vocabulary>]');
171
+ console.error(' entities: [<components>]');
172
+ console.error(' category: <from list>');
173
+ console.error(' ---');
174
+ console.error('');
175
+ }
176
+ }
177
+
178
+ if (hasErrors) {
179
+ console.error(
180
+ `[validate-memory] ${stagedFiles.length} file(s) checked, errors found. ` +
181
+ `Run 'node scripts/normalize-memory.js' to auto-fix, or fix manually.`
182
+ );
183
+ process.exit(1);
184
+ }
185
+
186
+ console.log(`[validate-memory] ✓ ${stagedFiles.length} memory file(s) validated`);
187
+ process.exit(0);
188
+ }
189
+
190
+ main();