agileflow 2.75.0 → 2.76.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.75.0",
3
+ "version": "2.76.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -23,7 +23,7 @@
23
23
  * --detect Show current status
24
24
  * --help Show help
25
25
  *
26
- * Features: sessionstart, precompact, archival, statusline, autoupdate
26
+ * Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline, autoupdate
27
27
  */
28
28
 
29
29
  const fs = require('fs');
@@ -71,7 +71,8 @@ const VERSION = getVersion();
71
71
  const FEATURES = {
72
72
  sessionstart: { hook: 'SessionStart', script: 'agileflow-welcome.js', type: 'node' },
73
73
  precompact: { hook: 'PreCompact', script: 'precompact-context.sh', type: 'bash' },
74
- // Note: Stop hook removed due to Claude Code reliability issues (see GitHub issues #6974, #11544)
74
+ ralphloop: { hook: 'Stop', script: 'ralph-loop.js', type: 'node' },
75
+ selfimprove: { hook: 'Stop', script: 'auto-self-improve.js', type: 'node' },
75
76
  archival: { script: 'archive-completed-stories.sh', requiresHook: 'sessionstart' },
76
77
  statusline: { script: 'agileflow-statusline.sh' },
77
78
  autoupdate: { metadataOnly: true }, // Stored in metadata.updates.autoUpdate
@@ -82,6 +83,8 @@ const ALL_SCRIPTS = {
82
83
  // Core feature scripts (linked to FEATURES)
83
84
  'agileflow-welcome.js': { feature: 'sessionstart', required: true },
84
85
  'precompact-context.sh': { feature: 'precompact', required: true },
86
+ 'ralph-loop.js': { feature: 'ralphloop', required: true },
87
+ 'auto-self-improve.js': { feature: 'selfimprove', required: true },
85
88
  'archive-completed-stories.sh': { feature: 'archival', required: true },
86
89
  'agileflow-statusline.sh': { feature: 'statusline', required: true },
87
90
 
@@ -119,25 +122,25 @@ const STATUSLINE_COMPONENTS = [
119
122
 
120
123
  const PROFILES = {
121
124
  full: {
122
- description: 'All features enabled',
123
- enable: ['sessionstart', 'precompact', 'archival', 'statusline'],
125
+ description: 'All features enabled (including experimental Stop hooks)',
126
+ enable: ['sessionstart', 'precompact', 'archival', 'statusline', 'ralphloop', 'selfimprove'],
124
127
  archivalDays: 30,
125
128
  },
126
129
  basic: {
127
130
  description: 'Essential hooks + archival (SessionStart + PreCompact + Archival)',
128
131
  enable: ['sessionstart', 'precompact', 'archival'],
129
- disable: ['statusline'],
132
+ disable: ['statusline', 'ralphloop', 'selfimprove'],
130
133
  archivalDays: 30,
131
134
  },
132
135
  minimal: {
133
136
  description: 'SessionStart + archival only',
134
137
  enable: ['sessionstart', 'archival'],
135
- disable: ['precompact', 'statusline'],
138
+ disable: ['precompact', 'statusline', 'ralphloop', 'selfimprove'],
136
139
  archivalDays: 30,
137
140
  },
138
141
  none: {
139
142
  description: 'Disable all AgileFlow features',
140
- disable: ['sessionstart', 'precompact', 'archival', 'statusline'],
143
+ disable: ['sessionstart', 'precompact', 'archival', 'statusline', 'ralphloop', 'selfimprove'],
141
144
  },
142
145
  };
143
146
 
@@ -208,6 +211,8 @@ function detectConfig() {
208
211
  features: {
209
212
  sessionstart: { enabled: false, valid: true, issues: [], version: null, outdated: false },
210
213
  precompact: { enabled: false, valid: true, issues: [], version: null, outdated: false },
214
+ ralphloop: { enabled: false, valid: true, issues: [], version: null, outdated: false },
215
+ selfimprove: { enabled: false, valid: true, issues: [], version: null, outdated: false },
211
216
  archival: { enabled: false, threshold: null, version: null, outdated: false },
212
217
  statusline: { enabled: false, valid: true, issues: [], version: null, outdated: false },
213
218
  },
@@ -276,7 +281,23 @@ function detectConfig() {
276
281
  }
277
282
  }
278
283
 
279
- // Note: Stop hook removed due to reliability issues
284
+ // Stop hooks (ralphloop and selfimprove)
285
+ if (settings.hooks.Stop) {
286
+ if (Array.isArray(settings.hooks.Stop) && settings.hooks.Stop.length > 0) {
287
+ const hook = settings.hooks.Stop[0];
288
+ if (hook.matcher !== undefined && hook.hooks) {
289
+ // Check for each Stop hook feature
290
+ for (const h of hook.hooks) {
291
+ if (h.command?.includes('ralph-loop')) {
292
+ status.features.ralphloop.enabled = true;
293
+ }
294
+ if (h.command?.includes('auto-self-improve')) {
295
+ status.features.selfimprove.enabled = true;
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
280
301
  }
281
302
 
282
303
  // StatusLine
@@ -370,6 +391,8 @@ function printStatus(status) {
370
391
 
371
392
  printFeature('sessionstart', 'SessionStart Hook');
372
393
  printFeature('precompact', 'PreCompact Hook');
394
+ printFeature('ralphloop', 'RalphLoop (Stop)');
395
+ printFeature('selfimprove', 'SelfImprove (Stop)');
373
396
 
374
397
  const arch = status.features.archival;
375
398
  log(
@@ -417,9 +440,9 @@ function migrateSettings() {
417
440
 
418
441
  let migrated = false;
419
442
 
420
- // Migrate hooks (Stop hook removed due to reliability issues)
443
+ // Migrate hooks to new format
421
444
  if (settings.hooks) {
422
- ['SessionStart', 'PreCompact', 'UserPromptSubmit'].forEach(hookName => {
445
+ ['SessionStart', 'PreCompact', 'UserPromptSubmit', 'Stop'].forEach(hookName => {
423
446
  const hook = settings.hooks[hookName];
424
447
  if (!hook) return;
425
448
 
@@ -556,16 +579,46 @@ function enableFeature(feature, options = {}) {
556
579
  return false;
557
580
  }
558
581
 
559
- // Configure hook
560
- const command = config.type === 'node' ? `node ${scriptPath}` : `bash ${scriptPath}`;
582
+ // Use absolute path so hooks work from any subdirectory
583
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
584
+
585
+ // Stop hooks use error suppression to avoid blocking Claude
586
+ const isStoHook = config.hook === 'Stop';
587
+ const command = config.type === 'node'
588
+ ? `node ${absoluteScriptPath}${isStoHook ? ' 2>/dev/null || true' : ''}`
589
+ : `bash ${absoluteScriptPath}${isStoHook ? ' 2>/dev/null || true' : ''}`;
590
+
591
+ if (isStoHook) {
592
+ // Stop hooks stack - add to existing hooks instead of replacing
593
+ if (!settings.hooks.Stop) {
594
+ settings.hooks.Stop = [{ matcher: '', hooks: [] }];
595
+ } else if (!Array.isArray(settings.hooks.Stop) || settings.hooks.Stop.length === 0) {
596
+ settings.hooks.Stop = [{ matcher: '', hooks: [] }];
597
+ } else if (!settings.hooks.Stop[0].hooks) {
598
+ settings.hooks.Stop[0].hooks = [];
599
+ }
600
+
601
+ // Check if this script is already added
602
+ const hasHook = settings.hooks.Stop[0].hooks.some(h =>
603
+ h.command?.includes(config.script)
604
+ );
561
605
 
562
- settings.hooks[config.hook] = [
563
- {
564
- matcher: '',
565
- hooks: [{ type: 'command', command }],
566
- },
567
- ];
568
- success(`${config.hook} hook enabled (${config.script})`);
606
+ if (!hasHook) {
607
+ settings.hooks.Stop[0].hooks.push({ type: 'command', command });
608
+ success(`Stop hook added (${config.script})`);
609
+ } else {
610
+ info(`${feature} already enabled`);
611
+ }
612
+ } else {
613
+ // Other hooks (SessionStart, PreCompact) replace entirely
614
+ settings.hooks[config.hook] = [
615
+ {
616
+ matcher: '',
617
+ hooks: [{ type: 'command', command }],
618
+ },
619
+ ];
620
+ success(`${config.hook} hook enabled (${config.script})`);
621
+ }
569
622
  }
570
623
 
571
624
  // Handle archival
@@ -579,7 +632,8 @@ function enableFeature(feature, options = {}) {
579
632
  return false;
580
633
  }
581
634
 
582
- // Add to SessionStart hook
635
+ // Use absolute path so hooks work from any subdirectory
636
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
583
637
  if (settings.hooks.SessionStart?.[0]?.hooks) {
584
638
  const hasArchival = settings.hooks.SessionStart[0].hooks.some(h =>
585
639
  h.command?.includes('archive-completed-stories')
@@ -587,7 +641,7 @@ function enableFeature(feature, options = {}) {
587
641
  if (!hasArchival) {
588
642
  settings.hooks.SessionStart[0].hooks.push({
589
643
  type: 'command',
590
- command: `bash ${scriptPath} --quiet`,
644
+ command: `bash ${absoluteScriptPath} --quiet`,
591
645
  });
592
646
  }
593
647
  }
@@ -607,9 +661,11 @@ function enableFeature(feature, options = {}) {
607
661
  return false;
608
662
  }
609
663
 
664
+ // Use absolute path so hooks work from any subdirectory
665
+ const absoluteScriptPath = path.join(process.cwd(), scriptPath);
610
666
  settings.statusLine = {
611
667
  type: 'command',
612
- command: `bash ${scriptPath}`,
668
+ command: `bash ${absoluteScriptPath}`,
613
669
  padding: 0,
614
670
  };
615
671
  success('Status line enabled');
@@ -656,8 +712,28 @@ function disableFeature(feature) {
656
712
 
657
713
  // Disable hook
658
714
  if (config.hook && settings.hooks?.[config.hook]) {
659
- delete settings.hooks[config.hook];
660
- success(`${config.hook} hook disabled`);
715
+ if (config.hook === 'Stop') {
716
+ // Stop hooks stack - remove only this script, not the entire hook
717
+ if (settings.hooks.Stop?.[0]?.hooks) {
718
+ const before = settings.hooks.Stop[0].hooks.length;
719
+ settings.hooks.Stop[0].hooks = settings.hooks.Stop[0].hooks.filter(
720
+ h => !h.command?.includes(config.script)
721
+ );
722
+ const after = settings.hooks.Stop[0].hooks.length;
723
+
724
+ if (before > after) {
725
+ success(`Stop hook removed (${config.script})`);
726
+ }
727
+
728
+ // If no more Stop hooks, remove the entire Stop hook
729
+ if (settings.hooks.Stop[0].hooks.length === 0) {
730
+ delete settings.hooks.Stop;
731
+ }
732
+ }
733
+ } else {
734
+ delete settings.hooks[config.hook];
735
+ success(`${config.hook} hook disabled`);
736
+ }
661
737
  }
662
738
 
663
739
  // Disable archival
@@ -1142,8 +1218,8 @@ ${c.cyan}Usage:${c.reset}
1142
1218
  node .agileflow/scripts/agileflow-configure.js [options]
1143
1219
 
1144
1220
  ${c.cyan}Profiles:${c.reset}
1145
- --profile=full All features (hooks, archival, statusline)
1146
- --profile=basic SessionStart + PreCompact + archival
1221
+ --profile=full All features (hooks, Stop hooks, archival, statusline)
1222
+ --profile=basic SessionStart + PreCompact + archival (no Stop hooks)
1147
1223
  --profile=minimal SessionStart + archival only
1148
1224
  --profile=none Disable all AgileFlow features
1149
1225
 
@@ -1151,7 +1227,9 @@ ${c.cyan}Feature Control:${c.reset}
1151
1227
  --enable=<list> Enable features (comma-separated)
1152
1228
  --disable=<list> Disable features (comma-separated)
1153
1229
 
1154
- Features: sessionstart, precompact, archival, statusline
1230
+ Features: sessionstart, precompact, ralphloop, selfimprove, archival, statusline
1231
+
1232
+ Stop hooks (ralphloop, selfimprove) run when Claude completes/pauses
1155
1233
 
1156
1234
  ${c.cyan}Statusline Components:${c.reset}
1157
1235
  --show=<list> Show statusline components (comma-separated)
@@ -433,6 +433,85 @@ async function runAutoUpdate(rootDir) {
433
433
  }
434
434
  }
435
435
 
436
+ function validateExpertise(rootDir) {
437
+ const result = { total: 0, passed: 0, warnings: 0, failed: 0, issues: [] };
438
+
439
+ // Find experts directory
440
+ let expertsDir = path.join(rootDir, '.agileflow', 'experts');
441
+ if (!fs.existsSync(expertsDir)) {
442
+ expertsDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'experts');
443
+ }
444
+ if (!fs.existsSync(expertsDir)) {
445
+ return result; // No experts directory found
446
+ }
447
+
448
+ const STALE_DAYS = 30;
449
+ const MAX_LINES = 200;
450
+
451
+ try {
452
+ const domains = fs.readdirSync(expertsDir, { withFileTypes: true })
453
+ .filter(d => d.isDirectory() && d.name !== 'templates')
454
+ .map(d => d.name);
455
+
456
+ for (const domain of domains) {
457
+ const filePath = path.join(expertsDir, domain, 'expertise.yaml');
458
+ if (!fs.existsSync(filePath)) {
459
+ result.total++;
460
+ result.failed++;
461
+ result.issues.push(`${domain}: missing file`);
462
+ continue;
463
+ }
464
+
465
+ result.total++;
466
+ const content = fs.readFileSync(filePath, 'utf8');
467
+ const lines = content.split('\n');
468
+ let status = 'pass';
469
+ let issue = '';
470
+
471
+ // Check required fields (use multiline flag)
472
+ const hasVersion = /^version:/m.test(content);
473
+ const hasDomain = /^domain:/m.test(content);
474
+ const hasLastUpdated = /^last_updated:/m.test(content);
475
+
476
+ if (!hasVersion || !hasDomain || !hasLastUpdated) {
477
+ status = 'fail';
478
+ issue = 'missing required fields';
479
+ }
480
+
481
+ // Check staleness
482
+ const lastUpdatedMatch = content.match(/^last_updated:\s*['"]?(\d{4}-\d{2}-\d{2})/m);
483
+ if (lastUpdatedMatch && status !== 'fail') {
484
+ const lastDate = new Date(lastUpdatedMatch[1]);
485
+ const daysSince = Math.floor((Date.now() - lastDate.getTime()) / (1000 * 60 * 60 * 24));
486
+ if (daysSince > STALE_DAYS) {
487
+ status = 'warn';
488
+ issue = `stale (${daysSince}d)`;
489
+ }
490
+ }
491
+
492
+ // Check file size
493
+ if (lines.length > MAX_LINES && status === 'pass') {
494
+ status = 'warn';
495
+ issue = `large (${lines.length} lines)`;
496
+ }
497
+
498
+ if (status === 'pass') {
499
+ result.passed++;
500
+ } else if (status === 'warn') {
501
+ result.warnings++;
502
+ result.issues.push(`${domain}: ${issue}`);
503
+ } else {
504
+ result.failed++;
505
+ result.issues.push(`${domain}: ${issue}`);
506
+ }
507
+ }
508
+ } catch (e) {
509
+ // Silently fail
510
+ }
511
+
512
+ return result;
513
+ }
514
+
436
515
  function getFeatureVersions(rootDir) {
437
516
  const result = {
438
517
  hooks: { version: null, outdated: false },
@@ -505,7 +584,7 @@ function truncate(str, maxLen, suffix = '..') {
505
584
  return str.substring(0, cutIndex) + suffix;
506
585
  }
507
586
 
508
- function formatTable(info, archival, session, precompact, parallelSessions, updateInfo = {}) {
587
+ function formatTable(info, archival, session, precompact, parallelSessions, updateInfo = {}, expertise = {}) {
509
588
  const W = 58; // inner width
510
589
  const R = W - 24; // right column width (34 chars)
511
590
  const lines = [];
@@ -662,6 +741,19 @@ function formatTable(info, archival, session, precompact, parallelSessions, upda
662
741
  }
663
742
  }
664
743
 
744
+ // Agent expertise validation (only show if issues exist)
745
+ if (expertise && expertise.total > 0) {
746
+ if (expertise.failed > 0) {
747
+ const expertStr = `❌ ${expertise.failed} failed, ${expertise.warnings} warnings`;
748
+ lines.push(row('Expertise', expertStr, c.dim, c.red));
749
+ } else if (expertise.warnings > 0) {
750
+ const expertStr = `⚠️ ${expertise.warnings} warnings (${expertise.passed} ok)`;
751
+ lines.push(row('Expertise', expertStr, c.dim, c.yellow));
752
+ } else {
753
+ lines.push(row('Expertise', `✓ ${expertise.total} valid`, c.dim, c.green));
754
+ }
755
+ }
756
+
665
757
  lines.push(divider());
666
758
 
667
759
  // Current story (if any) - row() auto-truncates
@@ -694,6 +786,7 @@ async function main() {
694
786
  const session = clearActiveCommands(rootDir);
695
787
  const precompact = checkPreCompact(rootDir);
696
788
  const parallelSessions = checkParallelSessions(rootDir);
789
+ const expertise = validateExpertise(rootDir);
697
790
 
698
791
  // Check for updates (async, cached)
699
792
  let updateInfo = {};
@@ -717,7 +810,7 @@ async function main() {
717
810
  // Update check failed - continue without it
718
811
  }
719
812
 
720
- console.log(formatTable(info, archival, session, precompact, parallelSessions, updateInfo));
813
+ console.log(formatTable(info, archival, session, precompact, parallelSessions, updateInfo, expertise));
721
814
 
722
815
  // Show warning and tip if other sessions are active
723
816
  if (parallelSessions.otherActive > 0) {
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * auto-self-improve.js - Automatic Agent Expertise Updates
5
+ *
6
+ * This script runs as a Stop hook and automatically updates agent
7
+ * expertise files based on work performed during the session.
8
+ *
9
+ * How it works:
10
+ * 1. Reads session-state.json to find which agent was active
11
+ * 2. Analyzes git diff to see what changed
12
+ * 3. Detects patterns, new files, significant changes
13
+ * 4. Generates a learning entry
14
+ * 5. Appends to the agent's expertise.yaml
15
+ *
16
+ * Usage (as Stop hook):
17
+ * node scripts/auto-self-improve.js
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { execSync } = require('child_process');
23
+
24
+ // ANSI colors
25
+ const c = {
26
+ reset: '\x1b[0m',
27
+ bold: '\x1b[1m',
28
+ dim: '\x1b[2m',
29
+ red: '\x1b[31m',
30
+ green: '\x1b[32m',
31
+ yellow: '\x1b[33m',
32
+ blue: '\x1b[34m',
33
+ cyan: '\x1b[36m',
34
+ brand: '\x1b[38;2;232;104;58m',
35
+ };
36
+
37
+ // Agents that have expertise files
38
+ const AGENTS_WITH_EXPERTISE = [
39
+ 'accessibility', 'adr-writer', 'analytics', 'api', 'ci', 'compliance',
40
+ 'database', 'datamigration', 'design', 'devops', 'documentation',
41
+ 'epic-planner', 'integrations', 'mentor', 'mobile', 'monitoring',
42
+ 'performance', 'product', 'qa', 'readme-updater', 'refactor',
43
+ 'research', 'security', 'testing', 'ui'
44
+ ];
45
+
46
+ // File patterns that suggest domain expertise
47
+ const DOMAIN_PATTERNS = {
48
+ 'database': [/schema/, /migration/, /\.sql$/, /prisma/, /drizzle/, /sequelize/],
49
+ 'api': [/\/api\//, /controller/, /route/, /endpoint/, /graphql/],
50
+ 'ui': [/component/, /\.tsx$/, /\.jsx$/, /styles/, /\.css$/, /\.scss$/],
51
+ 'testing': [/\.test\./, /\.spec\./, /__tests__/, /jest/, /vitest/],
52
+ 'security': [/auth/, /password/, /token/, /jwt/, /oauth/, /permission/],
53
+ 'ci': [/\.github\/workflows/, /\.gitlab-ci/, /dockerfile/i, /docker-compose/],
54
+ 'documentation': [/\.md$/, /readme/i, /docs\//, /jsdoc/],
55
+ 'performance': [/cache/, /optimize/, /performance/, /benchmark/],
56
+ 'devops': [/deploy/, /kubernetes/, /k8s/, /terraform/, /ansible/],
57
+ };
58
+
59
+ // Find project root
60
+ function getProjectRoot() {
61
+ let dir = process.cwd();
62
+ while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
63
+ dir = path.dirname(dir);
64
+ }
65
+ return dir !== '/' ? dir : process.cwd();
66
+ }
67
+
68
+ // Read session state
69
+ function getSessionState(rootDir) {
70
+ const statePath = path.join(rootDir, 'docs/09-agents/session-state.json');
71
+ try {
72
+ if (fs.existsSync(statePath)) {
73
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
74
+ }
75
+ } catch (e) {}
76
+ return {};
77
+ }
78
+
79
+ // Get git diff summary
80
+ function getGitDiff(rootDir) {
81
+ try {
82
+ // Get list of changed files (staged and unstaged)
83
+ const diffFiles = execSync('git diff --name-only HEAD 2>/dev/null || git diff --name-only', {
84
+ cwd: rootDir,
85
+ encoding: 'utf8',
86
+ }).trim().split('\n').filter(Boolean);
87
+
88
+ // Get staged files
89
+ const stagedFiles = execSync('git diff --cached --name-only 2>/dev/null', {
90
+ cwd: rootDir,
91
+ encoding: 'utf8',
92
+ }).trim().split('\n').filter(Boolean);
93
+
94
+ // Get untracked files
95
+ const untrackedFiles = execSync('git ls-files --others --exclude-standard 2>/dev/null', {
96
+ cwd: rootDir,
97
+ encoding: 'utf8',
98
+ }).trim().split('\n').filter(Boolean);
99
+
100
+ // Combine all
101
+ const allFiles = [...new Set([...diffFiles, ...stagedFiles, ...untrackedFiles])];
102
+
103
+ // Get diff stats
104
+ let additions = 0;
105
+ let deletions = 0;
106
+ try {
107
+ const stats = execSync('git diff --shortstat HEAD 2>/dev/null || echo ""', {
108
+ cwd: rootDir,
109
+ encoding: 'utf8',
110
+ });
111
+ const addMatch = stats.match(/(\d+) insertion/);
112
+ const delMatch = stats.match(/(\d+) deletion/);
113
+ if (addMatch) additions = parseInt(addMatch[1]);
114
+ if (delMatch) deletions = parseInt(delMatch[1]);
115
+ } catch (e) {}
116
+
117
+ return {
118
+ files: allFiles,
119
+ additions,
120
+ deletions,
121
+ hasChanges: allFiles.length > 0,
122
+ };
123
+ } catch (e) {
124
+ return { files: [], additions: 0, deletions: 0, hasChanges: false };
125
+ }
126
+ }
127
+
128
+ // Detect which domain the changes relate to
129
+ function detectDomain(files) {
130
+ const domainScores = {};
131
+
132
+ for (const file of files) {
133
+ for (const [domain, patterns] of Object.entries(DOMAIN_PATTERNS)) {
134
+ for (const pattern of patterns) {
135
+ if (pattern.test(file.toLowerCase())) {
136
+ domainScores[domain] = (domainScores[domain] || 0) + 1;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ // Return domain with highest score
143
+ const sorted = Object.entries(domainScores).sort((a, b) => b[1] - a[1]);
144
+ return sorted.length > 0 ? sorted[0][0] : null;
145
+ }
146
+
147
+ // Generate learning summary from changes
148
+ function generateLearningSummary(diff, activeAgent) {
149
+ const { files, additions, deletions } = diff;
150
+
151
+ if (files.length === 0) return null;
152
+
153
+ // Categorize files
154
+ const newFiles = files.filter(f => !f.includes('/'));
155
+ const testFiles = files.filter(f => /\.(test|spec)\.[jt]sx?$/.test(f));
156
+ const configFiles = files.filter(f => /\.(json|yaml|yml|toml|config\.)/.test(f));
157
+ const codeFiles = files.filter(f => /\.[jt]sx?$/.test(f) && !testFiles.includes(f));
158
+
159
+ // Build summary
160
+ const parts = [];
161
+
162
+ if (codeFiles.length > 0) {
163
+ const dirs = [...new Set(codeFiles.map(f => path.dirname(f)))];
164
+ parts.push(`Modified ${codeFiles.length} code file(s) in: ${dirs.slice(0, 3).join(', ')}`);
165
+ }
166
+
167
+ if (testFiles.length > 0) {
168
+ parts.push(`Updated ${testFiles.length} test file(s)`);
169
+ }
170
+
171
+ if (configFiles.length > 0) {
172
+ parts.push(`Changed config: ${configFiles.slice(0, 2).join(', ')}`);
173
+ }
174
+
175
+ if (additions > 50 || deletions > 50) {
176
+ parts.push(`Significant changes: +${additions}/-${deletions} lines`);
177
+ }
178
+
179
+ return parts.length > 0 ? parts.join('. ') : null;
180
+ }
181
+
182
+ // Find expertise file for agent
183
+ function getExpertisePath(rootDir, agent) {
184
+ // Try installed location first
185
+ const installedPath = path.join(rootDir, '.agileflow', 'experts', agent, 'expertise.yaml');
186
+ if (fs.existsSync(installedPath)) return installedPath;
187
+
188
+ // Try source location
189
+ const sourcePath = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'experts', agent, 'expertise.yaml');
190
+ if (fs.existsSync(sourcePath)) return sourcePath;
191
+
192
+ return null;
193
+ }
194
+
195
+ // Append learning to expertise file
196
+ function appendLearning(expertisePath, learning) {
197
+ try {
198
+ let content = fs.readFileSync(expertisePath, 'utf8');
199
+
200
+ // Find the learnings section
201
+ const learningsMatch = content.match(/^learnings:\s*$/m);
202
+
203
+ if (!learningsMatch) {
204
+ // No learnings section, add it at the end
205
+ content += `\n\nlearnings:\n${learning}`;
206
+ } else {
207
+ // Find where to insert (after "learnings:" line)
208
+ const insertPos = learningsMatch.index + learningsMatch[0].length;
209
+ content = content.slice(0, insertPos) + '\n' + learning + content.slice(insertPos);
210
+ }
211
+
212
+ fs.writeFileSync(expertisePath, content);
213
+ return true;
214
+ } catch (e) {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ // Format learning as YAML
220
+ function formatLearning(summary, files, detectedDomain) {
221
+ const date = new Date().toISOString().split('T')[0];
222
+ const topFiles = files.slice(0, 5).map(f => ` - ${f}`).join('\n');
223
+
224
+ return ` - date: "${date}"
225
+ auto_generated: true
226
+ context: "Session work - ${detectedDomain || 'general'} domain"
227
+ insight: "${summary.replace(/"/g, '\\"')}"
228
+ files_touched:
229
+ ${topFiles}`;
230
+ }
231
+
232
+ // Main function
233
+ function main() {
234
+ const rootDir = getProjectRoot();
235
+ const state = getSessionState(rootDir);
236
+ const diff = getGitDiff(rootDir);
237
+
238
+ // Check if there were any changes
239
+ if (!diff.hasChanges) {
240
+ return; // Silent exit - no changes to learn from
241
+ }
242
+
243
+ // Detect which agent was active
244
+ let activeAgent = null;
245
+
246
+ // Check session state for active command
247
+ if (state.active_command?.name) {
248
+ const name = state.active_command.name.replace('agileflow-', '');
249
+ if (AGENTS_WITH_EXPERTISE.includes(name)) {
250
+ activeAgent = name;
251
+ }
252
+ }
253
+
254
+ // If no agent from session, detect from file changes
255
+ if (!activeAgent) {
256
+ activeAgent = detectDomain(diff.files);
257
+ }
258
+
259
+ // If still no agent, skip
260
+ if (!activeAgent || !AGENTS_WITH_EXPERTISE.includes(activeAgent)) {
261
+ return; // Silent exit - can't determine which agent to update
262
+ }
263
+
264
+ // Find expertise file
265
+ const expertisePath = getExpertisePath(rootDir, activeAgent);
266
+ if (!expertisePath) {
267
+ return; // Silent exit - no expertise file found
268
+ }
269
+
270
+ // Generate learning summary
271
+ const summary = generateLearningSummary(diff, activeAgent);
272
+ if (!summary) {
273
+ return; // Silent exit - no meaningful summary
274
+ }
275
+
276
+ // Format and append learning
277
+ const learningYaml = formatLearning(summary, diff.files, activeAgent);
278
+ const success = appendLearning(expertisePath, learningYaml);
279
+
280
+ if (success) {
281
+ console.log('');
282
+ console.log(`${c.green}✓ Auto-learned:${c.reset} ${c.dim}${activeAgent}${c.reset}`);
283
+ console.log(`${c.dim} ${summary}${c.reset}`);
284
+ console.log(`${c.dim} → Updated ${path.basename(path.dirname(expertisePath))}/expertise.yaml${c.reset}`);
285
+ console.log('');
286
+ }
287
+ }
288
+
289
+ // Run if executed directly
290
+ if (require.main === module) {
291
+ try {
292
+ main();
293
+ } catch (e) {
294
+ // Silent fail - don't break the workflow
295
+ if (process.env.DEBUG) {
296
+ console.error('auto-self-improve error:', e.message);
297
+ }
298
+ }
299
+ }
300
+
301
+ module.exports = { main, detectDomain, generateLearningSummary };