agileflow 3.1.0 → 3.2.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 (106) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +57 -85
  3. package/lib/dashboard-automations.js +130 -0
  4. package/lib/dashboard-git.js +254 -0
  5. package/lib/dashboard-inbox.js +64 -0
  6. package/lib/dashboard-protocol.js +1 -0
  7. package/lib/dashboard-server.js +114 -924
  8. package/lib/dashboard-session.js +136 -0
  9. package/lib/dashboard-status.js +72 -0
  10. package/lib/dashboard-terminal.js +354 -0
  11. package/lib/dashboard-websocket.js +88 -0
  12. package/lib/drivers/codex-driver.ts +4 -4
  13. package/lib/logger.js +106 -0
  14. package/package.json +4 -2
  15. package/scripts/agileflow-configure.js +2 -2
  16. package/scripts/agileflow-welcome.js +409 -434
  17. package/scripts/claude-tmux.sh +80 -2
  18. package/scripts/context-loader.js +4 -9
  19. package/scripts/lib/browser-qa-evidence.js +409 -0
  20. package/scripts/lib/browser-qa-status.js +192 -0
  21. package/scripts/lib/command-prereqs.js +280 -0
  22. package/scripts/lib/configure-detect.js +92 -2
  23. package/scripts/lib/configure-features.js +295 -1
  24. package/scripts/lib/context-formatter.js +468 -233
  25. package/scripts/lib/context-loader.js +27 -15
  26. package/scripts/lib/damage-control-utils.js +8 -1
  27. package/scripts/lib/feature-catalog.js +321 -0
  28. package/scripts/lib/portable-tasks-cli.js +274 -0
  29. package/scripts/lib/portable-tasks.js +479 -0
  30. package/scripts/lib/signal-detectors.js +1 -1
  31. package/scripts/lib/team-events.js +86 -1
  32. package/scripts/obtain-context.js +28 -4
  33. package/scripts/smart-detect.js +17 -0
  34. package/scripts/strip-ai-attribution.js +63 -0
  35. package/scripts/team-manager.js +7 -2
  36. package/scripts/welcome-deferred.js +437 -0
  37. package/src/core/agents/browser-qa.md +328 -0
  38. package/src/core/agents/perf-analyzer-assets.md +174 -0
  39. package/src/core/agents/perf-analyzer-bundle.md +165 -0
  40. package/src/core/agents/perf-analyzer-caching.md +160 -0
  41. package/src/core/agents/perf-analyzer-compute.md +165 -0
  42. package/src/core/agents/perf-analyzer-memory.md +182 -0
  43. package/src/core/agents/perf-analyzer-network.md +157 -0
  44. package/src/core/agents/perf-analyzer-queries.md +155 -0
  45. package/src/core/agents/perf-analyzer-rendering.md +156 -0
  46. package/src/core/agents/perf-consensus.md +280 -0
  47. package/src/core/agents/security-analyzer-api.md +199 -0
  48. package/src/core/agents/security-analyzer-auth.md +160 -0
  49. package/src/core/agents/security-analyzer-authz.md +168 -0
  50. package/src/core/agents/security-analyzer-deps.md +147 -0
  51. package/src/core/agents/security-analyzer-infra.md +176 -0
  52. package/src/core/agents/security-analyzer-injection.md +148 -0
  53. package/src/core/agents/security-analyzer-input.md +191 -0
  54. package/src/core/agents/security-analyzer-secrets.md +175 -0
  55. package/src/core/agents/security-consensus.md +276 -0
  56. package/src/core/agents/test-analyzer-assertions.md +181 -0
  57. package/src/core/agents/test-analyzer-coverage.md +183 -0
  58. package/src/core/agents/test-analyzer-fragility.md +185 -0
  59. package/src/core/agents/test-analyzer-integration.md +155 -0
  60. package/src/core/agents/test-analyzer-maintenance.md +173 -0
  61. package/src/core/agents/test-analyzer-mocking.md +178 -0
  62. package/src/core/agents/test-analyzer-patterns.md +189 -0
  63. package/src/core/agents/test-analyzer-structure.md +177 -0
  64. package/src/core/agents/test-consensus.md +294 -0
  65. package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
  66. package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
  67. package/src/core/commands/audit/performance.md +443 -0
  68. package/src/core/commands/audit/security.md +443 -0
  69. package/src/core/commands/audit/test.md +442 -0
  70. package/src/core/commands/babysit.md +505 -463
  71. package/src/core/commands/browser-qa.md +240 -0
  72. package/src/core/commands/configure.md +8 -8
  73. package/src/core/commands/research/ask.md +42 -9
  74. package/src/core/commands/research/import.md +14 -8
  75. package/src/core/commands/research/list.md +17 -16
  76. package/src/core/commands/research/synthesize.md +8 -8
  77. package/src/core/commands/research/view.md +28 -4
  78. package/src/core/commands/whats-new.md +2 -2
  79. package/src/core/experts/devops/expertise.yaml +13 -2
  80. package/src/core/experts/documentation/expertise.yaml +26 -4
  81. package/src/core/profiles/COMPARISON.md +170 -0
  82. package/src/core/profiles/README.md +178 -0
  83. package/src/core/profiles/claude-code.yaml +111 -0
  84. package/src/core/profiles/codex.yaml +103 -0
  85. package/src/core/profiles/cursor.yaml +134 -0
  86. package/src/core/profiles/examples.js +250 -0
  87. package/src/core/profiles/loader.js +235 -0
  88. package/src/core/profiles/windsurf.yaml +159 -0
  89. package/src/core/teams/logic-audit.json +6 -0
  90. package/src/core/teams/perf-audit.json +71 -0
  91. package/src/core/teams/security-audit.json +71 -0
  92. package/src/core/teams/test-audit.json +71 -0
  93. package/src/core/templates/browser-qa-spec.yaml +94 -0
  94. package/src/core/templates/command-prerequisites.yaml +169 -0
  95. package/src/core/templates/damage-control-patterns.yaml +9 -0
  96. package/tools/cli/installers/ide/_base-ide.js +33 -3
  97. package/tools/cli/installers/ide/claude-code.js +2 -69
  98. package/tools/cli/installers/ide/codex.js +9 -9
  99. package/tools/cli/installers/ide/cursor.js +165 -4
  100. package/tools/cli/installers/ide/windsurf.js +237 -6
  101. package/tools/cli/lib/content-transformer.js +234 -9
  102. package/tools/cli/lib/docs-setup.js +1 -1
  103. package/tools/cli/lib/ide-generator.js +357 -0
  104. package/tools/cli/lib/ide-registry.js +2 -2
  105. package/scripts/tmux-task-name.sh +0 -105
  106. package/scripts/tmux-task-watcher.sh +0 -344
@@ -9,6 +9,17 @@
9
9
  * - Archival status
