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.
- package/CHANGELOG.md +10 -0
- package/README.md +57 -85
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +409 -434
- package/scripts/claude-tmux.sh +80 -2
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/browser-qa-evidence.js +409 -0
- package/scripts/lib/browser-qa-status.js +192 -0
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +295 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +7 -2
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/browser-qa.md +328 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/browser-qa.md +240 -0
- package/src/core/commands/configure.md +8 -8
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/browser-qa-spec.yaml +94 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -69
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -105
- 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
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
// PERFORMANCE OPTIMIZATION
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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: [],
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
502
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
503
|
+
if (!registry.sessions) return result;
|
|
480
504
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
542
|
+
// Quick cleanup of stale locks (prevents ghost session counts)
|
|
543
|
+
for (const stale of staleLocks) {
|
|
487
544
|
try {
|
|
488
|
-
|
|
489
|
-
result.
|
|
490
|
-
result.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1657
|
-
//
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
//
|
|
1690
|
-
|
|
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
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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:
|
|
1799
|
-
//
|
|
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
|
-
|
|
1807
|
-
|
|
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${
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2109
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2181
|
-
|
|
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
|
+
}
|