agileflow 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -8
- package/lib/api-server.js +3 -2
- package/lib/feedback.js +9 -2
- package/lib/flag-detection.js +4 -2
- package/lib/git-operations.js +4 -2
- package/lib/lazy-require.js +59 -0
- package/lib/process-executor.js +24 -9
- package/lib/skill-loader.js +11 -3
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +12 -0
- package/scripts/agileflow-welcome.js +146 -90
- package/scripts/claude-tmux.sh +42 -6
- package/scripts/damage-control-multi-agent.js +14 -10
- package/scripts/lib/bus-utils.js +3 -1
- package/scripts/lib/configure-detect.js +12 -9
- package/scripts/lib/configure-features.js +128 -7
- package/scripts/lib/configure-repair.js +6 -5
- package/scripts/lib/context-formatter.js +13 -3
- package/scripts/lib/damage-control-utils.js +5 -1
- package/scripts/lib/lifecycle-detector.js +5 -3
- package/scripts/lib/process-cleanup.js +8 -4
- package/scripts/lib/scale-detector.js +47 -8
- package/scripts/lib/signal-detectors.js +117 -59
- package/scripts/lib/task-registry.js +5 -1
- package/scripts/lib/team-events.js +4 -4
- package/scripts/messaging-bridge.js +7 -1
- package/scripts/ralph-loop.js +10 -8
- package/scripts/smart-detect.js +32 -11
- package/scripts/team-manager.js +86 -1
- package/scripts/tmux-task-name.sh +105 -0
- package/scripts/tmux-task-watcher.sh +344 -0
- package/src/core/agents/legal-analyzer-a11y.md +110 -0
- package/src/core/agents/legal-analyzer-ai.md +117 -0
- package/src/core/agents/legal-analyzer-consumer.md +108 -0
- package/src/core/agents/legal-analyzer-content.md +113 -0
- package/src/core/agents/legal-analyzer-international.md +115 -0
- package/src/core/agents/legal-analyzer-licensing.md +115 -0
- package/src/core/agents/legal-analyzer-privacy.md +108 -0
- package/src/core/agents/legal-analyzer-security.md +112 -0
- package/src/core/agents/legal-analyzer-terms.md +111 -0
- package/src/core/agents/legal-consensus.md +242 -0
- package/src/core/agents/team-lead.md +50 -13
- package/src/core/commands/babysit.md +75 -42
- package/src/core/commands/blockers.md +7 -7
- package/src/core/commands/configure.md +15 -61
- package/src/core/commands/discovery/brief.md +363 -0
- package/src/core/commands/discovery/new.md +395 -0
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/legal/audit.md +446 -0
- package/src/core/commands/logic/audit.md +5 -5
- package/src/core/commands/review.md +7 -1
- package/src/core/commands/rpi.md +61 -26
- package/src/core/commands/sprint.md +7 -6
- package/src/core/commands/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- package/src/core/templates/product-brief.md +136 -0
- package/tools/cli/installers/ide/claude-code.js +69 -2
- package/src/core/agents/configuration/archival.md +0 -350
- package/src/core/agents/configuration/attribution.md +0 -343
- package/src/core/agents/configuration/ci.md +0 -1103
- package/src/core/agents/configuration/damage-control.md +0 -375
- package/src/core/agents/configuration/git-config.md +0 -537
- package/src/core/agents/configuration/hooks.md +0 -623
- package/src/core/agents/configuration/precompact.md +0 -302
- package/src/core/agents/configuration/status-line.md +0 -557
- package/src/core/agents/configuration/verify.md +0 -618
- package/src/core/agents/configuration-damage-control.md +0 -259
- package/src/core/agents/configuration-visual-e2e.md +0 -339
|
@@ -30,6 +30,20 @@ const { readJSONCached, readFileCached } = require('../lib/file-cache');
|
|
|
30
30
|
// Session manager path (relative to script location)
|
|
31
31
|
const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
|
|
32
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return _sessionManager;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// Hook metrics module (kept at top level - needed early for timer)
|
|
34
48
|
let hookMetrics;
|
|
35
49
|
try {
|
|
@@ -43,6 +57,12 @@ try {
|
|
|
43
57
|
* Uses file-cache module for automatic caching with 15s TTL.
|
|
44
58
|
* Files are cached across script invocations within TTL window.
|
|
45
59
|
* Estimated savings: 60-120ms on cache hits
|
|
60
|
+
*
|
|
61
|
+
* Additional optimizations in this file (US-0356):
|
|
62
|
+
* - Git batching: 3 subprocess calls → 1 (~20-40ms savings)
|
|
63
|
+
* - Session-manager inline: subprocess → direct require() (~50-150ms savings)
|
|
64
|
+
* - Tmux cache: subprocess → session-state lookup (~10-20ms after first run)
|
|
65
|
+
* Total estimated savings: ~130-260ms
|
|
46
66
|
*/
|
|
47
67
|
function loadProjectFiles(rootDir) {
|
|
48
68
|
const paths = {
|
|
@@ -109,32 +129,57 @@ function detectPlatform() {
|
|
|
109
129
|
|
|
110
130
|
/**
|
|
111
131
|
* Check if tmux is installed
|
|
132
|
+
* PERFORMANCE OPTIMIZATION: Caches result in session-state.json (~10-20ms savings on subsequent runs)
|
|
112
133
|
* Returns object with availability info and platform-specific install suggestion
|
|
113
134
|
*/
|
|
114
|
-
function checkTmuxAvailability() {
|
|
135
|
+
function checkTmuxAvailability(cache) {
|
|
136
|
+
// Check session state cache first (tmux availability doesn't change within a session)
|
|
137
|
+
if (cache?.sessionState?.tmux_available !== undefined) {
|
|
138
|
+
if (cache.sessionState.tmux_available) return { available: true };
|
|
139
|
+
return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Actually check (first run or no cache)
|
|
115
143
|
const result = executeCommandSync('which', ['tmux'], { fallback: null });
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
const available = result.data !== null;
|
|
145
|
+
|
|
146
|
+
// Cache in session state for next invocation
|
|
147
|
+
try {
|
|
148
|
+
const rootDir = getProjectRoot();
|
|
149
|
+
const sessionStatePath = getSessionStatePath(rootDir);
|
|
150
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
151
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
152
|
+
state.tmux_available = available;
|
|
153
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Cache write failed, non-critical
|
|
118
157
|
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
platform,
|
|
123
|
-
noSudoCmd: 'conda install -c conda-forge tmux',
|
|
124
|
-
};
|
|
158
|
+
|
|
159
|
+
if (available) return { available: true };
|
|
160
|
+
return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
|
|
125
161
|
}
|
|
126
162
|
|
|
127
163
|
/**
|
|
128
164
|
* PERFORMANCE OPTIMIZATION: Batch git commands into single call
|
|
129
|
-
*
|
|
130
|
-
*
|
|
165
|
+
* Uses `git log -1 --format=%D%n%h%n%s` to get branch, short hash, and subject
|
|
166
|
+
* in a single subprocess instead of 3 separate calls.
|
|
167
|
+
* Savings: ~20-40ms (eliminates 2 subprocess spawns)
|
|
131
168
|
*/
|
|
132
169
|
function getGitInfo(rootDir) {
|
|
133
|
-
const opts = { cwd: rootDir, timeout: 5000, fallback: '
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
const opts = { cwd: rootDir, timeout: 5000, fallback: '' };
|
|
171
|
+
const result = git(['log', '-1', '--format=%D%n%h%n%s'], opts);
|
|
172
|
+
if (result.data) {
|
|
173
|
+
const lines = result.data.split('\n');
|
|
174
|
+
// %D gives decorations like "HEAD -> main, origin/main, tag: v3.0.0"
|
|
175
|
+
const branchMatch = (lines[0] || '').match(/HEAD -> ([^,\s]+)/);
|
|
176
|
+
return {
|
|
177
|
+
branch: branchMatch ? branchMatch[1] : 'detached',
|
|
178
|
+
commit: (lines[1] || 'unknown').trim(),
|
|
179
|
+
lastCommit: lines.slice(2).join('\n').trim(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { branch: 'unknown', commit: 'unknown', lastCommit: '' };
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
function getProjectInfo(rootDir, cache = null) {
|
|
@@ -403,20 +448,39 @@ function checkParallelSessions(rootDir) {
|
|
|
403
448
|
};
|
|
404
449
|
|
|
405
450
|
try {
|
|
406
|
-
//
|
|
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
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fallback: check if session manager script exists for subprocess call
|
|
407
473
|
const managerPath = path.join(getAgileflowDir(rootDir), 'scripts', 'session-manager.js');
|
|
408
474
|
if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
|
|
409
475
|
return result;
|
|
410
476
|
}
|
|
411
477
|
|
|
412
478
|
result.available = true;
|
|
413
|
-
|
|
414
|
-
// Try to use combined full-status command (saves ~200ms vs 3 separate calls)
|
|
415
479
|
const scriptPath = fs.existsSync(managerPath) ? managerPath : SESSION_MANAGER_PATH;
|
|
416
480
|
|
|
417
|
-
// PERFORMANCE: Single subprocess call instead of 3 (register + count + status)
|
|
418
481
|
const fullStatusResult = executeCommandSync('node', [scriptPath, 'full-status'], {
|
|
419
|
-
cwd: rootDir,
|
|
482
|
+
cwd: rootDir,
|
|
483
|
+
fallback: null,
|
|
420
484
|
});
|
|
421
485
|
|
|
422
486
|
if (fullStatusResult.data) {
|
|
@@ -434,49 +498,7 @@ function checkParallelSessions(rootDir) {
|
|
|
434
498
|
result.branch = data.current.branch;
|
|
435
499
|
result.sessionPath = data.current.path;
|
|
436
500
|
}
|
|
437
|
-
} catch (e) {
|
|
438
|
-
// JSON parse failed, fall through to individual calls
|
|
439
|
-
fullStatusResult.data = null;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (!fullStatusResult.data) {
|
|
444
|
-
// Fall back to individual calls if full-status not available (older version)
|
|
445
|
-
const registerResult = executeCommandSync('node', [scriptPath, 'register'], {
|
|
446
|
-
cwd: rootDir, fallback: null,
|
|
447
|
-
});
|
|
448
|
-
if (registerResult.data) {
|
|
449
|
-
try {
|
|
450
|
-
const registerData = JSON.parse(registerResult.data);
|
|
451
|
-
result.registered = true;
|
|
452
|
-
result.currentId = registerData.id;
|
|
453
|
-
} catch (e) {}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const countResult = executeCommandSync('node', [scriptPath, 'count'], {
|
|
457
|
-
cwd: rootDir, fallback: null,
|
|
458
|
-
});
|
|
459
|
-
if (countResult.data) {
|
|
460
|
-
try {
|
|
461
|
-
const countData = JSON.parse(countResult.data);
|
|
462
|
-
result.otherActive = countData.count || 0;
|
|
463
|
-
} catch (e) {}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const statusCmdResult = executeCommandSync('node', [scriptPath, 'status'], {
|
|
467
|
-
cwd: rootDir, fallback: null,
|
|
468
|
-
});
|
|
469
|
-
if (statusCmdResult.data) {
|
|
470
|
-
try {
|
|
471
|
-
const statusData = JSON.parse(statusCmdResult.data);
|
|
472
|
-
if (statusData.current) {
|
|
473
|
-
result.isMain = statusData.current.is_main === true;
|
|
474
|
-
result.nickname = statusData.current.nickname;
|
|
475
|
-
result.branch = statusData.current.branch;
|
|
476
|
-
result.sessionPath = statusData.current.path;
|
|
477
|
-
}
|
|
478
|
-
} catch (e) {}
|
|
479
|
-
}
|
|
501
|
+
} catch (e) {}
|
|
480
502
|
}
|
|
481
503
|
} catch (e) {
|
|
482
504
|
// Session system not available
|
|
@@ -930,7 +952,9 @@ async function checkUpdates() {
|
|
|
930
952
|
};
|
|
931
953
|
|
|
932
954
|
let updateChecker;
|
|
933
|
-
try {
|
|
955
|
+
try {
|
|
956
|
+
updateChecker = require('./check-update.js');
|
|
957
|
+
} catch (e) {}
|
|
934
958
|
if (!updateChecker) return result;
|
|
935
959
|
|
|
936
960
|
try {
|
|
@@ -1000,7 +1024,8 @@ function getChangelogEntries(version) {
|
|
|
1000
1024
|
async function runAutoUpdate(rootDir, fromVersion, toVersion) {
|
|
1001
1025
|
const runUpdate = () => {
|
|
1002
1026
|
return executeCommandSync('npx', ['agileflow@latest', 'update', '--force'], {
|
|
1003
|
-
cwd: rootDir,
|
|
1027
|
+
cwd: rootDir,
|
|
1028
|
+
timeout: 120000,
|
|
1004
1029
|
});
|
|
1005
1030
|
};
|
|
1006
1031
|
|
|
@@ -1528,17 +1553,26 @@ function formatTable(
|
|
|
1528
1553
|
// Scale detection (EP-0033)
|
|
1529
1554
|
if (scaleDetection && scaleDetection.scale) {
|
|
1530
1555
|
const scaleColors = {
|
|
1531
|
-
micro: c.cyan,
|
|
1532
|
-
|
|
1556
|
+
micro: c.cyan,
|
|
1557
|
+
small: c.teal,
|
|
1558
|
+
medium: c.mintGreen,
|
|
1559
|
+
large: c.peach,
|
|
1560
|
+
enterprise: c.coral,
|
|
1533
1561
|
};
|
|
1534
1562
|
const scaleIcons = {
|
|
1535
|
-
micro: '◦',
|
|
1563
|
+
micro: '◦',
|
|
1564
|
+
small: '○',
|
|
1565
|
+
medium: '◎',
|
|
1566
|
+
large: '●',
|
|
1567
|
+
enterprise: '◉',
|
|
1536
1568
|
};
|
|
1537
1569
|
const scale = scaleDetection.scale;
|
|
1538
1570
|
const icon = scaleIcons[scale] || '◎';
|
|
1539
1571
|
const label = scale.charAt(0).toUpperCase() + scale.slice(1);
|
|
1540
1572
|
const cacheNote = scaleDetection.fromCache ? '' : ` (${scaleDetection.detection_ms}ms)`;
|
|
1541
|
-
lines.push(
|
|
1573
|
+
lines.push(
|
|
1574
|
+
row('Scale', `${icon} ${label}${cacheNote}`, c.lavender, scaleColors[scale] || c.dim)
|
|
1575
|
+
);
|
|
1542
1576
|
}
|
|
1543
1577
|
|
|
1544
1578
|
lines.push(divider());
|
|
@@ -1634,13 +1668,20 @@ async function main() {
|
|
|
1634
1668
|
// Scale detection not available
|
|
1635
1669
|
}
|
|
1636
1670
|
|
|
1637
|
-
const scaleRecommendations = earlyScale
|
|
1638
|
-
|
|
1639
|
-
|
|
1671
|
+
const scaleRecommendations = earlyScale
|
|
1672
|
+
? (() => {
|
|
1673
|
+
try {
|
|
1674
|
+
return require('./lib/scale-detector').getScaleRecommendations(earlyScale.scale);
|
|
1675
|
+
} catch {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
})()
|
|
1679
|
+
: null;
|
|
1640
1680
|
|
|
1641
|
-
const archival =
|
|
1642
|
-
|
|
1643
|
-
|
|
1681
|
+
const archival =
|
|
1682
|
+
scaleRecommendations && scaleRecommendations.skipArchival
|
|
1683
|
+
? { ran: false, threshold: 0, archived: 0, remaining: 0, skippedByScale: true }
|
|
1684
|
+
: runArchival(rootDir, cache);
|
|
1644
1685
|
const session = clearActiveCommands(rootDir, cache);
|
|
1645
1686
|
const precompact = checkPreCompact(rootDir, cache);
|
|
1646
1687
|
const parallelSessions = checkParallelSessions(rootDir);
|
|
@@ -1654,7 +1695,9 @@ async function main() {
|
|
|
1654
1695
|
|
|
1655
1696
|
// Agent Teams feature flag detection
|
|
1656
1697
|
let featureFlags;
|
|
1657
|
-
try {
|
|
1698
|
+
try {
|
|
1699
|
+
featureFlags = require('../lib/feature-flags');
|
|
1700
|
+
} catch (e) {}
|
|
1658
1701
|
let agentTeamsInfo = {};
|
|
1659
1702
|
if (featureFlags) {
|
|
1660
1703
|
try {
|
|
@@ -1727,7 +1770,7 @@ async function main() {
|
|
|
1727
1770
|
let tmuxCheck = { available: true };
|
|
1728
1771
|
const tmuxAutoSpawnEnabled = cache?.metadata?.features?.tmuxAutoSpawn?.enabled !== false;
|
|
1729
1772
|
if (tmuxAutoSpawnEnabled) {
|
|
1730
|
-
tmuxCheck = checkTmuxAvailability();
|
|
1773
|
+
tmuxCheck = checkTmuxAvailability(cache);
|
|
1731
1774
|
}
|
|
1732
1775
|
|
|
1733
1776
|
// Show session banner FIRST if in a non-main session
|
|
@@ -1776,14 +1819,18 @@ async function main() {
|
|
|
1776
1819
|
|
|
1777
1820
|
// Mark current version as seen to track for next update
|
|
1778
1821
|
let updateChecker;
|
|
1779
|
-
try {
|
|
1822
|
+
try {
|
|
1823
|
+
updateChecker = require('./check-update.js');
|
|
1824
|
+
} catch (e) {}
|
|
1780
1825
|
if (freshUpdateInfo.justUpdated && updateChecker) {
|
|
1781
1826
|
updateChecker.markVersionSeen(info.version);
|
|
1782
1827
|
}
|
|
1783
1828
|
} else {
|
|
1784
1829
|
// Mark current version as seen (for "just updated" case)
|
|
1785
1830
|
let updateChecker;
|
|
1786
|
-
try {
|
|
1831
|
+
try {
|
|
1832
|
+
updateChecker = require('./check-update.js');
|
|
1833
|
+
} catch (e) {}
|
|
1787
1834
|
if (updateChecker) {
|
|
1788
1835
|
updateChecker.markVersionSeen(info.version);
|
|
1789
1836
|
}
|
|
@@ -1868,13 +1915,12 @@ async function main() {
|
|
|
1868
1915
|
|
|
1869
1916
|
// === SESSION HEALTH WARNINGS ===
|
|
1870
1917
|
// Check for forgotten sessions with uncommitted changes, stale sessions, orphaned entries
|
|
1918
|
+
// PERFORMANCE OPTIMIZATION: Direct function call instead of subprocess (~50-100ms savings)
|
|
1871
1919
|
try {
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
});
|
|
1920
|
+
const sm = getSessionManager();
|
|
1921
|
+
const health = sm ? sm.getSessionsHealth({ staleDays: 7 }) : null;
|
|
1875
1922
|
|
|
1876
|
-
if (
|
|
1877
|
-
const health = JSON.parse(healthResult.data);
|
|
1923
|
+
if (health) {
|
|
1878
1924
|
const hasIssues =
|
|
1879
1925
|
health.uncommitted.length > 0 ||
|
|
1880
1926
|
health.stale.length > 0 ||
|
|
@@ -1922,7 +1968,9 @@ async function main() {
|
|
|
1922
1968
|
// === DUPLICATE CLAUDE PROCESS DETECTION ===
|
|
1923
1969
|
// Check for multiple Claude processes in the same working directory
|
|
1924
1970
|
let processCleanup;
|
|
1925
|
-
try {
|
|
1971
|
+
try {
|
|
1972
|
+
processCleanup = require('./lib/process-cleanup.js');
|
|
1973
|
+
} catch (e) {}
|
|
1926
1974
|
if (processCleanup) {
|
|
1927
1975
|
try {
|
|
1928
1976
|
// Auto-kill is explicitly opt-in at runtime.
|
|
@@ -1976,7 +2024,9 @@ async function main() {
|
|
|
1976
2024
|
|
|
1977
2025
|
// Story claiming: cleanup stale claims and show warnings
|
|
1978
2026
|
let storyClaiming;
|
|
1979
|
-
try {
|
|
2027
|
+
try {
|
|
2028
|
+
storyClaiming = require('./lib/story-claiming.js');
|
|
2029
|
+
} catch (e) {}
|
|
1980
2030
|
if (storyClaiming) {
|
|
1981
2031
|
try {
|
|
1982
2032
|
// Clean up stale claims (dead PIDs, expired TTL)
|
|
@@ -2003,7 +2053,9 @@ async function main() {
|
|
|
2003
2053
|
|
|
2004
2054
|
// File tracking: cleanup stale touches and show overlap warnings
|
|
2005
2055
|
let fileTracking;
|
|
2006
|
-
try {
|
|
2056
|
+
try {
|
|
2057
|
+
fileTracking = require('./lib/file-tracking.js');
|
|
2058
|
+
} catch (e) {}
|
|
2007
2059
|
if (fileTracking) {
|
|
2008
2060
|
try {
|
|
2009
2061
|
// Clean up stale file touches (dead PIDs, expired TTL)
|
|
@@ -2028,7 +2080,9 @@ async function main() {
|
|
|
2028
2080
|
|
|
2029
2081
|
// Epic completion check: auto-complete epics where all stories are done
|
|
2030
2082
|
let storyStateMachine;
|
|
2031
|
-
try {
|
|
2083
|
+
try {
|
|
2084
|
+
storyStateMachine = require('./lib/story-state-machine.js');
|
|
2085
|
+
} catch (e) {}
|
|
2032
2086
|
if (storyStateMachine && cache.status) {
|
|
2033
2087
|
try {
|
|
2034
2088
|
const statusPath = getStatusPath(rootDir);
|
|
@@ -2058,7 +2112,9 @@ async function main() {
|
|
|
2058
2112
|
|
|
2059
2113
|
// Ideation sync: mark ideas as implemented when linked epics complete
|
|
2060
2114
|
let syncIdeationStatus;
|
|
2061
|
-
try {
|
|
2115
|
+
try {
|
|
2116
|
+
syncIdeationStatus = require('./lib/sync-ideation-status.js');
|
|
2117
|
+
} catch (e) {}
|
|
2062
2118
|
if (syncIdeationStatus) {
|
|
2063
2119
|
try {
|
|
2064
2120
|
const syncResult = syncIdeationStatus.syncImplementedIdeas(rootDir);
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -165,6 +165,9 @@ configure_tmux_session() {
|
|
|
165
165
|
# Enable mouse support
|
|
166
166
|
tmux set-option -t "$target_session" mouse on
|
|
167
167
|
|
|
168
|
+
# Automatically renumber windows when one is closed (no gaps)
|
|
169
|
+
tmux set-option -t "$target_session" renumber-windows on
|
|
170
|
+
|
|
168
171
|
# Fix colors - proper terminal support
|
|
169
172
|
tmux set-option -t "$target_session" default-terminal "xterm-256color"
|
|
170
173
|
tmux set-option -t "$target_session" -ga terminal-overrides ",xterm-256color:Tc"
|
|
@@ -245,7 +248,8 @@ configure_tmux_session() {
|
|
|
245
248
|
|
|
246
249
|
# ─── Session Creation Keybindings ──────────────────────────────────────────
|
|
247
250
|
# Alt+s to create a new Claude window (starts fresh, future re-runs in same pane resume)
|
|
248
|
-
|
|
251
|
+
# Window gets sequential name (claude-2, claude-3, ...) so windows are distinguishable
|
|
252
|
+
tmux bind-key -n M-s run-shell "N=\$(( \$(tmux list-windows -F '#{window_name}' 2>/dev/null | grep -c '^claude') + 1 )); tmux new-window -n \"claude-\$N\" -c '#{pane_current_path}' && tmux send-keys '\"\$AGILEFLOW_SCRIPTS/claude-smart.sh\" --fresh \$CLAUDE_SESSION_FLAGS' Enter"
|
|
249
253
|
|
|
250
254
|
# ─── Freeze Recovery Keybindings ───────────────────────────────────────────
|
|
251
255
|
# Alt+k to send Ctrl+C twice (soft interrupt for frozen processes)
|
|
@@ -253,7 +257,7 @@ configure_tmux_session() {
|
|
|
253
257
|
|
|
254
258
|
# ─── Help Panel ──────────────────────────────────────────────────────────
|
|
255
259
|
# Alt+h to show all Alt keybindings in a popup
|
|
256
|
-
tmux bind-key -n M-h display-popup -E -w 52 -h
|
|
260
|
+
tmux bind-key -n M-h display-popup -E -w 52 -h 30 "\
|
|
257
261
|
printf '\\n';\
|
|
258
262
|
printf ' \\033[1;38;5;208mSESSIONS\\033[0m\\n';\
|
|
259
263
|
printf ' Alt+s New Claude session\\n';\
|
|
@@ -272,7 +276,9 @@ configure_tmux_session() {
|
|
|
272
276
|
printf ' Alt+v Split top / bottom\\n';\
|
|
273
277
|
printf ' Alt+arrows Navigate panes\\n';\
|
|
274
278
|
printf ' Alt+z Zoom / unzoom\\n';\
|
|
275
|
-
printf ' Alt+x Close pane\\n';\
|
|
279
|
+
printf ' Alt+x Close pane (confirm)\\n';\
|
|
280
|
+
printf ' Alt+K Kill pane (no confirm)\\n';\
|
|
281
|
+
printf ' Alt+R Restart pane\\n';\
|
|
276
282
|
printf '\\n';\
|
|
277
283
|
printf ' \\033[1;38;5;208mOTHER\\033[0m\\n';\
|
|
278
284
|
printf ' Alt+[ Scroll mode\\n';\
|
|
@@ -354,7 +360,7 @@ fi
|
|
|
354
360
|
# Silently remove sessions where all panes have exited (dead/empty shells).
|
|
355
361
|
# This prevents accumulation of orphan sessions over time.
|
|
356
362
|
SESSION_BASE="claude-${DIR_NAME}"
|
|
357
|
-
for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}
|
|
363
|
+
for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)"); do
|
|
358
364
|
# Count alive panes (pane_dead=0 means alive)
|
|
359
365
|
ALIVE=$(tmux list-panes -t "$sid" -F '#{pane_dead}' 2>/dev/null | grep -c '^0$' || true)
|
|
360
366
|
if [ "$ALIVE" = "0" ]; then
|
|
@@ -362,6 +368,36 @@ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESS
|
|
|
362
368
|
fi
|
|
363
369
|
done
|
|
364
370
|
|
|
371
|
+
# ── Consolidate duplicate sessions ───────────────────────────────────────
|
|
372
|
+
# Kill numbered duplicates (e.g. claude-Acuide-2, -3) that were created by
|
|
373
|
+
# a previous bug. If the base session exists, duplicates are unnecessary.
|
|
374
|
+
# If only numbered sessions remain, promote the lowest to the base name.
|
|
375
|
+
if [ "$FORCE_NEW" = false ]; then
|
|
376
|
+
HAS_BASE=false
|
|
377
|
+
NUMBERED=()
|
|
378
|
+
if tmux has-session -t "$SESSION_BASE" 2>/dev/null; then
|
|
379
|
+
HAS_BASE=true
|
|
380
|
+
fi
|
|
381
|
+
while IFS= read -r sid; do
|
|
382
|
+
[ -n "$sid" ] && NUMBERED+=("$sid")
|
|
383
|
+
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}-[0-9]+$" | sort -t- -k3 -n)
|
|
384
|
+
|
|
385
|
+
if [ "$HAS_BASE" = true ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
|
|
386
|
+
# Base exists — kill all numbered duplicates
|
|
387
|
+
for sid in "${NUMBERED[@]}"; do
|
|
388
|
+
tmux kill-session -t "$sid" 2>/dev/null || true
|
|
389
|
+
done
|
|
390
|
+
elif [ "$HAS_BASE" = false ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
|
|
391
|
+
# No base — promote lowest numbered session to base name
|
|
392
|
+
PROMOTE="${NUMBERED[0]}"
|
|
393
|
+
tmux rename-session -t "$PROMOTE" "$SESSION_BASE" 2>/dev/null || true
|
|
394
|
+
# Kill remaining duplicates
|
|
395
|
+
for sid in "${NUMBERED[@]:1}"; do
|
|
396
|
+
tmux kill-session -t "$sid" 2>/dev/null || true
|
|
397
|
+
done
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
|
|
365
401
|
# ── Auto-reattach to detached session ──────────────────────────────────────
|
|
366
402
|
# When user does Alt+Q (detach) and then runs `af` again, reattach to the
|
|
367
403
|
# existing session instead of creating a new one. This preserves tmux windows,
|
|
@@ -371,7 +407,7 @@ if [ "$FORCE_NEW" = false ]; then
|
|
|
371
407
|
DETACHED=()
|
|
372
408
|
while IFS= read -r sid; do
|
|
373
409
|
[ -n "$sid" ] && DETACHED+=("$sid")
|
|
374
|
-
done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep "^${SESSION_BASE}
|
|
410
|
+
done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
|
|
375
411
|
|
|
376
412
|
if [ "${#DETACHED[@]}" -eq 1 ]; then
|
|
377
413
|
# Single detached session — just reattach
|
|
@@ -410,7 +446,7 @@ if [ "$FORCE_NEW" = false ]; then
|
|
|
410
446
|
EXISTING=()
|
|
411
447
|
while IFS= read -r sid; do
|
|
412
448
|
[ -n "$sid" ] && EXISTING+=("$sid")
|
|
413
|
-
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}
|
|
449
|
+
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
|
|
414
450
|
|
|
415
451
|
if [ "${#EXISTING[@]}" -gt 0 ]; then
|
|
416
452
|
# Prefer the base session, otherwise pick the first one
|
|
@@ -48,8 +48,12 @@ if (!utils || typeof utils.runDamageControlHook !== 'function') {
|
|
|
48
48
|
|
|
49
49
|
// Tools this hook handles
|
|
50
50
|
const MULTI_AGENT_TOOLS = [
|
|
51
|
-
'TeamCreate',
|
|
52
|
-
'
|
|
51
|
+
'TeamCreate',
|
|
52
|
+
'TeamDelete',
|
|
53
|
+
'TaskCreate',
|
|
54
|
+
'TaskUpdate',
|
|
55
|
+
'TaskGet',
|
|
56
|
+
'TaskList',
|
|
53
57
|
'SendMessage',
|
|
54
58
|
];
|
|
55
59
|
|
|
@@ -65,10 +69,10 @@ const MAX_MESSAGE_SIZE = 10240;
|
|
|
65
69
|
// Blocked patterns in SendMessage content
|
|
66
70
|
const BLOCKED_MESSAGE_PATTERNS = [
|
|
67
71
|
// Command injection attempts
|
|
68
|
-
/\$\{.*\}/,
|
|
69
|
-
/`[^`]*`/,
|
|
70
|
-
/\bexec\s*\(/,
|
|
71
|
-
/\beval\s*\(/,
|
|
72
|
+
/\$\{.*\}/, // Template injection ${...}
|
|
73
|
+
/`[^`]*`/, // Backtick execution
|
|
74
|
+
/\bexec\s*\(/, // exec() calls
|
|
75
|
+
/\beval\s*\(/, // eval() calls
|
|
72
76
|
// Dangerous git operations
|
|
73
77
|
/\bgit\s+push\s+--force\b/i,
|
|
74
78
|
/\bgit\s+reset\s+--hard\b/i,
|
|
@@ -143,9 +147,9 @@ function validateTaskOperation(input) {
|
|
|
143
147
|
// Check for secrets in task descriptions
|
|
144
148
|
const secretPatterns = [
|
|
145
149
|
/\b(?:API_KEY|SECRET|PASSWORD|TOKEN|CREDENTIALS)\s*[:=]\s*\S+/i,
|
|
146
|
-
/\bsk-[a-zA-Z0-9]{20,}/,
|
|
147
|
-
/\bghp_[a-zA-Z0-9]{36}/,
|
|
148
|
-
/\bnpm_[a-zA-Z0-9]{36}/,
|
|
150
|
+
/\bsk-[a-zA-Z0-9]{20,}/, // API keys starting with sk-
|
|
151
|
+
/\bghp_[a-zA-Z0-9]{36}/, // GitHub personal access tokens
|
|
152
|
+
/\bnpm_[a-zA-Z0-9]{36}/, // npm tokens
|
|
149
153
|
];
|
|
150
154
|
|
|
151
155
|
for (const pattern of secretPatterns) {
|
|
@@ -163,7 +167,7 @@ function validateTaskOperation(input) {
|
|
|
163
167
|
|
|
164
168
|
try {
|
|
165
169
|
utils.runDamageControlHook({
|
|
166
|
-
getInputValue:
|
|
170
|
+
getInputValue: input => {
|
|
167
171
|
// Check if this is a multi-agent tool
|
|
168
172
|
const toolName = input.tool_name || '';
|
|
169
173
|
if (!MULTI_AGENT_TOOLS.includes(toolName)) {
|
package/scripts/lib/bus-utils.js
CHANGED
|
@@ -341,7 +341,9 @@ function formatLogStats(stats) {
|
|
|
341
341
|
lines.push('Archives:');
|
|
342
342
|
if (stats.archives && stats.archives.length > 0) {
|
|
343
343
|
for (const archive of stats.archives) {
|
|
344
|
-
lines.push(
|
|
344
|
+
lines.push(
|
|
345
|
+
` ${archive.filename}: ${archive.lineCount} lines (${Math.round(archive.size / 1024)} KB)`
|
|
346
|
+
);
|
|
345
347
|
}
|
|
346
348
|
}
|
|
347
349
|
|
|
@@ -107,10 +107,15 @@ function detectConfig(version) {
|
|
|
107
107
|
// Git detection
|
|
108
108
|
if (fs.existsSync('.git')) {
|
|
109
109
|
status.git.initialized = true;
|
|
110
|
-
status.git.remote =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
status.git.remote =
|
|
111
|
+
tryOptional(
|
|
112
|
+
() =>
|
|
113
|
+
execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
|
+
}).trim(),
|
|
117
|
+
'git remote'
|
|
118
|
+
) ?? null;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
// Settings file detection
|
|
@@ -312,17 +317,15 @@ function detectMetadata(status, version) {
|
|
|
312
317
|
|
|
313
318
|
// Content-based outdated detection
|
|
314
319
|
const featureConfig = FEATURES[statusKey];
|
|
315
|
-
const scriptsToCheck =
|
|
316
|
-
|| (featureConfig?.script ? [featureConfig.script] : []);
|
|
320
|
+
const scriptsToCheck =
|
|
321
|
+
featureConfig?.scripts || (featureConfig?.script ? [featureConfig.script] : []);
|
|
317
322
|
|
|
318
323
|
if (scriptsToCheck.length > 0 && packageScriptDir) {
|
|
319
324
|
// Compare installed scripts against package source
|
|
320
325
|
let isOutdated = false;
|
|
321
326
|
for (const scriptName of scriptsToCheck) {
|
|
322
327
|
const packageScript = path.join(packageScriptDir, scriptName);
|
|
323
|
-
const installedScript = path.join(
|
|
324
|
-
process.cwd(), '.agileflow', 'scripts', scriptName
|
|
325
|
-
);
|
|
328
|
+
const installedScript = path.join(process.cwd(), '.agileflow', 'scripts', scriptName);
|
|
326
329
|
const packageHash = hashFile(packageScript);
|
|
327
330
|
const installedHash = hashFile(installedScript);
|
|
328
331
|
|