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 +1 -1
- package/scripts/agileflow-configure.js +105 -27
- package/scripts/agileflow-welcome.js +95 -2
- package/scripts/auto-self-improve.js +301 -0
- package/scripts/ralph-loop.js +491 -0
- package/src/core/agents/design.md +1 -1
- package/src/core/agents/documentation.md +1 -1
- package/src/core/agents/integrations.md +1 -1
- package/src/core/agents/mobile.md +1 -1
- package/src/core/agents/monitoring.md +1 -1
- package/src/core/agents/performance.md +1 -1
- package/src/core/commands/babysit.md +73 -0
- package/src/core/commands/configure.md +50 -7
- package/src/core/experts/documentation/expertise.yaml +4 -0
- package/src/core/experts/research/expertise.yaml +2 -2
- package/tools/cli/lib/docs-setup.js +1 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
560
|
-
const
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
//
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
660
|
-
|
|
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 };
|