10
10
  * - Session cleanup status
11
11
  * - Last commit
12
+ *
13
+ * PERFORMANCE OPTIMIZATION (US-0356):
14
+ * Phase 1: Table display + instant notifications (~300-350ms)
15
+ * Phase 2: Cached update check + instant post-table notifications
16
+ * Phase 3: Background deferred work via welcome-deferred.js
17
+ * - npm update check (with cache write)
18
+ * - Session health warnings
19
+ * - Duplicate process detection
20
+ * - Story claiming/file tracking cleanup
21
+ * - Epic completion, ideation sync, automations
22
+ * Deferred warnings saved to session-state.json, displayed next session.
12
23
  */
13
24
 
14
25
  const fs = require('fs');
@@ -26,22 +37,34 @@ const {
26
37
  getClaudeDir,
27
38
  } = require('../lib/paths');
28
39
  const { readJSONCached, readFileCached } = require('../lib/file-cache');
29
-
30
- // Session manager path (relative to script location)
31
- const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
32
-
33
- // PERFORMANCE OPTIMIZATION: Lazy-loaded session-manager module
34
- // Importing directly avoids ~50-150ms subprocess overhead per call.
35
- let _sessionManager;
36
- function getSessionManager() {
37
- if (_sessionManager === undefined) {
38
- try {
39
- _sessionManager = require('./session-manager.js');
40
- } catch (e) {
41
- _sessionManager = null;
42
- }
40
+ const { tryOptional } = require('../lib/errors');
41
+ const { createLogger } = require('../lib/logger');
42
+ const log = createLogger('welcome');
43
+
44
+ // PERFORMANCE OPTIMIZATION (US-0356): Profiling helper
45
+ // Only active when AGILEFLOW_DEBUG=welcome is set
46
+ const _profiling = process.env.AGILEFLOW_DEBUG === 'welcome';
47
+ const _timings = {};
48
+ function _mark(label) {
49
+ if (_profiling) _timings[label] = process.hrtime.bigint();
50
+ }
51
+ function _elapsed(from, to) {
52
+ if (!_profiling) return '';
53
+ const ns = Number((_timings[to] || process.hrtime.bigint()) - (_timings[from] || 0n));
54
+ return `${(ns / 1e6).toFixed(1)}ms`;
55
+ }
56
+ function _logTimings() {
57
+ if (!_profiling) return;
58
+ const labels = Object.keys(_timings);
59
+ const pairs = [];
60
+ for (let i = 1; i < labels.length; i++) {
61
+ pairs.push(`${labels[i]}=${_elapsed(labels[i - 1], labels[i])}`);
43
62
  }
44
- return _sessionManager;
63
+ if (labels.length >= 2) {
64
+ pairs.push(`TOTAL=${_elapsed(labels[0], labels[labels.length - 1])}`);
65
+ }
66
+ // Output to stderr so it doesn't mix with the welcome table
67
+ console.error(`[welcome:timing] ${pairs.join(' ')}`);
45
68
  }
46
69
 
47
70
  // Hook metrics module (kept at top level - needed early for timer)
@@ -136,7 +159,11 @@ function checkTmuxAvailability(cache) {
136
159
  // Check session state cache first (tmux availability doesn't change within a session)
137
160
  if (cache?.sessionState?.tmux_available !== undefined) {
138
161
  if (cache.sessionState.tmux_available) return { available: true };
139
- return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
162
+ return {
163
+ available: false,
164
+ platform: detectPlatform(),
165
+ noSudoCmd: 'conda install -c conda-forge tmux',
166
+ };
140
167
  }
141
168
 
142
169
  // Actually check (first run or no cache)
@@ -157,7 +184,11 @@ function checkTmuxAvailability(cache) {
157
184
  }
158
185
 
159
186
  if (available) return { available: true };
160
- return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
187
+ return {
188
+ available: false,
189
+ platform: detectPlatform(),
190
+ noSudoCmd: 'conda install -c conda-forge tmux',
191
+ };
161
192
  }
162
193
 
163
194
  /**
@@ -289,7 +320,9 @@ function getProjectInfo(rootDir, cache = null) {
289
320
  }
290
321
  }
291
322
  }
292
- } catch (e) {}
323
+ } catch (e) {
324
+ log.debug('getProjectInfo:', e?.message || String(e));
325
+ }
293
326
 
294
327
  return info;
295
328
  }
@@ -353,7 +386,9 @@ function runArchival(rootDir, cache = null) {
353
386
  result.remaining -= toArchiveCount;
354
387
  }
355
388
  }
356
- } catch (e) {}
389
+ } catch (e) {
390
+ log.debug('runArchival:', e?.message || String(e));
391
+ }
357
392
 
358
393
  return result;
359
394
  }
@@ -426,20 +461,31 @@ function clearActiveCommands(rootDir, cache = null) {
426
461
  if (result.cleared > 0) {
427
462
  fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
428
463
  }
429
- } catch (e) {}
464
+ } catch (e) {
465
+ log.debug('clearActiveCommands:', e?.message || String(e));
466
+ }
430
467
 
431
468
  return result;
432
469
  }
433
470
 
434
- function checkParallelSessions(rootDir) {
471
+ /**
472
+ * PERFORMANCE OPTIMIZATION (US-0356): Lightweight session check
473
+ *
474
+ * Replaces the full session-manager require chain (~3,600 lines across 8 modules)
475
+ * with direct file I/O on the registry.json and lock files (~30 lines).
476
+ * Only checks active session count and current session info.
477
+ * Full registration, cleanup, and worktree detection deferred to Phase 3 (welcome-deferred.js).
478
+ *
479
+ * Estimated savings: 100-200ms
480
+ */
481
+ function checkParallelSessionsFast(rootDir) {
435
482
  const result = {
436
483
  available: false,
437
484
  registered: false,
438
485
  otherActive: 0,
439
486
  currentId: null,
440
487
  cleaned: 0,
441
- cleanedSessions: [], // Detailed info about cleaned sessions
442
- // Extended session info for non-main sessions
488
+ cleanedSessions: [],
443
489
  isMain: true,
444
490
  nickname: null,
445
491
  branch: null,
@@ -448,60 +494,67 @@ function checkParallelSessions(rootDir) {
448
494
  };
449
495
 
450
496
  try {
451
- // PERFORMANCE OPTIMIZATION: Import session-manager directly instead of subprocess
452
- // Saves ~50-150ms by avoiding Node subprocess spawn overhead
453
- const sm = getSessionManager();
454
- if (sm && sm.fullStatus) {
455
- result.available = true;
456
- const data = sm.fullStatus();
457
- result.registered = data.registered;
458
- result.currentId = data.id;
459
- result.otherActive = data.otherActive || 0;
460
- result.cleaned = data.cleaned || 0;
461
- result.cleanedSessions = data.cleanedSessions || [];
462
-
463
- if (data.current) {
464
- result.isMain = data.current.is_main === true;
465
- result.nickname = data.current.nickname;
466
- result.branch = data.current.branch;
467
- result.sessionPath = data.current.path;
468
- }
469
- return result;
470
- }
497
+ const sessionsDir = path.join(getAgileflowDir(rootDir), 'sessions');
498
+ const registryPath = path.join(sessionsDir, 'registry.json');
471
499
 
472
- // Fallback: check if session manager script exists for subprocess call
473
- const managerPath = path.join(getAgileflowDir(rootDir), 'scripts', 'session-manager.js');
474
- if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
475
- return result;
476
- }
500
+ if (!fs.existsSync(registryPath)) return result;
477
501
 
478
- result.available = true;
479
- const scriptPath = fs.existsSync(managerPath) ? managerPath : SESSION_MANAGER_PATH;
502
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
503
+ if (!registry.sessions) return result;
480
504
 
481
- const fullStatusResult = executeCommandSync('node', [scriptPath, 'full-status'], {
482
- cwd: rootDir,
483
- fallback: null,
484
- });
505
+ result.available = true;
506
+ const cwd = process.cwd();
507
+ const staleLocks = [];
508
+
509
+ for (const [id, session] of Object.entries(registry.sessions)) {
510
+ if (session.path === cwd) {
511
+ // Found current session
512
+ result.registered = true;
513
+ result.currentId = id;
514
+ result.isMain = session.is_main === true;
515
+ result.nickname = session.nickname || null;
516
+ result.branch = session.branch || null;
517
+ result.sessionPath = session.path;
518
+ } else {
519
+ // Check if other session is alive via lock file
520
+ const lockPath = path.join(sessionsDir, `${id}.lock`);
521
+ if (fs.existsSync(lockPath)) {
522
+ try {
523
+ const content = fs.readFileSync(lockPath, 'utf8');
524
+ const pidMatch = content.match(/^pid=(\d+)/m);
525
+ if (pidMatch) {
526
+ const pid = parseInt(pidMatch[1], 10);
527
+ try {
528
+ process.kill(pid, 0); // Signal 0 = check alive
529
+ result.otherActive++;
530
+ } catch (e) {
531
+ // Process dead - collect for cleanup
532
+ staleLocks.push({ id, lockPath, pid });
533
+ }
534
+ }
535
+ } catch (e) {
536
+ // Lock read failed
537
+ }
538
+ }
539
+ }
540
+ }
485
541
 
486
- if (fullStatusResult.data) {
542
+ // Quick cleanup of stale locks (prevents ghost session counts)
543
+ for (const stale of staleLocks) {
487
544
  try {
488
- const data = JSON.parse(fullStatusResult.data);
489
- result.registered = data.registered;
490
- result.currentId = data.id;
491
- result.otherActive = data.otherActive || 0;
492
- result.cleaned = data.cleaned || 0;
493
- result.cleanedSessions = data.cleanedSessions || [];
494
-
495
- if (data.current) {
496
- result.isMain = data.current.is_main === true;
497
- result.nickname = data.current.nickname;
498
- result.branch = data.current.branch;
499
- result.sessionPath = data.current.path;
500
- }
501
- } catch (e) {}
545
+ fs.unlinkSync(stale.lockPath);
546
+ result.cleaned++;
547
+ result.cleanedSessions.push({
548
+ id: stale.id,
549
+ pid: stale.pid,
550
+ reason: 'pid_dead',
551
+ });
552
+ } catch (e) {
553
+ // Cleanup failed, deferred will retry
554
+ }
502
555
  }
503
556
  } catch (e) {
504
- // Session system not available
557
+ log.debug('checkParallelSessionsFast:', e?.message || String(e));
505
558
  }
506
559
 
507
560
  return result;
@@ -560,7 +613,9 @@ function checkPreCompact(rootDir, cache = null) {
560
613
  }
561
614
  }
562
615
  }
563
- } catch (e) {}
616
+ } catch (e) {
617
+ log.debug('checkPreCompact:', e?.message || String(e));
618
+ }
564
619
 
