agileflow 2.91.0 → 2.92.1
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/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +32 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +491 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +50 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +127 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +408 -55
- package/scripts/spawn-parallel.js +666 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +132 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +95 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +113 -2
- package/tools/cli/lib/ui.js +15 -25
|
@@ -17,7 +17,14 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
17
17
|
|
|
18
18
|
// Shared utilities
|
|
19
19
|
const { c, box } = require('../lib/colors');
|
|
20
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
getProjectRoot,
|
|
22
|
+
getStatusPath,
|
|
23
|
+
getMetadataPath,
|
|
24
|
+
getSessionStatePath,
|
|
25
|
+
getAgileflowDir,
|
|
26
|
+
getClaudeDir,
|
|
27
|
+
} = require('../lib/paths');
|
|
21
28
|
const { readJSONCached, readFileCached } = require('../lib/file-cache');
|
|
22
29
|
|
|
23
30
|
// Session manager path (relative to script location)
|
|
@@ -31,6 +38,14 @@ try {
|
|
|
31
38
|
// Story claiming not available
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
// Story state machine (for epic completion check)
|
|
42
|
+
let storyStateMachine;
|
|
43
|
+
try {
|
|
44
|
+
storyStateMachine = require('./lib/story-state-machine.js');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Story state machine not available
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
// File tracking module
|
|
35
50
|
let fileTracking;
|
|
36
51
|
try {
|
|
@@ -55,11 +70,11 @@ try {
|
|
|
55
70
|
*/
|
|
56
71
|
function loadProjectFiles(rootDir) {
|
|
57
72
|
const paths = {
|
|
58
|
-
status:
|
|
59
|
-
metadata:
|
|
60
|
-
settings: path.join(rootDir, '
|
|
61
|
-
sessionState:
|
|
62
|
-
configYaml: path.join(rootDir, '
|
|
73
|
+
status: getStatusPath(rootDir),
|
|
74
|
+
metadata: getMetadataPath(rootDir),
|
|
75
|
+
settings: path.join(getClaudeDir(rootDir), 'settings.json'),
|
|
76
|
+
sessionState: getSessionStatePath(rootDir),
|
|
77
|
+
configYaml: path.join(getAgileflowDir(rootDir), 'config.yaml'),
|
|
63
78
|
cliPackage: path.join(rootDir, 'packages', 'cli', 'package.json'),
|
|
64
79
|
};
|
|
65
80
|
|
|
@@ -73,6 +88,67 @@ function loadProjectFiles(rootDir) {
|
|
|
73
88
|
};
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Detect the user's platform for install guidance
|
|
93
|
+
*/
|
|
94
|
+
function detectPlatform() {
|
|
95
|
+
const platform = process.platform;
|
|
96
|
+
|
|
97
|
+
if (platform === 'darwin') {
|
|
98
|
+
return { os: 'macOS', installCmd: 'brew install tmux', hasSudo: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (platform === 'linux') {
|
|
102
|
+
// Try to detect Linux distribution
|
|
103
|
+
try {
|
|
104
|
+
const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
|
|
105
|
+
if (
|
|
106
|
+
osRelease.includes('Ubuntu') ||
|
|
107
|
+
osRelease.includes('Debian') ||
|
|
108
|
+
osRelease.includes('Pop!_OS') ||
|
|
109
|
+
osRelease.includes('Mint')
|
|
110
|
+
) {
|
|
111
|
+
return { os: 'Ubuntu/Debian', installCmd: 'sudo apt install tmux', hasSudo: true };
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
osRelease.includes('Fedora') ||
|
|
115
|
+
osRelease.includes('Red Hat') ||
|
|
116
|
+
osRelease.includes('CentOS') ||
|
|
117
|
+
osRelease.includes('Rocky')
|
|
118
|
+
) {
|
|
119
|
+
return { os: 'Fedora/RHEL', installCmd: 'sudo dnf install tmux', hasSudo: true };
|
|
120
|
+
}
|
|
121
|
+
if (osRelease.includes('Arch')) {
|
|
122
|
+
return { os: 'Arch', installCmd: 'sudo pacman -S tmux', hasSudo: true };
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Can't read /etc/os-release
|
|
126
|
+
}
|
|
127
|
+
return { os: 'Linux', installCmd: 'sudo apt install tmux', hasSudo: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Windows WSL or unknown
|
|
131
|
+
return { os: 'Unknown', installCmd: null, hasSudo: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if tmux is installed
|
|
136
|
+
* Returns object with availability info and platform-specific install suggestion
|
|
137
|
+
*/
|
|
138
|
+
function checkTmuxAvailability() {
|
|
139
|
+
try {
|
|
140
|
+
execSync('which tmux', { encoding: 'utf8', stdio: 'pipe' });
|
|
141
|
+
return { available: true };
|
|
142
|
+
} catch (e) {
|
|
143
|
+
const platform = detectPlatform();
|
|
144
|
+
return {
|
|
145
|
+
available: false,
|
|
146
|
+
platform,
|
|
147
|
+
noSudoCmd: 'conda install -c conda-forge tmux',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
76
152
|
/**
|
|
77
153
|
* PERFORMANCE OPTIMIZATION: Batch git commands into single call
|
|
78
154
|
* Reduces subprocess overhead from 3 calls to 1.
|
|
@@ -129,7 +205,7 @@ function getProjectInfo(rootDir, cache = null) {
|
|
|
129
205
|
info.version = cache.cliPackage.version;
|
|
130
206
|
} else {
|
|
131
207
|
// No cache - fall back to file reads (for backwards compatibility)
|
|
132
|
-
const configPath = path.join(rootDir, '
|
|
208
|
+
const configPath = path.join(getAgileflowDir(rootDir), 'config.yaml');
|
|
133
209
|
if (fs.existsSync(configPath)) {
|
|
134
210
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
135
211
|
const versionMatch = content.match(/^version:\s*['"]?([0-9.]+)/m);
|
|
@@ -137,7 +213,7 @@ function getProjectInfo(rootDir, cache = null) {
|
|
|
137
213
|
info.version = versionMatch[1];
|
|
138
214
|
}
|
|
139
215
|
} else {
|
|
140
|
-
const metadataPath =
|
|
216
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
141
217
|
if (fs.existsSync(metadataPath)) {
|
|
142
218
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
143
219
|
info.version = metadata.version || info.version;
|
|
@@ -180,7 +256,7 @@ function getProjectInfo(rootDir, cache = null) {
|
|
|
180
256
|
}
|
|
181
257
|
} else if (!cache) {
|
|
182
258
|
// No cache - fall back to file read
|
|
183
|
-
const statusPath =
|
|
259
|
+
const statusPath = getStatusPath(rootDir);
|
|
184
260
|
if (fs.existsSync(statusPath)) {
|
|
185
261
|
const statusData = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
186
262
|
if (statusData.stories) {
|
|
@@ -221,7 +297,7 @@ function runArchival(rootDir, cache = null) {
|
|
|
221
297
|
result.threshold = metadata.archival?.threshold_days || 7;
|
|
222
298
|
} else {
|
|
223
299
|
// No cache - fall back to file read
|
|
224
|
-
const metadataPath =
|
|
300
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
225
301
|
if (fs.existsSync(metadataPath)) {
|
|
226
302
|
const metadataData = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
227
303
|
if (metadataData.archival?.enabled === false) {
|
|
@@ -235,7 +311,7 @@ function runArchival(rootDir, cache = null) {
|
|
|
235
311
|
// Use cached status if available
|
|
236
312
|
const status = cache?.status;
|
|
237
313
|
if (!status && !cache) {
|
|
238
|
-
const statusPath =
|
|
314
|
+
const statusPath = getStatusPath(rootDir);
|
|
239
315
|
if (!fs.existsSync(statusPath)) return result;
|
|
240
316
|
}
|
|
241
317
|
|
|
@@ -277,7 +353,7 @@ function clearActiveCommands(rootDir, cache = null) {
|
|
|
277
353
|
const result = { ran: false, cleared: 0, commandNames: [], preserved: false };
|
|
278
354
|
|
|
279
355
|
try {
|
|
280
|
-
const sessionStatePath =
|
|
356
|
+
const sessionStatePath = getSessionStatePath(rootDir);
|
|
281
357
|
|
|
282
358
|
// Use cached sessionState if available, but we still need to read fresh for clearing
|
|
283
359
|
// because we need to write back. Cache is only useful to check if file exists.
|
|
@@ -364,7 +440,7 @@ function checkParallelSessions(rootDir) {
|
|
|
364
440
|
|
|
365
441
|
try {
|
|
366
442
|
// Check if session manager exists
|
|
367
|
-
const managerPath = path.join(rootDir, '
|
|
443
|
+
const managerPath = path.join(getAgileflowDir(rootDir), 'scripts', 'session-manager.js');
|
|
368
444
|
if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
|
|
369
445
|
return result;
|
|
370
446
|
}
|
|
@@ -458,7 +534,7 @@ function checkPreCompact(rootDir, cache = null) {
|
|
|
458
534
|
}
|
|
459
535
|
} else {
|
|
460
536
|
// No cache - fall back to file read
|
|
461
|
-
const settingsPath = path.join(rootDir, '
|
|
537
|
+
const settingsPath = path.join(getClaudeDir(rootDir), 'settings.json');
|
|
462
538
|
if (fs.existsSync(settingsPath)) {
|
|
463
539
|
const settingsData = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
464
540
|
if (settingsData.hooks?.PreCompact?.length > 0) {
|
|
@@ -468,7 +544,7 @@ function checkPreCompact(rootDir, cache = null) {
|
|
|
468
544
|
}
|
|
469
545
|
|
|
470
546
|
// Check if the script exists (must always check filesystem)
|
|
471
|
-
const scriptPath = path.join(rootDir, 'scripts
|
|
547
|
+
const scriptPath = path.join(rootDir, 'scripts', 'precompact-context.sh');
|
|
472
548
|
if (fs.existsSync(scriptPath)) {
|
|
473
549
|
result.scriptExists = true;
|
|
474
550
|
}
|
|
@@ -487,7 +563,7 @@ function checkPreCompact(rootDir, cache = null) {
|
|
|
487
563
|
}
|
|
488
564
|
} else if (!cache) {
|
|
489
565
|
// No cache - fall back to file read
|
|
490
|
-
const metadataPath =
|
|
566
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
491
567
|
if (fs.existsSync(metadataPath)) {
|
|
492
568
|
const metadataData = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
493
569
|
if (metadataData.features?.precompact?.configured_version) {
|
|
@@ -512,7 +588,7 @@ function checkDamageControl(rootDir, cache = null) {
|
|
|
512
588
|
let settings = cache?.settings;
|
|
513
589
|
if (!settings && !cache) {
|
|
514
590
|
// No cache - fall back to file read
|
|
515
|
-
const settingsPath = path.join(rootDir, '
|
|
591
|
+
const settingsPath = path.join(getClaudeDir(rootDir), 'settings.json');
|
|
516
592
|
if (fs.existsSync(settingsPath)) {
|
|
517
593
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
518
594
|
}
|
|
@@ -542,7 +618,7 @@ function checkDamageControl(rootDir, cache = null) {
|
|
|
542
618
|
}
|
|
543
619
|
|
|
544
620
|
// Check if all required scripts exist (in .claude/hooks/damage-control/)
|
|
545
|
-
const hooksDir = path.join(rootDir, '
|
|
621
|
+
const hooksDir = path.join(getClaudeDir(rootDir), 'hooks', 'damage-control');
|
|
546
622
|
const requiredScripts = [
|
|
547
623
|
'bash-tool-damage-control.js',
|
|
548
624
|
'edit-tool-damage-control.js',
|
|
@@ -560,8 +636,8 @@ function checkDamageControl(rootDir, cache = null) {
|
|
|
560
636
|
|
|
561
637
|
// Count patterns in patterns.yaml
|
|
562
638
|
const patternsLocations = [
|
|
563
|
-
path.join(rootDir, '
|
|
564
|
-
path.join(rootDir, '
|
|
639
|
+
path.join(getClaudeDir(rootDir), 'hooks', 'damage-control', 'patterns.yaml'),
|
|
640
|
+
path.join(getAgileflowDir(rootDir), 'scripts', 'damage-control', 'patterns.yaml'),
|
|
565
641
|
];
|
|
566
642
|
for (const patternsPath of patternsLocations) {
|
|
567
643
|
if (fs.existsSync(patternsPath)) {
|
|
@@ -591,6 +667,293 @@ function compareVersions(a, b) {
|
|
|
591
667
|
return 0;
|
|
592
668
|
}
|
|
593
669
|
|
|
670
|
+
/**
|
|
671
|
+
* All available config options with their version requirements
|
|
672
|
+
* These are the options that can be configured through /agileflow:configure
|
|
673
|
+
*/
|
|
674
|
+
const ALL_CONFIG_OPTIONS = {
|
|
675
|
+
claudeMdReinforcement: {
|
|
676
|
+
since: '2.92.0',
|
|
677
|
+
description: 'Add /babysit rules to CLAUDE.md',
|
|
678
|
+
autoApplyable: true,
|
|
679
|
+
},
|
|
680
|
+
sessionStartHook: {
|
|
681
|
+
since: '2.35.0',
|
|
682
|
+
description: 'Welcome display on session start',
|
|
683
|
+
autoApplyable: false,
|
|
684
|
+
},
|
|
685
|
+
precompactHook: {
|
|
686
|
+
since: '2.40.0',
|
|
687
|
+
description: 'Context preservation during /compact',
|
|
688
|
+
autoApplyable: false,
|
|
689
|
+
},
|
|
690
|
+
damageControlHooks: {
|
|
691
|
+
since: '2.50.0',
|
|
692
|
+
description: 'Block destructive commands',
|
|
693
|
+
autoApplyable: false,
|
|
694
|
+
},
|
|
695
|
+
statusLine: { since: '2.35.0', description: 'Custom status bar display', autoApplyable: false },
|
|
696
|
+
autoArchival: {
|
|
697
|
+
since: '2.35.0',
|
|
698
|
+
description: 'Auto-archive completed stories',
|
|
699
|
+
autoApplyable: false,
|
|
700
|
+
},
|
|
701
|
+
autoUpdate: {
|
|
702
|
+
since: '2.70.0',
|
|
703
|
+
description: 'Auto-update on session start',
|
|
704
|
+
autoApplyable: false,
|
|
705
|
+
},
|
|
706
|
+
ralphLoop: { since: '2.60.0', description: 'Autonomous story processing', autoApplyable: false },
|
|
707
|
+
tmuxAutoSpawn: {
|
|
708
|
+
since: '2.92.0',
|
|
709
|
+
description: 'Auto-start Claude in tmux session',
|
|
710
|
+
autoApplyable: true,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Check for new config options that haven't been presented to the user
|
|
716
|
+
* Returns info about outdated config and whether to auto-apply (for "full" profile)
|
|
717
|
+
*/
|
|
718
|
+
function checkConfigStaleness(rootDir, currentVersion, cache = null) {
|
|
719
|
+
const result = {
|
|
720
|
+
outdated: false,
|
|
721
|
+
newOptionsCount: 0,
|
|
722
|
+
newOptions: [],
|
|
723
|
+
configSchemaVersion: null,
|
|
724
|
+
activeProfile: null,
|
|
725
|
+
autoApply: false,
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
const metadata = cache?.metadata;
|
|
730
|
+
if (!metadata) return result;
|
|
731
|
+
|
|
732
|
+
result.configSchemaVersion = metadata.config_schema_version || null;
|
|
733
|
+
result.activeProfile = metadata.active_profile || null;
|
|
734
|
+
|
|
735
|
+
const configOptions = metadata.agileflow?.config_options || {};
|
|
736
|
+
|
|
737
|
+
// If no config_schema_version, this is an old installation - all options are "new"
|
|
738
|
+
if (!result.configSchemaVersion) {
|
|
739
|
+
// For old installations, detect which features are actually configured via settings.json
|
|
740
|
+
const settings = cache?.settings || {};
|
|
741
|
+
const hooks = settings.hooks || {};
|
|
742
|
+
|
|
743
|
+
// Check each option against actual configuration
|
|
744
|
+
for (const [name, optionInfo] of Object.entries(ALL_CONFIG_OPTIONS)) {
|
|
745
|
+
const isConfigured = isOptionActuallyConfigured(name, hooks, settings);
|
|
746
|
+
if (!isConfigured) {
|
|
747
|
+
result.outdated = true;
|
|
748
|
+
result.newOptionsCount++;
|
|
749
|
+
result.newOptions.push({
|
|
750
|
+
name,
|
|
751
|
+
description: optionInfo.description,
|
|
752
|
+
autoApplyable: optionInfo.autoApplyable,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
// Check for unconfigured options in metadata
|
|
758
|
+
for (const [name, option] of Object.entries(configOptions)) {
|
|
759
|
+
if (option.configured === false) {
|
|
760
|
+
const optionInfo = ALL_CONFIG_OPTIONS[name] || {
|
|
761
|
+
description: name,
|
|
762
|
+
autoApplyable: false,
|
|
763
|
+
};
|
|
764
|
+
result.outdated = true;
|
|
765
|
+
result.newOptionsCount++;
|
|
766
|
+
result.newOptions.push({
|
|
767
|
+
name,
|
|
768
|
+
description: optionInfo.description,
|
|
769
|
+
autoApplyable: optionInfo.autoApplyable,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Check for options that might not be in metadata yet (added after their install)
|
|
775
|
+
for (const [name, optionInfo] of Object.entries(ALL_CONFIG_OPTIONS)) {
|
|
776
|
+
if (!configOptions[name]) {
|
|
777
|
+
// Option doesn't exist in metadata - check if it was added after their config_schema_version
|
|
778
|
+
if (compareVersions(result.configSchemaVersion, optionInfo.since) < 0) {
|
|
779
|
+
const alreadyAdded = result.newOptions.some(o => o.name === name);
|
|
780
|
+
if (!alreadyAdded) {
|
|
781
|
+
result.outdated = true;
|
|
782
|
+
result.newOptionsCount++;
|
|
783
|
+
result.newOptions.push({
|
|
784
|
+
name,
|
|
785
|
+
description: optionInfo.description,
|
|
786
|
+
autoApplyable: optionInfo.autoApplyable,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// If profile is "full", auto-apply features that support it
|
|
795
|
+
if (result.outdated && result.activeProfile === 'full') {
|
|
796
|
+
const autoApplyableOptions = result.newOptions.filter(o => o.autoApplyable);
|
|
797
|
+
if (autoApplyableOptions.length > 0) {
|
|
798
|
+
result.autoApply = true;
|
|
799
|
+
// Only auto-apply the auto-applyable ones
|
|
800
|
+
result.autoApplyOptions = autoApplyableOptions;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch (e) {
|
|
804
|
+
// Silently fail - config check is non-critical
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Check if a config option is actually configured in settings
|
|
812
|
+
*/
|
|
813
|
+
function isOptionActuallyConfigured(optionName, hooks, settings) {
|
|
814
|
+
switch (optionName) {
|
|
815
|
+
case 'sessionStartHook':
|
|
816
|
+
return hooks.SessionStart && hooks.SessionStart.length > 0;
|
|
817
|
+
case 'precompactHook':
|
|
818
|
+
return hooks.PreCompact && hooks.PreCompact.length > 0;
|
|
819
|
+
case 'damageControlHooks':
|
|
820
|
+
return (
|
|
821
|
+
hooks.PreToolUse &&
|
|
822
|
+
hooks.PreToolUse.some(h => h.hooks?.some(hk => hk.command?.includes('damage-control')))
|
|
823
|
+
);
|
|
824
|
+
case 'statusLine':
|
|
825
|
+
return settings.statusLine && settings.statusLine.command;
|
|
826
|
+
case 'autoArchival':
|
|
827
|
+
// Archival is tied to SessionStart hook running archive script
|
|
828
|
+
return (
|
|
829
|
+
hooks.SessionStart &&
|
|
830
|
+
hooks.SessionStart.some(h => h.hooks?.some(hk => hk.command?.includes('archive')))
|
|
831
|
+
);
|
|
832
|
+
case 'autoUpdate':
|
|
833
|
+
// Would need to check metadata for autoUpdate setting
|
|
834
|
+
return false; // Default to not configured
|
|
835
|
+
case 'ralphLoop':
|
|
836
|
+
return (
|
|
837
|
+
hooks.Stop && hooks.Stop.some(h => h.hooks?.some(hk => hk.command?.includes('ralph-loop')))
|
|
838
|
+
);
|
|
839
|
+
case 'claudeMdReinforcement':
|
|
840
|
+
// Check if CLAUDE.md has the marker - can't easily check from here
|
|
841
|
+
return false; // Let welcome script handle this
|
|
842
|
+
case 'tmuxAutoSpawn':
|
|
843
|
+
// Check metadata for tmuxAutoSpawn setting (default is enabled)
|
|
844
|
+
return false; // Let welcome script handle this via metadata
|
|
845
|
+
default:
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Auto-apply new config options for "full" profile
|
|
852
|
+
* Returns true if any options were applied
|
|
853
|
+
*/
|
|
854
|
+
function autoApplyConfigOptions(rootDir, newOptions) {
|
|
855
|
+
let applied = 0;
|
|
856
|
+
|
|
857
|
+
for (const option of newOptions) {
|
|
858
|
+
try {
|
|
859
|
+
if (option.name === 'claudeMdReinforcement') {
|
|
860
|
+
// Apply CLAUDE.md reinforcement
|
|
861
|
+
const claudeMdPath = path.join(rootDir, 'CLAUDE.md');
|
|
862
|
+
const marker = '<!-- AGILEFLOW_BABYSIT_RULES -->';
|
|
863
|
+
const content = `
|
|
864
|
+
|
|
865
|
+
${marker}
|
|
866
|
+
## AgileFlow /babysit Context Preservation Rules
|
|
867
|
+
|
|
868
|
+
When \`/agileflow:babysit\` is active (check session-state.json), these rules are MANDATORY:
|
|
869
|
+
|
|
870
|
+
1. **ALWAYS end responses with the AskUserQuestion tool** - Not text like "What next?" but the ACTUAL TOOL CALL
|
|
871
|
+
2. **Use Plan Mode for non-trivial tasks** - Call \`EnterPlanMode\` before complex implementations
|
|
872
|
+
3. **Delegate complex work to domain experts** - Use \`Task\` tool with appropriate \`subagent_type\`
|
|
873
|
+
4. **Track progress with TodoWrite** - For any task with 3+ steps
|
|
874
|
+
|
|
875
|
+
These rules persist across conversation compaction. Check \`docs/09-agents/session-state.json\` for active commands.
|
|
876
|
+
${marker}
|
|
877
|
+
`;
|
|
878
|
+
|
|
879
|
+
let existingContent = '';
|
|
880
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
881
|
+
existingContent = fs.readFileSync(claudeMdPath, 'utf8');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!existingContent.includes(marker)) {
|
|
885
|
+
fs.appendFileSync(claudeMdPath, content);
|
|
886
|
+
applied++;
|
|
887
|
+
}
|
|
888
|
+
} else if (option.name === 'tmuxAutoSpawn') {
|
|
889
|
+
// Auto-enable tmux auto-spawn via metadata
|
|
890
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
891
|
+
if (fs.existsSync(metadataPath)) {
|
|
892
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
893
|
+
if (!metadata.features) metadata.features = {};
|
|
894
|
+
if (
|
|
895
|
+
!metadata.features.tmuxAutoSpawn ||
|
|
896
|
+
metadata.features.tmuxAutoSpawn.enabled === undefined
|
|
897
|
+
) {
|
|
898
|
+
metadata.features.tmuxAutoSpawn = {
|
|
899
|
+
enabled: true,
|
|
900
|
+
version: metadata.version || '2.92.0',
|
|
901
|
+
at: new Date().toISOString(),
|
|
902
|
+
};
|
|
903
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
904
|
+
applied++;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Add more option handlers here as new options are added
|
|
909
|
+
} catch (e) {
|
|
910
|
+
// Silently fail individual options
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Update metadata to mark options as configured
|
|
915
|
+
if (applied > 0) {
|
|
916
|
+
try {
|
|
917
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
918
|
+
if (fs.existsSync(metadataPath)) {
|
|
919
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
920
|
+
|
|
921
|
+
// Get current CLI version for updating config_schema_version
|
|
922
|
+
let currentVersion = '2.92.0';
|
|
923
|
+
try {
|
|
924
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
925
|
+
if (fs.existsSync(pkgPath)) {
|
|
926
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
927
|
+
currentVersion = pkg.version;
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {}
|
|
930
|
+
|
|
931
|
+
// Update config_schema_version
|
|
932
|
+
metadata.config_schema_version = currentVersion;
|
|
933
|
+
|
|
934
|
+
// Mark applied options as configured
|
|
935
|
+
if (!metadata.agileflow) metadata.agileflow = {};
|
|
936
|
+
if (!metadata.agileflow.config_options) metadata.agileflow.config_options = {};
|
|
937
|
+
|
|
938
|
+
for (const option of newOptions) {
|
|
939
|
+
metadata.agileflow.config_options[option.name] = {
|
|
940
|
+
...metadata.agileflow.config_options[option.name],
|
|
941
|
+
configured: true,
|
|
942
|
+
enabled: true,
|
|
943
|
+
configured_at: new Date().toISOString(),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
|
|
948
|
+
}
|
|
949
|
+
} catch (e) {
|
|
950
|
+
// Silently fail metadata update
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return applied;
|
|
955
|
+
}
|
|
956
|
+
|
|
594
957
|
// Check for updates (async but we'll use sync approach for welcome)
|
|
595
958
|
async function checkUpdates() {
|
|
596
959
|
const result = {
|
|
@@ -718,7 +1081,7 @@ function getExpertiseCountFast(rootDir) {
|
|
|
718
1081
|
const result = { total: 0, passed: 0, warnings: 0, failed: 0, issues: [], validated: false };
|
|
719
1082
|
|
|
720
1083
|
// Find experts directory
|
|
721
|
-
let expertsDir = path.join(rootDir, '
|
|
1084
|
+
let expertsDir = path.join(getAgileflowDir(rootDir), 'experts');
|
|
722
1085
|
if (!fs.existsSync(expertsDir)) {
|
|
723
1086
|
expertsDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'experts');
|
|
724
1087
|
}
|
|
@@ -781,7 +1144,7 @@ function validateExpertise(rootDir) {
|
|
|
781
1144
|
const result = { total: 0, passed: 0, warnings: 0, failed: 0, issues: [] };
|
|
782
1145
|
|
|
783
1146
|
// Find experts directory
|
|
784
|
-
let expertsDir = path.join(rootDir, '
|
|
1147
|
+
let expertsDir = path.join(getAgileflowDir(rootDir), 'experts');
|
|
785
1148
|
if (!fs.existsSync(expertsDir)) {
|
|
786
1149
|
expertsDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'experts');
|
|
787
1150
|
}
|
|
@@ -874,7 +1237,7 @@ function getFeatureVersions(rootDir) {
|
|
|
874
1237
|
};
|
|
875
1238
|
|
|
876
1239
|
try {
|
|
877
|
-
const metadataPath =
|
|
1240
|
+
const metadataPath = getMetadataPath(rootDir);
|
|
878
1241
|
if (fs.existsSync(metadataPath)) {
|
|
879
1242
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
880
1243
|
|
|
@@ -1268,6 +1631,35 @@ async function main() {
|
|
|
1268
1631
|
// Update check failed - continue without it
|
|
1269
1632
|
}
|
|
1270
1633
|
|
|
1634
|
+
// Check for new config options
|
|
1635
|
+
let configStaleness = { outdated: false, autoApply: false };
|
|
1636
|
+
let configAutoApplied = 0;
|
|
1637
|
+
try {
|
|
1638
|
+
configStaleness = checkConfigStaleness(rootDir, info.version, cache);
|
|
1639
|
+
|
|
1640
|
+
// Auto-apply new options if profile is "full" (only auto-applyable ones)
|
|
1641
|
+
if (configStaleness.autoApply && configStaleness.autoApplyOptions?.length > 0) {
|
|
1642
|
+
configAutoApplied = autoApplyConfigOptions(rootDir, configStaleness.autoApplyOptions);
|
|
1643
|
+
if (configAutoApplied > 0) {
|
|
1644
|
+
// Remove auto-applied options from the list, keep non-auto-applyable ones
|
|
1645
|
+
configStaleness.newOptions = configStaleness.newOptions.filter(o => !o.autoApplyable);
|
|
1646
|
+
configStaleness.newOptionsCount = configStaleness.newOptions.length;
|
|
1647
|
+
if (configStaleness.newOptionsCount === 0) {
|
|
1648
|
+
configStaleness.outdated = false;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
// Config check failed - continue without it
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Check tmux availability (only if tmuxAutoSpawn is enabled)
|
|
1657
|
+
let tmuxCheck = { available: true };
|
|
1658
|
+
const tmuxAutoSpawnEnabled = cache?.metadata?.features?.tmuxAutoSpawn?.enabled !== false;
|
|
1659
|
+
if (tmuxAutoSpawnEnabled) {
|
|
1660
|
+
tmuxCheck = checkTmuxAvailability();
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1271
1663
|
// Show session banner FIRST if in a non-main session
|
|
1272
1664
|
const sessionBanner = formatSessionBanner(parallelSessions);
|
|
1273
1665
|
if (sessionBanner) {
|
|
@@ -1287,6 +1679,54 @@ async function main() {
|
|
|
1287
1679
|
)
|
|
1288
1680
|
);
|
|
1289
1681
|
|
|
1682
|
+
// Show config auto-apply confirmation (for "full" profile)
|
|
1683
|
+
if (configAutoApplied > 0) {
|
|
1684
|
+
console.log('');
|
|
1685
|
+
console.log(
|
|
1686
|
+
`${c.mintGreen}✨ Auto-applied ${configAutoApplied} new config option(s)${c.reset}`
|
|
1687
|
+
);
|
|
1688
|
+
console.log(` ${c.slate}Profile "full" enables all new features automatically.${c.reset}`);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Show config staleness notification (for custom profiles)
|
|
1692
|
+
if (configStaleness.outdated && configStaleness.newOptionsCount > 0) {
|
|
1693
|
+
console.log('');
|
|
1694
|
+
console.log(
|
|
1695
|
+
`${c.amber}⚙️ ${configStaleness.newOptionsCount} new configuration option(s) available${c.reset}`
|
|
1696
|
+
);
|
|
1697
|
+
for (const opt of configStaleness.newOptions.slice(0, 3)) {
|
|
1698
|
+
console.log(` ${c.dim}• ${opt.description}${c.reset}`);
|
|
1699
|
+
}
|
|
1700
|
+
console.log(
|
|
1701
|
+
` ${c.slate}Run ${c.skyBlue}/agileflow:configure${c.reset}${c.slate} to enable them.${c.reset}`
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// Show tmux installation notice if tmux auto-spawn is enabled but tmux not installed
|
|
1706
|
+
if (tmuxAutoSpawnEnabled && !tmuxCheck.available) {
|
|
1707
|
+
console.log('');
|
|
1708
|
+
console.log(
|
|
1709
|
+
`${c.amber}📦 tmux not installed${c.reset} ${c.dim}(enables parallel sessions in one terminal)${c.reset}`
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
// Show platform-specific install command
|
|
1713
|
+
if (tmuxCheck.platform?.installCmd) {
|
|
1714
|
+
console.log(` ${c.slate}Install for ${tmuxCheck.platform.os}:${c.reset}`);
|
|
1715
|
+
console.log(` ${c.cyan}${tmuxCheck.platform.installCmd}${c.reset}`);
|
|
1716
|
+
console.log('');
|
|
1717
|
+
console.log(` ${c.dim}No sudo? Use: ${c.cyan}${tmuxCheck.noSudoCmd}${c.reset}`);
|
|
1718
|
+
} else {
|
|
1719
|
+
// Unknown platform - show all options
|
|
1720
|
+
console.log(` ${c.slate}Install with one of:${c.reset}`);
|
|
1721
|
+
console.log(` ${c.dim}• macOS:${c.reset} ${c.cyan}brew install tmux${c.reset}`);
|
|
1722
|
+
console.log(` ${c.dim}• Ubuntu:${c.reset} ${c.cyan}sudo apt install tmux${c.reset}`);
|
|
1723
|
+
console.log(` ${c.dim}• No sudo:${c.reset} ${c.cyan}${tmuxCheck.noSudoCmd}${c.reset}`);
|
|
1724
|
+
}
|
|
1725
|
+
console.log(
|
|
1726
|
+
` ${c.dim}Or disable this notice: ${c.skyBlue}/agileflow:configure --disable=tmuxautospawn${c.reset}`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1290
1730
|
// Show warning and tip if other sessions are active (vibrant colors)
|
|
1291
1731
|
if (parallelSessions.otherActive > 0) {
|
|
1292
1732
|
console.log('');
|
|
@@ -1360,6 +1800,34 @@ async function main() {
|
|
|
1360
1800
|
// Silently ignore file tracking errors
|
|
1361
1801
|
}
|
|
1362
1802
|
}
|
|
1803
|
+
|
|
1804
|
+
// Epic completion check: auto-complete epics where all stories are done
|
|
1805
|
+
if (storyStateMachine && cache.status) {
|
|
1806
|
+
try {
|
|
1807
|
+
const statusPath = getStatusPath(rootDir);
|
|
1808
|
+
const statusData = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
1809
|
+
const incompleteEpics = storyStateMachine.findIncompleteEpics(statusData);
|
|
1810
|
+
|
|
1811
|
+
if (incompleteEpics.length > 0) {
|
|
1812
|
+
let autoCompleted = 0;
|
|
1813
|
+
for (const { epicId, completed, total } of incompleteEpics) {
|
|
1814
|
+
const result = storyStateMachine.autoCompleteEpic(statusData, epicId);
|
|
1815
|
+
if (result.updated) {
|
|
1816
|
+
autoCompleted++;
|
|
1817
|
+
console.log('');
|
|
1818
|
+
console.log(
|
|
1819
|
+
`${c.mintGreen}✅ Auto-completed ${c.bold}${epicId}${c.reset}${c.mintGreen} (${completed}/${total} stories done)${c.reset}`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
if (autoCompleted > 0) {
|
|
1824
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2) + '\n');
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
// Silently ignore epic completion errors
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1363
1831
|
}
|
|
1364
1832
|
|
|
1365
1833
|
main().catch(console.error);
|