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.
Files changed (100) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +32 -23
  5. package/lib/colors.js +190 -12
  6. package/lib/consent.js +232 -0
  7. package/lib/correlation.js +277 -0
  8. package/lib/error-codes.js +46 -0
  9. package/lib/errors.js +48 -6
  10. package/lib/file-cache.js +182 -0
  11. package/lib/format-error.js +156 -0
  12. package/lib/path-resolver.js +155 -7
  13. package/lib/paths.js +212 -20
  14. package/lib/placeholder-registry.js +205 -0
  15. package/lib/registry-di.js +358 -0
  16. package/lib/result-schema.js +363 -0
  17. package/lib/result.js +210 -0
  18. package/lib/session-registry.js +13 -0
  19. package/lib/session-state-machine.js +465 -0
  20. package/lib/validate-commands.js +308 -0
  21. package/lib/validate.js +116 -52
  22. package/package.json +1 -1
  23. package/scripts/af +34 -0
  24. package/scripts/agent-loop.js +63 -9
  25. package/scripts/agileflow-configure.js +2 -2
  26. package/scripts/agileflow-welcome.js +491 -23
  27. package/scripts/archive-completed-stories.sh +57 -11
  28. package/scripts/claude-tmux.sh +102 -0
  29. package/scripts/damage-control-bash.js +3 -70
  30. package/scripts/damage-control-edit.js +3 -20
  31. package/scripts/damage-control-write.js +3 -20
  32. package/scripts/dependency-check.js +310 -0
  33. package/scripts/get-env.js +11 -4
  34. package/scripts/lib/configure-detect.js +23 -1
  35. package/scripts/lib/configure-features.js +50 -2
  36. package/scripts/lib/context-formatter.js +771 -0
  37. package/scripts/lib/context-loader.js +699 -0
  38. package/scripts/lib/damage-control-utils.js +107 -0
  39. package/scripts/lib/json-utils.sh +162 -0
  40. package/scripts/lib/state-migrator.js +353 -0
  41. package/scripts/lib/story-state-machine.js +437 -0
  42. package/scripts/obtain-context.js +80 -1248
  43. package/scripts/pre-push-check.sh +46 -0
  44. package/scripts/precompact-context.sh +23 -10
  45. package/scripts/query-codebase.js +127 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +408 -55
  48. package/scripts/spawn-parallel.js +666 -0
  49. package/scripts/tui/blessed/data/watcher.js +20 -15
  50. package/scripts/tui/blessed/index.js +2 -2
  51. package/scripts/tui/blessed/panels/output.js +14 -8
  52. package/scripts/tui/blessed/panels/sessions.js +22 -15
  53. package/scripts/tui/blessed/panels/trace.js +14 -8
  54. package/scripts/tui/blessed/ui/help.js +3 -3
  55. package/scripts/tui/blessed/ui/screen.js +4 -4
  56. package/scripts/tui/blessed/ui/statusbar.js +5 -9
  57. package/scripts/tui/blessed/ui/tabbar.js +11 -11
  58. package/scripts/validators/component-validator.js +41 -14
  59. package/scripts/validators/json-schema-validator.js +11 -4
  60. package/scripts/validators/markdown-validator.js +1 -2
  61. package/scripts/validators/migration-validator.js +17 -5
  62. package/scripts/validators/security-validator.js +137 -33
  63. package/scripts/validators/story-format-validator.js +31 -10
  64. package/scripts/validators/test-result-validator.js +19 -4
  65. package/scripts/validators/workflow-validator.js +12 -5
  66. package/src/core/agents/codebase-query.md +24 -0
  67. package/src/core/commands/adr.md +114 -0
  68. package/src/core/commands/agent.md +120 -0
  69. package/src/core/commands/assign.md +145 -0
  70. package/src/core/commands/babysit.md +32 -5
  71. package/src/core/commands/changelog.md +118 -0
  72. package/src/core/commands/configure.md +42 -6
  73. package/src/core/commands/diagnose.md +114 -0
  74. package/src/core/commands/epic.md +113 -0
  75. package/src/core/commands/handoff.md +128 -0
  76. package/src/core/commands/help.md +75 -0
  77. package/src/core/commands/pr.md +96 -0
  78. package/src/core/commands/roadmap/analyze.md +400 -0
  79. package/src/core/commands/session/new.md +132 -6
  80. package/src/core/commands/session/spawn.md +197 -0
  81. package/src/core/commands/sprint.md +22 -0
  82. package/src/core/commands/status.md +74 -0
  83. package/src/core/commands/story.md +143 -4
  84. package/src/core/templates/agileflow-metadata.json +55 -2
  85. package/src/core/templates/plan-template.md +125 -0
  86. package/src/core/templates/story-lifecycle.md +213 -0
  87. package/src/core/templates/story-template.md +4 -0
  88. package/src/core/templates/tdd-test-template.js +241 -0
  89. package/tools/cli/commands/setup.js +95 -0
  90. package/tools/cli/installers/core/installer.js +94 -0
  91. package/tools/cli/installers/ide/_base-ide.js +20 -11
  92. package/tools/cli/installers/ide/codex.js +29 -47
  93. package/tools/cli/installers/ide/windsurf.js +1 -1
  94. package/tools/cli/lib/config-manager.js +17 -2
  95. package/tools/cli/lib/content-transformer.js +271 -0
  96. package/tools/cli/lib/error-handler.js +14 -22
  97. package/tools/cli/lib/ide-error-factory.js +421 -0
  98. package/tools/cli/lib/ide-health-monitor.js +364 -0
  99. package/tools/cli/lib/ide-registry.js +113 -2
  100. 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 { getProjectRoot } = require('../lib/paths');
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: path.join(rootDir, 'docs', '09-agents', 'status.json'),
59
- metadata: path.join(rootDir, 'docs', '00-meta', 'agileflow-metadata.json'),
60
- settings: path.join(rootDir, '.claude', 'settings.json'),
61
- sessionState: path.join(rootDir, 'docs', '09-agents', 'session-state.json'),
62
- configYaml: path.join(rootDir, '.agileflow', 'config.yaml'),
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, '.agileflow', 'config.yaml');
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 = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
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 = path.join(rootDir, 'docs/09-agents/status.json');
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 = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
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 = path.join(rootDir, 'docs/09-agents/status.json');
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 = path.join(rootDir, 'docs/09-agents/session-state.json');
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, '.agileflow', 'scripts', 'session-manager.js');
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, '.claude/settings.json');
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/precompact-context.sh');
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 = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
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, '.claude/settings.json');
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, '.claude', 'hooks', 'damage-control');
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, '.claude', 'hooks', 'damage-control', 'patterns.yaml'),
564
- path.join(rootDir, '.agileflow', 'scripts', 'damage-control', 'patterns.yaml'),
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, '.agileflow', 'experts');
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, '.agileflow', 'experts');
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 = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
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);