565
620
  return result;
566
621
  }
@@ -633,7 +688,9 @@ function checkDamageControl(rootDir, cache = null) {
633
688
  break;
634
689
  }
635
690
  }
636
- } catch (e) {}
691
+ } catch (e) {
692
+ log.debug('checkDamageControl:', e?.message || String(e));
693
+ }
637
694
 
638
695
  return result;
639
696
  }
@@ -911,7 +968,9 @@ ${marker}
911
968
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
912
969
  currentVersion = pkg.version;
913
970
  }
914
- } catch (e) {}
971
+ } catch (e) {
972
+ log.debug('getPackageVersion:', e?.message || String(e));
973
+ }
915
974
 
916
975
  // Update config_schema_version
917
976
  metadata.config_schema_version = currentVersion;
@@ -939,7 +998,115 @@ ${marker}
939
998
  return applied;
940
999
  }
941
1000
 
942
- // Check for updates (async but we'll use sync approach for welcome)
1001
+ /**
1002
+ * PERFORMANCE OPTIMIZATION: Check update cache in session-state.json (1-hour TTL)
1003
+ * Avoids ~139ms npm registry network call when cache is fresh.
1004
+ * Returns cached update result or null if stale/missing.
1005
+ */
1006
+ function getUpdateFromCache(cache) {
1007
+ try {
1008
+ const updateCache = cache?.sessionState?.update_cache;
1009
+ if (!updateCache || typeof updateCache !== 'object') return null;
1010
+ if (!updateCache.checked_at || !updateCache.result) return null;
1011
+
1012
+ const checkedAt = new Date(updateCache.checked_at).getTime();
1013
+ if (!Number.isFinite(checkedAt)) return null;
1014
+
1015
+ const age = Date.now() - checkedAt;
1016
+ const ONE_HOUR = 60 * 60 * 1000;
1017
+
1018
+ // Reject negative age (future timestamps from clock skew) or absurdly old
1019
+ if (age < 0 || age > 24 * ONE_HOUR) return null;
1020
+
1021
+ if (age < ONE_HOUR) {
1022
+ return updateCache.result;
1023
+ }
1024
+ } catch (e) {
1025
+ // Cache read failed
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ /**
1031
+ * Display deferred warnings from previous session's background work.
1032
+ * Warnings are saved by welcome-deferred.js and displayed here on next start.
1033
+ * Clears warnings after display.
1034
+ */
1035
+ function displayDeferredWarnings(rootDir) {
1036
+ try {
1037
+ const sessionStatePath = getSessionStatePath(rootDir);
1038
+ if (!fs.existsSync(sessionStatePath)) return;
1039
+
1040
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1041
+ const deferredWarnings = state.deferred_warnings;
1042
+ if (!deferredWarnings || !Array.isArray(deferredWarnings) || deferredWarnings.length === 0)
1043
+ return;
1044
+
1045
+ for (const warning of deferredWarnings) {
1046
+ if (!warning.lines || warning.lines.length === 0) continue;
1047
+
1048
+ console.log('');
1049
+ switch (warning.type) {
1050
+ case 'update_available':
1051
+ console.log(`${c.amber}↑ ${warning.lines[0]}${c.reset}`);
1052
+ if (warning.lines[1]) console.log(` ${c.skyBlue}${warning.lines[1]}${c.reset}`);
1053
+ break;
1054
+ case 'session_health':
1055
+ for (const line of warning.lines) {
1056
+ if (line.startsWith(' ')) {
1057
+ console.log(`${c.dim} └─ ${line.trim()}${c.reset}`);
1058
+ } else {
1059
+ console.log(`${c.coral}⚠️ ${line}${c.reset}`);
1060
+ }
1061
+ }
1062
+ break;
1063
+ case 'process_cleanup':
1064
+ console.log(`${c.amber}⚠️ ${warning.lines[0]}${c.reset}`);
1065
+ if (warning.lines.length > 1) {
1066
+ for (const line of warning.lines.slice(1)) {
1067
+ console.log(`${c.slate} ${line}${c.reset}`);
1068
+ }
1069
+ }
1070
+ break;
1071
+ case 'epic_completion':
1072
+ for (const line of warning.lines) {
1073
+ console.log(`${c.mintGreen}✅ ${line}${c.reset}`);
1074
+ }
1075
+ break;
1076
+ case 'ideation_sync':
1077
+ console.log(`${c.dim}📊 ${warning.lines[0]}${c.reset}`);
1078
+ break;
1079
+ case 'automations':
1080
+ console.log(`${c.teal}🤖 ${warning.lines[0]}${c.reset}`);
1081
+ for (const line of warning.lines.slice(1)) {
1082
+ console.log(`${c.dim} └─ ${line}${c.reset}`);
1083
+ }
1084
+ break;
1085
+ case 'story_claiming':
1086
+ console.log(`${c.amber}🔒 ${warning.lines[0]}${c.reset}`);
1087
+ for (const line of warning.lines.slice(1)) {
1088
+ console.log(`${c.dim} └─ ${line.trim()}${c.reset}`);
1089
+ }
1090
+ break;
1091
+ case 'file_tracking':
1092
+ console.log(`${c.amber}📁 ${warning.lines[0]}${c.reset}`);
1093
+ break;
1094
+ default:
1095
+ for (const line of warning.lines) {
1096
+ console.log(`${c.dim}${line}${c.reset}`);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ // Clear deferred warnings after display
1102
+ delete state.deferred_warnings;
1103
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
1104
+ } catch (e) {
1105
+ // Display failed, non-critical
1106
+ }
1107
+ }
1108
+
1109
+ // Check for updates (now handled by welcome-deferred.js, kept for backward compatibility)
943
1110
  async function checkUpdates() {
944
1111
  const result = {
945
1112
  available: false,
@@ -951,10 +1118,7 @@ async function checkUpdates() {
951
1118
  changelog: [],
952
1119
  };
953
1120
 
954
- let updateChecker;
955
- try {
956
- updateChecker = require('./check-update.js');
957
- } catch (e) {}
1121
+ const updateChecker = tryOptional(() => require('./check-update.js'), 'check-update');
958
1122
  if (!updateChecker) return result;
959
1123
 
960
1124
  try {
@@ -1266,7 +1430,9 @@ function getFeatureVersions(rootDir) {
1266
1430
  }
1267
1431
  }
1268
1432
  }
1269
- } catch (e) {}
1433
+ } catch (e) {
1434
+ log.debug('getFeatureVersions:', e?.message || String(e));
1435
+ }
1270
1436
 
1271
1437
  return result;
1272
1438
  }
@@ -1636,8 +1802,10 @@ function formatSessionBanner(parallelSessions) {
1636
1802
  return lines.join('\n');
1637
1803
  }
1638
1804
 
1639
- // Main
1640
- async function main() {
1805
+ // Main (synchronous - async work deferred to welcome-deferred.js)
1806
+ function main() {
1807
+ _mark('start');
1808
+
1641
1809
  // Start hook timer for metrics
1642
1810
  const timer = hookMetrics ? hookMetrics.startHookTimer('SessionStart', 'welcome') : null;
1643
1811
 
@@ -1646,26 +1814,36 @@ async function main() {
1646
1814
  // PERFORMANCE: Load all project files once into cache
1647
1815
  // This eliminates 6-8 duplicate file reads across functions
1648
1816
  const cache = loadProjectFiles(rootDir);
1817
+ _mark('loadFiles');
1649
1818
 
1650
1819
  // ============================================
1651
1820
  // PHASE 1: INSTANT WELCOME (< 300ms)
1652
1821
  // All fast operations - no network, no auto-update
1653
1822
  // ============================================
1654
1823
  const info = getProjectInfo(rootDir, cache);
1824
+ _mark('getInfo');
1655
1825
 
1656
- // Smart hook scheduling: skip archival for micro/small projects (EP-0033)
1657
- // Scale detection is done early to inform hook scheduling
1826
+ // PERFORMANCE OPTIMIZATION (US-0356): Read scale from session-state cache
1827
+ // Avoids requiring scale-detector module + git subprocess when cache is fresh (< 5 min)
1658
1828
  let earlyScale = null;
1659
- try {
1660
- const scaleDetector = require('./lib/scale-detector');
1661
- // Check cache only (fast path, no full detection yet)
1662
- earlyScale = scaleDetector.detectScale({
1663
- rootDir,
1664
- statusJson: cache?.status,
1665
- sessionState: cache?.sessionState,
1666
- });
1667
- } catch (e) {
1668
- // Scale detection not available
1829
+ const cachedScale = cache?.sessionState?.scale_detection;
1830
+ const scaleFresh =
1831
+ cachedScale &&
1832
+ cachedScale.detected_at &&
1833
+ Date.now() - new Date(cachedScale.detected_at).getTime() < 300000; // 5 min
1834
+ if (scaleFresh) {
1835
+ earlyScale = { ...cachedScale, fromCache: true };
1836
+ } else {
1837
+ try {
1838
+ const scaleDetector = require('./lib/scale-detector');
1839
+ earlyScale = scaleDetector.detectScale({
1840
+ rootDir,
1841
+ statusJson: cache?.status,
1842
+ sessionState: cache?.sessionState,
1843
+ });
1844
+ } catch (e) {
1845
+ // Scale detection not available
1846
+ }
1669
1847
  }
1670
1848
 
1671
1849
  const scaleRecommendations = earlyScale
@@ -1677,94 +1855,127 @@ async function main() {
1677
1855
  }
1678
1856
  })()
1679
1857
  : null;
1858
+ _mark('scale');
1680
1859
 
1681
1860
  const archival =
1682
1861
  scaleRecommendations && scaleRecommendations.skipArchival
1683
1862
  ? { ran: false, threshold: 0, archived: 0, remaining: 0, skippedByScale: true }
1684
1863
  : runArchival(rootDir, cache);
1864
+ _mark('archival');
1865
+
1685
1866
  const session = clearActiveCommands(rootDir, cache);
1867
+ _mark('clearCmds');
1868
+
1686
1869
  const precompact = checkPreCompact(rootDir, cache);
1687
- const parallelSessions = checkParallelSessions(rootDir);
1688
- // PERFORMANCE: Use fast expertise count (directory scan only, ~3 file samples)
1689
- // Full validation available via /agileflow:validate-expertise
1690
- const expertise = getExpertiseCountFast(rootDir);
1870
+ _mark('precompact');
1871
+
1872
+ // PERFORMANCE OPTIMIZATION (US-0356): Use lightweight session check
1873
+ // Reads registry.json + lock files directly (~30 lines) instead of
1874
+ // requiring the full session-manager module chain (~3,600 lines across 8 modules).
1875
+ // Full session registration deferred to Phase 3 (welcome-deferred.js).
1876
+ const parallelSessions = checkParallelSessionsFast(rootDir);
1877
+ _mark('sessions');
1878
+
1879
+ // PERFORMANCE OPTIMIZATION (US-0356): Defer expertise count to Phase 3
1880
+ // Directory scan with file reads is non-critical for the table.
1881
+ // Use cached result from session-state if available, otherwise show placeholder.
1882
+ let expertise;
1883
+ const cachedExpertise = cache?.sessionState?.expertise_count;
1884
+ const validExpertiseCache =
1885
+ cachedExpertise &&
1886
+ cachedExpertise.total > 0 &&
1887
+ typeof cachedExpertise.passed === 'number' &&
1888
+ typeof cachedExpertise.warnings === 'number' &&
1889
+ typeof cachedExpertise.failed === 'number';
1890
+ if (validExpertiseCache) {
1891
+ expertise = cachedExpertise;
1892
+ } else {
1893
+ expertise = getExpertiseCountFast(rootDir);
1894
+ }
1895
+ _mark('expertise');
1896
+
1691
1897
  const damageControl = checkDamageControl(rootDir, cache);
1898
+ _mark('damageCtl');
1692
1899
 
1693
1900
  // Use early scale detection result (already computed for hook scheduling)
1694
1901
  const scaleDetection = earlyScale || { scale: 'medium' };
1695
1902
 
1696
- // Agent Teams feature flag detection
1697
- let featureFlags;
1698
- try {
1699
- featureFlags = require('../lib/feature-flags');
1700
- } catch (e) {}
1903
+ // PERFORMANCE OPTIMIZATION (US-0356): Read Agent Teams mode from metadata cache
1904
+ // Avoids requiring feature-flags module when metadata is already loaded.
1905
+ // Uses same { label, value, status } format as getAgentTeamsDisplayInfo().
1701
1906
  let agentTeamsInfo = {};
1702
- if (featureFlags) {
1703
- try {
1704
- agentTeamsInfo = featureFlags.getAgentTeamsDisplayInfo({
1705
- rootDir,
1706
- metadata: cache?.metadata,
1707
- });
1708
- } catch (e) {
1709
- // Silently fail - Agent Teams info is non-critical
1710
- }
1907
+ const envTeams = process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
1908
+ const envTeamsEnabled = envTeams === '1' || envTeams === 'true' || envTeams === 'yes';
1909
+ const metaTeamsEnabled = cache?.metadata?.features?.agentTeams?.enabled === true;
1910
+ if (envTeamsEnabled || metaTeamsEnabled) {
1911
+ agentTeamsInfo = { label: 'Agent Teams', value: 'ENABLED (native)', status: 'enabled' };
1912
+ } else {
1913
+ agentTeamsInfo = { label: 'Agent Teams', value: 'subagent mode', status: 'fallback' };
1711
1914
  }
1915
+ _mark('agentTeams');
1712
1916
 
1713
1917
  // Check if a previous background update completed successfully
1714
1918
  // This allows us to show "just updated" even for background updates
1715
1919
  let updateInfo = {};
1716
1920
  try {
1717
- const sessionStatePath = getSessionStatePath(rootDir);
1718
- if (fs.existsSync(sessionStatePath)) {
1719
- const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
1720
-
1721
- // Check if pending update from previous session completed
1722
- if (state.pending_update) {
1723
- const pendingUpdate = state.pending_update;
1724
- const startedAt = new Date(pendingUpdate.started_at);
1725
- const minutesAgo = (Date.now() - startedAt.getTime()) / (1000 * 60);
1726
-
1727
- // If current version matches target, update succeeded
1728
- if (info.version === pendingUpdate.to) {
1729
- updateInfo.justUpdated = true;
1730
- updateInfo.previousVersion = pendingUpdate.from;
1731
- updateInfo.changelog = getChangelogEntries(info.version);
1732
- // Clear pending update
1733
- delete state.pending_update;
1734
- fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
1735
- } else if (minutesAgo > 5) {
1736
- // Update timed out (5 minutes) - clear it
1737
- delete state.pending_update;
1738
- fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
1739
- }
1740
- // If still pending and < 5 minutes, leave it (update may still be running)
1921
+ // Use already-loaded session state from cache instead of re-reading
1922
+ const state = cache?.sessionState;
1923
+ if (state?.pending_update) {
1924
+ const pendingUpdate = state.pending_update;
1925
+ const startedAt = new Date(pendingUpdate.started_at);
1926
+ const minutesAgo = (Date.now() - startedAt.getTime()) / (1000 * 60);
1927
+
1928
+ if (info.version === pendingUpdate.to) {
1929
+ updateInfo.justUpdated = true;
1930
+ updateInfo.previousVersion = pendingUpdate.from;
1931
+ updateInfo.changelog = getChangelogEntries(info.version);
1932
+ // Clear pending update
1933
+ const sessionStatePath = getSessionStatePath(rootDir);
1934
+ delete state.pending_update;
1935
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
1936
+ } else if (minutesAgo > 5) {
1937
+ const sessionStatePath = getSessionStatePath(rootDir);
1938
+ delete state.pending_update;
1939
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
1741
1940
  }
1742
1941
  }
1743
1942
  } catch (e) {
1744
1943
  // Silently continue - pending update check is non-critical
1745
1944
  }
1945
+ _mark('updateInfo');
1746
1946
 
1747
- // Check for new config options
1947
+ // PERFORMANCE OPTIMIZATION (US-0356): Defer config staleness to Phase 3
1948
+ // Only produces a notification; not needed for the main table.
1949
+ // Read cached result from session-state if available.
1748
1950
  let configStaleness = { outdated: false, autoApply: false };
1749
1951
  let configAutoApplied = 0;
1750
- try {
1751
- configStaleness = checkConfigStaleness(rootDir, info.version, cache);
1752
-
1753
- // Auto-apply new options if profile is "full" (only auto-applyable ones)
1754
- if (configStaleness.autoApply && configStaleness.autoApplyOptions?.length > 0) {
1755
- configAutoApplied = autoApplyConfigOptions(rootDir, configStaleness.autoApplyOptions);
1756
- if (configAutoApplied > 0) {
1757
- // Remove auto-applied options from the list, keep non-auto-applyable ones
1758
- configStaleness.newOptions = configStaleness.newOptions.filter(o => !o.autoApplyable);
1759
- configStaleness.newOptionsCount = configStaleness.newOptions.length;
1760
- if (configStaleness.newOptionsCount === 0) {
1761
- configStaleness.outdated = false;
1952
+ const cachedConfigStaleness = cache?.sessionState?.config_staleness;
1953
+ const configCacheAge = cachedConfigStaleness?.cached_at
1954
+ ? Date.now() - new Date(cachedConfigStaleness.cached_at).getTime()
1955
+ : Infinity;
1956
+ const configCacheFresh = configCacheAge < 300000; // 5 min TTL
1957
+ if (cachedConfigStaleness && configCacheFresh) {
1958
+ configStaleness = cachedConfigStaleness;
1959
+ } else {
1960
+ try {
1961
+ configStaleness = checkConfigStaleness(rootDir, info.version, cache);
1962
+
1963
+ // Auto-apply new options if profile is "full" (only auto-applyable ones)
1964
+ if (configStaleness.autoApply && configStaleness.autoApplyOptions?.length > 0) {
1965
+ configAutoApplied = autoApplyConfigOptions(rootDir, configStaleness.autoApplyOptions);
1966
+ if (configAutoApplied > 0) {
1967
+ configStaleness.newOptions = configStaleness.newOptions.filter(o => !o.autoApplyable);
1968
+ configStaleness.newOptionsCount = configStaleness.newOptions.length;
1969
+ if (configStaleness.newOptionsCount === 0) {
1970
+ configStaleness.outdated = false;
1971
+ }
1762
1972
  }
1763
1973
  }
1974
+ } catch (e) {
1975
+ // Config check failed - continue without it
1764
1976
  }
1765
- } catch (e) {
1766
- // Config check failed - continue without it
1767
1977
  }
1978
+ _mark('configStale');
1768
1979
 
1769
1980
  // Check tmux availability (only if tmuxAutoSpawn is enabled)
1770
1981
  let tmuxCheck = { available: true };
@@ -1772,6 +1983,7 @@ async function main() {
1772
1983
  if (tmuxAutoSpawnEnabled) {
1773
1984
  tmuxCheck = checkTmuxAvailability(cache);
1774
1985
  }
1986
+ _mark('tmux');
1775
1987
 
1776
1988
  // Show session banner FIRST if in a non-main session
1777
1989
  const sessionBanner = formatSessionBanner(parallelSessions);
@@ -1795,48 +2007,31 @@ async function main() {
1795
2007
  );
1796
2008
 
1797
2009
  // ============================================
1798
- // PHASE 2: BACKGROUND UPDATE CHECK (after table displays)
1799
- // This runs async and shows notification AFTER the table
2010
+ // PHASE 2: FAST POST-TABLE NOTIFICATIONS + DEFERRED BACKGROUND WORK
2011
+ // Only instant operations here. Expensive work deferred to welcome-deferred.js
1800
2012
  // ============================================
1801
- try {
1802
- // Only check for updates if we didn't already detect a "just updated" from previous session
1803
- if (!updateInfo.justUpdated) {
1804
- const freshUpdateInfo = await checkUpdates();
1805
2013
 
1806
- // If update is available, show notification AFTER the table
1807
- if (freshUpdateInfo.available && freshUpdateInfo.latest) {
2014
+ // Display deferred warnings from previous session's background work
2015
+ displayDeferredWarnings(rootDir);
2016
+
2017
+ // Check update cache (1-hour TTL) - instant if cached, skipped if stale
2018
+ let skipUpdateInDeferred = false;
2019
+ try {
2020
+ const cachedUpdate = getUpdateFromCache(cache);
2021
+ if (cachedUpdate) {
2022
+ skipUpdateInDeferred = true;
2023
+ // Use cached result for notification
2024
+ if (cachedUpdate.available && cachedUpdate.latest && !updateInfo.justUpdated) {
1808
2025
  console.log('');
1809
2026
  console.log(
1810
- `${c.amber}↑ Update available:${c.reset} v${info.version} → ${c.softGold}v${freshUpdateInfo.latest}${c.reset}`
2027
+ `${c.amber}↑ Update available:${c.reset} v${info.version} → ${c.softGold}v${cachedUpdate.latest}${c.reset}`
1811
2028
  );
1812
2029
  console.log(` Run: ${c.skyBlue}npx agileflow update${c.reset}`);
1813
-
1814
- // If auto-update is enabled, spawn it in background (non-blocking)
1815
- if (freshUpdateInfo.autoUpdate) {
1816
- spawnAutoUpdateInBackground(rootDir, info.version, freshUpdateInfo.latest);
1817
- }
1818
- }
1819
-
1820
- // Mark current version as seen to track for next update
1821
- let updateChecker;
1822
- try {
1823
- updateChecker = require('./check-update.js');
1824
- } catch (e) {}
1825
- if (freshUpdateInfo.justUpdated && updateChecker) {
1826
- updateChecker.markVersionSeen(info.version);
1827
- }
1828
- } else {
1829
- // Mark current version as seen (for "just updated" case)
1830
- let updateChecker;
1831
- try {
1832
- updateChecker = require('./check-update.js');
1833
- } catch (e) {}
1834
- if (updateChecker) {
1835
- updateChecker.markVersionSeen(info.version);
1836
2030
  }
1837
2031
  }
2032
+ // If no cache or stale, the deferred script will check and cache the result
1838
2033
  } catch (e) {
1839
- // Update check failed - continue without it (non-critical)
2034
+ // Cache check failed, deferred script will handle it
1840
2035
  }
1841
2036
 
1842
2037
  // Show config auto-apply confirmation (for "full" profile)
@@ -1913,272 +2108,52 @@ async function main() {
1913
2108
  );
1914
2109
  }
1915
2110
 
1916
- // === SESSION HEALTH WARNINGS ===
1917
- // Check for forgotten sessions with uncommitted changes, stale sessions, orphaned entries
1918
- // PERFORMANCE OPTIMIZATION: Direct function call instead of subprocess (~50-100ms savings)
1919
- try {
1920
- const sm = getSessionManager();
1921
- const health = sm ? sm.getSessionsHealth({ staleDays: 7 }) : null;
1922
-
1923
- if (health) {
1924
- const hasIssues =
1925
- health.uncommitted.length > 0 ||
1926
- health.stale.length > 0 ||
1927
- health.orphanedRegistry.length > 0;
1928
-
1929
- if (hasIssues) {
1930
- console.log('');
2111
+ _mark('postTable');
1931
2112
 
1932
- // Uncommitted changes - MOST IMPORTANT (potential data loss)
1933
- if (health.uncommitted.length > 0) {
1934
- console.log(
1935
- `${c.coral}⚠️ ${health.uncommitted.length} session(s) have uncommitted changes:${c.reset}`
1936
- );
1937
- health.uncommitted.slice(0, 3).forEach(sess => {
1938
- const name = sess.nickname ? `"${sess.nickname}"` : `Session ${sess.id}`;
1939
- console.log(`${c.dim} └─ ${name}: ${sess.changeCount} file(s)${c.reset}`);
1940
- });
1941
- if (health.uncommitted.length > 3) {
1942
- console.log(`${c.dim} └─ ... and ${health.uncommitted.length - 3} more${c.reset}`);
1943
- }
1944
- console.log(
1945
- `${c.slate} Run: ${c.skyBlue}/agileflow:session:status${c.slate} to see details${c.reset}`
1946
- );
1947
- }
1948
-
1949
- // Stale sessions (inactive 7+ days)
1950
- if (health.stale.length > 0) {
1951
- console.log(
1952
- `${c.amber}📅 ${health.stale.length} session(s) inactive for 7+ days${c.reset}`
1953
- );
1954
- }
1955
-
1956
- // Orphaned registry entries (path doesn't exist)
1957
- if (health.orphanedRegistry.length > 0) {
1958
- console.log(
1959
- `${c.peach}🗑️ ${health.orphanedRegistry.length} session(s) have missing directories${c.reset}`
1960
- );
1961
- }
1962
- }
1963
- }
1964
- } catch (e) {
1965
- // Health check failed, skip silently
1966
- }
1967
-
1968
- // === DUPLICATE CLAUDE PROCESS DETECTION ===
1969
- // Check for multiple Claude processes in the same working directory
1970
- let processCleanup;
1971
- try {
1972
- processCleanup = require('./lib/process-cleanup.js');
1973
- } catch (e) {}
1974
- if (processCleanup) {
1975
- try {
1976
- // Auto-kill is explicitly opt-in at runtime.
1977
- // Even if metadata has autoKill=true from older configs, we require
1978
- // AGILEFLOW_PROCESS_CLEANUP_AUTOKILL=1 to prevent accidental session kills.
1979
- const metadata = cache?.metadata;
1980
- const autoKillConfigured = metadata?.features?.processCleanup?.autoKill === true;
1981
- const autoKill = autoKillConfigured && process.env.AGILEFLOW_PROCESS_CLEANUP_AUTOKILL === '1';
1982
-
1983
- const cleanupResult = processCleanup.cleanupDuplicateProcesses({
1984
- rootDir,
1985
- autoKill,
1986
- dryRun: false,
1987
- });
1988
-
1989
- if (cleanupResult.duplicates > 0) {
1990
- console.log('');
1991
-
1992
- if (cleanupResult.killed.length > 0) {
1993
- // Auto-kill was enabled and processes were terminated
1994
- console.log(
1995
- `${c.mintGreen}🔧 Cleaned ${cleanupResult.killed.length} duplicate Claude process(es)${c.reset}`
1996
- );
1997
- cleanupResult.killed.forEach(proc => {
1998
- console.log(`${c.dim} └─ PID ${proc.pid} (${proc.method})${c.reset}`);
1999
- });
2000
- } else {
2001
- // Warn only (auto-kill disabled or skipped by safety guards)
2002
- console.log(
2003
- `${c.amber}⚠️ ${cleanupResult.duplicates} other Claude process(es) in same directory${c.reset}`
2004
- );
2005
- console.log(`${c.slate} This may cause slowdowns and freezing. Options:${c.reset}`);
2006
- console.log(`${c.slate} • Close duplicate Claude windows/tabs${c.reset}`);
2007
- if (autoKillConfigured) {
2008
- console.log(
2009
- `${c.slate} • Auto-kill configured but runtime opt-in is off (safer default)${c.reset}`
2010
- );
2011
- }
2012
- }
2013
-
2014
- if (cleanupResult.errors.length > 0) {
2015
- cleanupResult.errors.forEach(err => {
2016
- console.log(`${c.coral} ⚠ Failed to kill PID ${err.pid}: ${err.error}${c.reset}`);
2017
- });
2018
- }
2019
- }
2020
- } catch (e) {
2021
- // Silently ignore process cleanup errors
2022
- }
2023
- }
2024
-
2025
- // Story claiming: cleanup stale claims and show warnings
2026
- let storyClaiming;
2027
- try {
2028
- storyClaiming = require('./lib/story-claiming.js');
2029
- } catch (e) {}
2030
- if (storyClaiming) {
2031
- try {
2032
- // Clean up stale claims (dead PIDs, expired TTL)
2033
- const cleanupResult = storyClaiming.cleanupStaleClaims({ rootDir });
2034
- if (cleanupResult.ok && cleanupResult.cleaned > 0) {
2035
- console.log('');
2036
- console.log(`${c.dim}Cleaned ${cleanupResult.cleaned} stale story claim(s)${c.reset}`);
2037
- }
2038
-
2039
- // Show stories claimed by other sessions
2040
- const othersResult = storyClaiming.getStoriesClaimedByOthers({ rootDir });
2041
- if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
2042
- console.log('');
2043
- console.log(storyClaiming.formatClaimedStories(othersResult.stories));
2044
- console.log('');
2045
- console.log(
2046
- `${c.slate} These stories are locked - pick a different one to avoid conflicts.${c.reset}`
2047
- );
2048
- }
2049
- } catch (e) {
2050
- // Silently ignore story claiming errors
2051
- }
2052
- }
2053
-
2054
- // File tracking: cleanup stale touches and show overlap warnings
2055
- let fileTracking;
2113
+ // ============================================
2114
+ // PHASE 3: SPAWN DEFERRED BACKGROUND WORK
2115
+ // Session health, process cleanup, story claiming, file tracking,
2116
+ // epic completion, ideation sync, automations, update check (if stale)
2117
+ // Also: full session registration, expertise re-scan, config staleness check
2118
+ // Results saved to session-state.json for next session display.
2119
+ // ============================================
2056
2120
  try {
2057
- fileTracking = require('./lib/file-tracking.js');
2058
- } catch (e) {}
2059
- if (fileTracking) {
2060
- try {
2061
- // Clean up stale file touches (dead PIDs, expired TTL)
2062
- const cleanupResult = fileTracking.cleanupStaleTouches({ rootDir });
2063
- if (cleanupResult.ok && cleanupResult.cleaned > 0) {
2064
- console.log('');
2065
- console.log(
2066
- `${c.dim}Cleaned ${cleanupResult.cleaned} stale file tracking session(s)${c.reset}`
2067
- );
2121
+ const deferredScript = path.join(__dirname, 'welcome-deferred.js');
2122
+ if (fs.existsSync(deferredScript)) {
2123
+ const deferredArgs = [deferredScript, rootDir, `--version=${info.version}`];
2124
+ if (skipUpdateInDeferred) {
2125
+ deferredArgs.push('--skip-update');
2068
2126
  }
2069
-
2070
- // Show file overlaps with other sessions
2071
- const overlapsResult = fileTracking.getMyFileOverlaps({ rootDir });
2072
- if (overlapsResult.ok && overlapsResult.overlaps && overlapsResult.overlaps.length > 0) {
2073
- console.log('');
2074
- console.log(fileTracking.formatFileOverlaps(overlapsResult.overlaps));
2075
- }
2076
- } catch (e) {
2077
- // Silently ignore file tracking errors
2078
- }
2079
- }
2080
-
2081
- // Epic completion check: auto-complete epics where all stories are done
2082
- let storyStateMachine;
2083
- try {
2084
- storyStateMachine = require('./lib/story-state-machine.js');
2085
- } catch (e) {}
2086
- if (storyStateMachine && cache.status) {
2087
- try {
2088
- const statusPath = getStatusPath(rootDir);
2089
- const statusData = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
2090
- const incompleteEpics = storyStateMachine.findIncompleteEpics(statusData);
2091
-
2092
- if (incompleteEpics.length > 0) {
2093
- let autoCompleted = 0;
2094
- for (const { epicId, completed, total } of incompleteEpics) {
2095
- const result = storyStateMachine.autoCompleteEpic(statusData, epicId);
2096
- if (result.updated) {
2097
- autoCompleted++;
2098
- console.log('');
2099
- console.log(
2100
- `${c.mintGreen}✅ Auto-completed ${c.bold}${epicId}${c.reset}${c.mintGreen} (${completed}/${total} stories done)${c.reset}`
2101
- );
2102
- }
2103
- }
2104
- if (autoCompleted > 0) {
2105
- fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2) + '\n');
2106
- }
2127
+ if (updateInfo.justUpdated) {
2128
+ deferredArgs.push('--just-updated');
2107
2129
  }
2108
- } catch (e) {
2109
- // Silently ignore epic completion errors
2110
- }
2111
- }
2112
-
2113
- // Ideation sync: mark ideas as implemented when linked epics complete
2114
- let syncIdeationStatus;
2115
- try {
2116
- syncIdeationStatus = require('./lib/sync-ideation-status.js');
2117
- } catch (e) {}
2118
- if (syncIdeationStatus) {
2119
- try {
2120
- const syncResult = syncIdeationStatus.syncImplementedIdeas(rootDir);
2121
- if (syncResult.ok && syncResult.updated > 0) {
2122
- console.log('');
2123
- console.log(`${c.dim}📊 Synced ${syncResult.updated} idea(s) as implemented${c.reset}`);
2130
+ // US-0356: Signal deferred to run full session registration and expertise scan
2131
+ deferredArgs.push('--run-session-register');
2132
+ deferredArgs.push('--run-expertise-scan');
2133
+ if (!cachedConfigStaleness || !configCacheFresh) {
2134
+ deferredArgs.push('--run-config-staleness');
2124
2135
  }
2125
- } catch (e) {
2126
- // Silently ignore ideation sync errors
2136
+ spawnBackground('node', deferredArgs, { cwd: rootDir });
2127
2137
  }
2128
- }
2129
-
2130
- // === SCHEDULED AUTOMATIONS ===
2131
- // Check for and run due automations (non-blocking)
2132
- let automationRegistry, automationRunner;
2133
- try {
2134
- automationRegistry = require('./lib/automation-registry.js');
2135
- automationRunner = require('./lib/automation-runner.js');
2136
2138
  } catch (e) {
2137
- // Automation system not available
2139
+ // Deferred script spawn failed, non-critical
2138
2140
  }
2139
- if (automationRegistry && automationRunner) {
2140
- try {
2141
- const registry = automationRegistry.getAutomationRegistry({ rootDir });
2142
- const runner = automationRunner.getAutomationRunner({ rootDir });
2143
- const dueStatus = runner.getDueStatus();
2144
2141
 
2145
- if (dueStatus.due > 0) {
2146
- console.log('');
2147
- console.log(`${c.teal}🤖 ${dueStatus.due} automation(s) due to run${c.reset}`);
2148
-
2149
- // Show what's due
2150
- for (const auto of dueStatus.dueAutomations.slice(0, 3)) {
2151
- console.log(`${c.dim} └─ ${auto.name}${c.reset}`);
2152
- }
2153
- if (dueStatus.due > 3) {
2154
- console.log(`${c.dim} └─ ... and ${dueStatus.due - 3} more${c.reset}`);
2155
- }
2156
-
2157
- // Run due automations in background (spawn detached process)
2158
- // This prevents blocking the welcome hook
2159
- const runnerScriptPath = path.join(__dirname, 'automation-run-due.js');
2160
-
2161
- // Only spawn if the runner script exists
2162
- if (fs.existsSync(runnerScriptPath)) {
2163
- spawnBackground('node', [runnerScriptPath], { cwd: rootDir });
2164
- console.log(`${c.dim} Running in background...${c.reset}`);
2165
- } else {
2166
- console.log(`${c.slate} Run: ${c.skyBlue}/agileflow:automate ACTION=run-due${c.reset}`);
2167
- }
2168
- }
2169
- } catch (e) {
2170
- // Silently ignore automation errors
2171
- }
2172
- }
2142
+ _mark('deferred');
2173
2143
 
2174
2144
  // Record hook metrics
2175
2145
  if (timer && hookMetrics) {
2176
2146
  hookMetrics.recordHookMetrics(timer, 'success', null, { rootDir });
2177
2147
  }
2148
+
2149
+ // Output profiling data if enabled
2150
+ _logTimings();
2178
2151
  }
2179
2152
 
2180
- main().catch(err => {
2181
- console.error(err);
2153
+ try {
2154
+ main();
2155
+ } catch (err) {
2156
+ log.error(err.message || String(err));
2182
2157
  // Record error in metrics if possible
2183
2158
  if (hookMetrics) {
2184
2159
  try {
@@ -2189,4 +2164,4 @@ main().catch(err => {
2189
2164
  // Silently ignore metrics errors
2190
2165
  }
2191
2166
  }
2192
- });
2167
+ }