agileflow 3.0.0 → 3.0.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 +5 -0
- package/README.md +3 -3
- package/lib/dashboard-server.js +131 -50
- package/package.json +1 -1
- package/scripts/archive-completed-stories.sh +3 -0
- package/scripts/ci-summary.js +294 -0
- package/scripts/claude-smart.sh +18 -0
- package/scripts/claude-tmux.sh +43 -19
- package/scripts/lib/configure-detect.js +82 -4
- package/scripts/lib/configure-features.js +72 -10
- package/scripts/precompact-context.sh +3 -0
- package/src/core/commands/configure.md +44 -27
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.0.1] - 2026-02-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Configure writes directly to Claude Code settings.json for Agent Teams and permissions
|
|
14
|
+
|
|
10
15
|
## [3.0.0] - 2026-02-13
|
|
11
16
|
|
|
12
17
|
### Added
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](docs/04-architecture/commands.md)
|
|
7
7
|
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](docs/04-architecture/skills.md)
|
|
9
9
|
|
|
@@ -65,7 +65,7 @@ AgileFlow combines three proven methodologies:
|
|
|
65
65
|
|
|
66
66
|
| Component | Count | Description |
|
|
67
67
|
|-----------|-------|-------------|
|
|
68
|
-
| [Commands](docs/04-architecture/commands.md) |
|
|
68
|
+
| [Commands](docs/04-architecture/commands.md) | 93 | Slash commands for agile workflows |
|
|
69
69
|
| [Agents/Experts](docs/04-architecture/subagents.md) | 47 | Specialized agents with self-improving knowledge bases |
|
|
70
70
|
| [Skills](docs/04-architecture/skills.md) | Dynamic | Generated on-demand with `/agileflow:skill:create` |
|
|
71
71
|
|
|
@@ -76,7 +76,7 @@ AgileFlow combines three proven methodologies:
|
|
|
76
76
|
Full documentation lives in [`docs/04-architecture/`](docs/04-architecture/):
|
|
77
77
|
|
|
78
78
|
### Reference
|
|
79
|
-
- [Commands](docs/04-architecture/commands.md) - All
|
|
79
|
+
- [Commands](docs/04-architecture/commands.md) - All 93 slash commands
|
|
80
80
|
- [Agents/Experts](docs/04-architecture/subagents.md) - 47 specialized agents with self-improving knowledge
|
|
81
81
|
- [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
|
|
82
82
|
|
package/lib/dashboard-server.js
CHANGED
|
@@ -26,12 +26,30 @@ const { EventEmitter } = require('events');
|
|
|
26
26
|
// Lazy-loaded dependencies - deferred until first use
|
|
27
27
|
let _http, _crypto, _protocol, _paths, _validatePaths, _childProcess;
|
|
28
28
|
|
|
29
|
-
function getHttp() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
29
|
+
function getHttp() {
|
|
30
|
+
if (!_http) _http = require('http');
|
|
31
|
+
return _http;
|
|
32
|
+
}
|
|
33
|
+
function getCrypto() {
|
|
34
|
+
if (!_crypto) _crypto = require('crypto');
|
|
35
|
+
return _crypto;
|
|
36
|
+
}
|
|
37
|
+
function getProtocol() {
|
|
38
|
+
if (!_protocol) _protocol = require('./dashboard-protocol');
|
|
39
|
+
return _protocol;
|
|
40
|
+
}
|
|
41
|
+
function getPaths() {
|
|
42
|
+
if (!_paths) _paths = require('./paths');
|
|
43
|
+
return _paths;
|
|
44
|
+
}
|
|
45
|
+
function getValidatePaths() {
|
|
46
|
+
if (!_validatePaths) _validatePaths = require('./validate-paths');
|
|
47
|
+
return _validatePaths;
|
|
48
|
+
}
|
|
49
|
+
function getChildProcess() {
|
|
50
|
+
if (!_childProcess) _childProcess = require('child_process');
|
|
51
|
+
return _childProcess;
|
|
52
|
+
}
|
|
35
53
|
|
|
36
54
|
// Lazy-load automation modules to avoid circular dependencies
|
|
37
55
|
let AutomationRegistry = null;
|
|
@@ -294,7 +312,9 @@ class TerminalInstance {
|
|
|
294
312
|
this.pty.on('error', error => {
|
|
295
313
|
console.error('[Terminal] Shell error:', error.message);
|
|
296
314
|
if (!this.closed) {
|
|
297
|
-
this.session.send(
|
|
315
|
+
this.session.send(
|
|
316
|
+
getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`)
|
|
317
|
+
);
|
|
298
318
|
}
|
|
299
319
|
});
|
|
300
320
|
|
|
@@ -558,7 +578,9 @@ class DashboardServer extends EventEmitter {
|
|
|
558
578
|
|
|
559
579
|
this._automationRunner.on('failed', ({ automationId, result }) => {
|
|
560
580
|
this._runningAutomations.delete(automationId);
|
|
561
|
-
this.broadcast(
|
|
581
|
+
this.broadcast(
|
|
582
|
+
getProtocol().createAutomationStatus(automationId, 'error', { error: result.error })
|
|
583
|
+
);
|
|
562
584
|
|
|
563
585
|
// Add failure to inbox
|
|
564
586
|
this._addToInbox(automationId, result);
|
|
@@ -732,7 +754,7 @@ class DashboardServer extends EventEmitter {
|
|
|
732
754
|
|
|
733
755
|
// Complete WebSocket handshake
|
|
734
756
|
const key = req.headers['sec-websocket-key'];
|
|
735
|
-
const acceptKey =
|
|
757
|
+
const acceptKey = getCrypto()
|
|
736
758
|
.createHash('sha1')
|
|
737
759
|
.update(key + WS_GUID)
|
|
738
760
|
.digest('base64');
|
|
@@ -865,7 +887,9 @@ class DashboardServer extends EventEmitter {
|
|
|
865
887
|
handleMessage(session, data) {
|
|
866
888
|
// Rate limit incoming messages
|
|
867
889
|
if (!session.checkRateLimit()) {
|
|
868
|
-
session.send(
|
|
890
|
+
session.send(
|
|
891
|
+
getProtocol().createError('RATE_LIMITED', 'Too many messages, please slow down')
|
|
892
|
+
);
|
|
869
893
|
return;
|
|
870
894
|
}
|
|
871
895
|
|
|
@@ -1059,7 +1083,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1059
1083
|
switch (type) {
|
|
1060
1084
|
case getProtocol().InboundMessageType.GIT_STAGE:
|
|
1061
1085
|
if (fileArgs) {
|
|
1062
|
-
getChildProcess().execFileSync('git', ['add', '--', ...fileArgs], {
|
|
1086
|
+
getChildProcess().execFileSync('git', ['add', '--', ...fileArgs], {
|
|
1087
|
+
cwd: this.projectRoot,
|
|
1088
|
+
});
|
|
1063
1089
|
} else {
|
|
1064
1090
|
getChildProcess().execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
|
|
1065
1091
|
}
|
|
@@ -1070,24 +1096,32 @@ class DashboardServer extends EventEmitter {
|
|
|
1070
1096
|
cwd: this.projectRoot,
|
|
1071
1097
|
});
|
|
1072
1098
|
} else {
|
|
1073
|
-
getChildProcess().execFileSync('git', ['restore', '--staged', '.'], {
|
|
1099
|
+
getChildProcess().execFileSync('git', ['restore', '--staged', '.'], {
|
|
1100
|
+
cwd: this.projectRoot,
|
|
1101
|
+
});
|
|
1074
1102
|
}
|
|
1075
1103
|
break;
|
|
1076
1104
|
case getProtocol().InboundMessageType.GIT_REVERT:
|
|
1077
1105
|
if (fileArgs) {
|
|
1078
|
-
getChildProcess().execFileSync('git', ['checkout', '--', ...fileArgs], {
|
|
1106
|
+
getChildProcess().execFileSync('git', ['checkout', '--', ...fileArgs], {
|
|
1107
|
+
cwd: this.projectRoot,
|
|
1108
|
+
});
|
|
1079
1109
|
}
|
|
1080
1110
|
break;
|
|
1081
1111
|
case getProtocol().InboundMessageType.GIT_COMMIT:
|
|
1082
1112
|
if (commitMessage) {
|
|
1083
|
-
getChildProcess().execFileSync('git', ['commit', '-m', commitMessage], {
|
|
1113
|
+
getChildProcess().execFileSync('git', ['commit', '-m', commitMessage], {
|
|
1114
|
+
cwd: this.projectRoot,
|
|
1115
|
+
});
|
|
1084
1116
|
}
|
|
1085
1117
|
break;
|
|
1086
1118
|
}
|
|
1087
1119
|
|
|
1088
1120
|
// Send updated git status
|
|
1089
1121
|
this.sendGitStatus(session);
|
|
1090
|
-
session.send(
|
|
1122
|
+
session.send(
|
|
1123
|
+
getProtocol().createNotification('success', 'Git', `${type.replace('git_', '')} completed`)
|
|
1124
|
+
);
|
|
1091
1125
|
} catch (error) {
|
|
1092
1126
|
console.error('[Git Error]', error.message);
|
|
1093
1127
|
session.send(getProtocol().createError('GIT_ERROR', 'Git operation failed'));
|
|
@@ -1115,11 +1149,13 @@ class DashboardServer extends EventEmitter {
|
|
|
1115
1149
|
*/
|
|
1116
1150
|
getGitStatus() {
|
|
1117
1151
|
try {
|
|
1118
|
-
const branch = getChildProcess()
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1152
|
+
const branch = getChildProcess()
|
|
1153
|
+
.execFileSync('git', ['branch', '--show-current'], {
|
|
1154
|
+
cwd: this.projectRoot,
|
|
1155
|
+
encoding: 'utf8',
|
|
1156
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1157
|
+
})
|
|
1158
|
+
.trim();
|
|
1123
1159
|
|
|
1124
1160
|
const statusOutput = getChildProcess().execFileSync('git', ['status', '--porcelain'], {
|
|
1125
1161
|
cwd: this.projectRoot,
|
|
@@ -1209,7 +1245,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1209
1245
|
*/
|
|
1210
1246
|
getFileDiff(filePath, staged = false) {
|
|
1211
1247
|
// Validate filePath stays within project root
|
|
1212
|
-
const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
|
|
1248
|
+
const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
|
|
1249
|
+
allowSymlinks: true,
|
|
1250
|
+
});
|
|
1213
1251
|
if (!pathResult.ok) {
|
|
1214
1252
|
return '';
|
|
1215
1253
|
}
|
|
@@ -1224,10 +1262,12 @@ class DashboardServer extends EventEmitter {
|
|
|
1224
1262
|
|
|
1225
1263
|
// If no diff, file might be untracked - show entire file content as addition
|
|
1226
1264
|
if (!diff && !staged) {
|
|
1227
|
-
const statusOutput = getChildProcess()
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1265
|
+
const statusOutput = getChildProcess()
|
|
1266
|
+
.execFileSync('git', ['status', '--porcelain', '--', filePath], {
|
|
1267
|
+
cwd: this.projectRoot,
|
|
1268
|
+
encoding: 'utf8',
|
|
1269
|
+
})
|
|
1270
|
+
.trim();
|
|
1231
1271
|
|
|
1232
1272
|
// Check if file is untracked
|
|
1233
1273
|
if (statusOutput.startsWith('??')) {
|
|
@@ -1398,23 +1438,23 @@ class DashboardServer extends EventEmitter {
|
|
|
1398
1438
|
// Get branch and sync status via git
|
|
1399
1439
|
try {
|
|
1400
1440
|
const cwd = s.metadata.worktreePath || this.projectRoot;
|
|
1401
|
-
entry.branch = getChildProcess()
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1441
|
+
entry.branch = getChildProcess()
|
|
1442
|
+
.execFileSync('git', ['branch', '--show-current'], {
|
|
1443
|
+
cwd,
|
|
1444
|
+
encoding: 'utf8',
|
|
1445
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1446
|
+
})
|
|
1447
|
+
.trim();
|
|
1406
1448
|
|
|
1407
1449
|
// Get ahead/behind counts relative to upstream
|
|
1408
1450
|
try {
|
|
1409
|
-
const counts = getChildProcess()
|
|
1410
|
-
'git',
|
|
1411
|
-
['rev-list', '--left-right', '--count', 'HEAD...@{u}'],
|
|
1412
|
-
{
|
|
1451
|
+
const counts = getChildProcess()
|
|
1452
|
+
.execFileSync('git', ['rev-list', '--left-right', '--count', 'HEAD...@{u}'], {
|
|
1413
1453
|
cwd,
|
|
1414
1454
|
encoding: 'utf8',
|
|
1415
1455
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1456
|
+
})
|
|
1457
|
+
.trim();
|
|
1418
1458
|
const [ahead, behind] = counts.split(/\s+/).map(Number);
|
|
1419
1459
|
entry.ahead = ahead || 0;
|
|
1420
1460
|
entry.behind = behind || 0;
|
|
@@ -1454,7 +1494,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1454
1494
|
}
|
|
1455
1495
|
|
|
1456
1496
|
// Validate the path stays within project root
|
|
1457
|
-
const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
|
|
1497
|
+
const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
|
|
1498
|
+
allowSymlinks: true,
|
|
1499
|
+
});
|
|
1458
1500
|
if (!pathResult.ok) {
|
|
1459
1501
|
session.send(getProtocol().createError('OPEN_FILE_ERROR', 'File path outside project'));
|
|
1460
1502
|
return;
|
|
@@ -1474,7 +1516,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1474
1516
|
case 'cursor':
|
|
1475
1517
|
case 'windsurf': {
|
|
1476
1518
|
const gotoArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
|
|
1477
|
-
getChildProcess()
|
|
1519
|
+
getChildProcess()
|
|
1520
|
+
.spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' })
|
|
1521
|
+
.unref();
|
|
1478
1522
|
break;
|
|
1479
1523
|
}
|
|
1480
1524
|
case 'subl':
|
|
@@ -1491,11 +1535,17 @@ class DashboardServer extends EventEmitter {
|
|
|
1491
1535
|
}
|
|
1492
1536
|
|
|
1493
1537
|
session.send(
|
|
1494
|
-
getProtocol().createNotification(
|
|
1538
|
+
getProtocol().createNotification(
|
|
1539
|
+
'info',
|
|
1540
|
+
'Editor',
|
|
1541
|
+
`Opened ${require('path').basename(fullPath)}`
|
|
1542
|
+
)
|
|
1495
1543
|
);
|
|
1496
1544
|
} catch (error) {
|
|
1497
1545
|
console.error('[Open File Error]', error.message);
|
|
1498
|
-
session.send(
|
|
1546
|
+
session.send(
|
|
1547
|
+
getProtocol().createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`)
|
|
1548
|
+
);
|
|
1499
1549
|
}
|
|
1500
1550
|
}
|
|
1501
1551
|
|
|
@@ -1508,10 +1558,15 @@ class DashboardServer extends EventEmitter {
|
|
|
1508
1558
|
// Validate cwd stays within project root
|
|
1509
1559
|
let safeCwd = this.projectRoot;
|
|
1510
1560
|
if (cwd) {
|
|
1511
|
-
const cwdResult = getValidatePaths().validatePath(cwd, this.projectRoot, {
|
|
1561
|
+
const cwdResult = getValidatePaths().validatePath(cwd, this.projectRoot, {
|
|
1562
|
+
allowSymlinks: true,
|
|
1563
|
+
});
|
|
1512
1564
|
if (!cwdResult.ok) {
|
|
1513
1565
|
session.send(
|
|
1514
|
-
getProtocol().createError(
|
|
1566
|
+
getProtocol().createError(
|
|
1567
|
+
'TERMINAL_ERROR',
|
|
1568
|
+
'Working directory must be within project root'
|
|
1569
|
+
)
|
|
1515
1570
|
);
|
|
1516
1571
|
return;
|
|
1517
1572
|
}
|
|
@@ -1684,7 +1739,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1684
1739
|
}
|
|
1685
1740
|
|
|
1686
1741
|
if (!this._automationRunner) {
|
|
1687
|
-
session.send(
|
|
1742
|
+
session.send(
|
|
1743
|
+
getProtocol().createError('AUTOMATION_ERROR', 'Automation runner not initialized')
|
|
1744
|
+
);
|
|
1688
1745
|
return;
|
|
1689
1746
|
}
|
|
1690
1747
|
|
|
@@ -1692,12 +1749,18 @@ class DashboardServer extends EventEmitter {
|
|
|
1692
1749
|
// Check if already running
|
|
1693
1750
|
if (this._runningAutomations.has(automationId)) {
|
|
1694
1751
|
session.send(
|
|
1695
|
-
getProtocol().createNotification(
|
|
1752
|
+
getProtocol().createNotification(
|
|
1753
|
+
'warning',
|
|
1754
|
+
'Automation',
|
|
1755
|
+
`${automationId} is already running`
|
|
1756
|
+
)
|
|
1696
1757
|
);
|
|
1697
1758
|
return;
|
|
1698
1759
|
}
|
|
1699
1760
|
|
|
1700
|
-
session.send(
|
|
1761
|
+
session.send(
|
|
1762
|
+
getProtocol().createNotification('info', 'Automation', `Starting ${automationId}...`)
|
|
1763
|
+
);
|
|
1701
1764
|
|
|
1702
1765
|
// Run the automation (async)
|
|
1703
1766
|
const result = await this._automationRunner.run(automationId);
|
|
@@ -1705,23 +1768,39 @@ class DashboardServer extends EventEmitter {
|
|
|
1705
1768
|
// Send result notification
|
|
1706
1769
|
if (result.success) {
|
|
1707
1770
|
session.send(
|
|
1708
|
-
getProtocol().createNotification(
|
|
1771
|
+
getProtocol().createNotification(
|
|
1772
|
+
'success',
|
|
1773
|
+
'Automation',
|
|
1774
|
+
`${automationId} completed successfully`
|
|
1775
|
+
)
|
|
1709
1776
|
);
|
|
1710
1777
|
} else {
|
|
1711
1778
|
session.send(
|
|
1712
|
-
getProtocol().createNotification(
|
|
1779
|
+
getProtocol().createNotification(
|
|
1780
|
+
'error',
|
|
1781
|
+
'Automation',
|
|
1782
|
+
`${automationId} failed: ${result.error}`
|
|
1783
|
+
)
|
|
1713
1784
|
);
|
|
1714
1785
|
}
|
|
1715
1786
|
|
|
1716
1787
|
// Send final status
|
|
1717
|
-
session.send(
|
|
1788
|
+
session.send(
|
|
1789
|
+
getProtocol().createAutomationStatus(
|
|
1790
|
+
automationId,
|
|
1791
|
+
result.success ? 'idle' : 'error',
|
|
1792
|
+
result
|
|
1793
|
+
)
|
|
1794
|
+
);
|
|
1718
1795
|
|
|
1719
1796
|
// Refresh the list
|
|
1720
1797
|
this.sendAutomationList(session);
|
|
1721
1798
|
} catch (error) {
|
|
1722
1799
|
console.error('[Automation Error]', error.message);
|
|
1723
1800
|
session.send(getProtocol().createError('AUTOMATION_ERROR', 'Automation execution failed'));
|
|
1724
|
-
session.send(
|
|
1801
|
+
session.send(
|
|
1802
|
+
getProtocol().createAutomationStatus(automationId, 'error', { error: 'Execution failed' })
|
|
1803
|
+
);
|
|
1725
1804
|
}
|
|
1726
1805
|
}
|
|
1727
1806
|
|
|
@@ -1782,7 +1861,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1782
1861
|
case 'accept':
|
|
1783
1862
|
// Mark as accepted and remove
|
|
1784
1863
|
item.status = 'accepted';
|
|
1785
|
-
session.send(
|
|
1864
|
+
session.send(
|
|
1865
|
+
getProtocol().createNotification('success', 'Inbox', `Accepted: ${item.title}`)
|
|
1866
|
+
);
|
|
1786
1867
|
this._inbox.delete(itemId);
|
|
1787
1868
|
break;
|
|
1788
1869
|
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ set -e
|
|
|
7
7
|
|
|
8
8
|
# Track start time for hook metrics
|
|
9
9
|
HOOK_START_TIME=$(date +%s%3N 2>/dev/null || date +%s)
|
|
10
|
+
# macOS date doesn't support %N - outputs literal "3N" instead of millis
|
|
11
|
+
[[ ! "$HOOK_START_TIME" =~ ^[0-9]+$ ]] && HOOK_START_TIME="$(date +%s)000"
|
|
10
12
|
|
|
11
13
|
# Source shared utilities
|
|
12
14
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -286,6 +288,7 @@ fi
|
|
|
286
288
|
# Record hook metrics
|
|
287
289
|
if command -v node &> /dev/null && [[ -f "$SCRIPT_DIR/lib/hook-metrics.js" ]]; then
|
|
288
290
|
HOOK_END_TIME=$(date +%s%3N 2>/dev/null || date +%s)
|
|
291
|
+
[[ ! "$HOOK_END_TIME" =~ ^[0-9]+$ ]] && HOOK_END_TIME="$(date +%s)000"
|
|
289
292
|
HOOK_DURATION=$((HOOK_END_TIME - HOOK_START_TIME))
|
|
290
293
|
PROJECT_ROOT="$PROJECT_ROOT" HOOK_DURATION="$HOOK_DURATION" node -e '
|
|
291
294
|
try {
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgileFlow CLI - CI Summary Script
|
|
5
|
+
*
|
|
6
|
+
* Summarizes CI/CD workflow failures from the past 24 hours.
|
|
7
|
+
* Uses GitHub CLI (gh) to fetch workflow run data.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/ci-summary.js [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --json Output results as JSON
|
|
14
|
+
* --hours=N Look back N hours (default: 24)
|
|
15
|
+
* --quiet Only show failures
|
|
16
|
+
* --help Show help
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { execFileSync } = require('child_process');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ANSI colors
|
|
23
|
+
const c = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
bold: '\x1b[1m',
|
|
26
|
+
dim: '\x1b[2m',
|
|
27
|
+
red: '\x1b[31m',
|
|
28
|
+
green: '\x1b[32m',
|
|
29
|
+
yellow: '\x1b[33m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse command line arguments
|
|
35
|
+
*/
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const options = {
|
|
38
|
+
json: false,
|
|
39
|
+
hours: 24,
|
|
40
|
+
quiet: false,
|
|
41
|
+
help: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (const arg of args) {
|
|
45
|
+
if (arg === '--json') options.json = true;
|
|
46
|
+
else if (arg === '--quiet') options.quiet = true;
|
|
47
|
+
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
48
|
+
else if (arg.startsWith('--hours=')) {
|
|
49
|
+
const hours = parseInt(arg.split('=')[1], 10);
|
|
50
|
+
if (!isNaN(hours) && hours > 0) {
|
|
51
|
+
options.hours = hours;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Show help message
|
|
61
|
+
*/
|
|
62
|
+
function showHelp() {
|
|
63
|
+
console.log(`
|
|
64
|
+
${c.bold}AgileFlow CI Summary${c.reset}
|
|
65
|
+
|
|
66
|
+
${c.cyan}Usage:${c.reset}
|
|
67
|
+
node scripts/ci-summary.js [options]
|
|
68
|
+
|
|
69
|
+
${c.cyan}Options:${c.reset}
|
|
70
|
+
--json Output results as JSON
|
|
71
|
+
--hours=N Look back N hours (default: 24)
|
|
72
|
+
--quiet Only show failures
|
|
73
|
+
--help, -h Show this help message
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if gh CLI is available
|
|
79
|
+
*/
|
|
80
|
+
function hasGhCli() {
|
|
81
|
+
try {
|
|
82
|
+
execFileSync('gh', ['--version'], { stdio: 'pipe' });
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if we're in a git repo with a GitHub remote
|
|
91
|
+
*/
|
|
92
|
+
function hasGitHubRemote() {
|
|
93
|
+
try {
|
|
94
|
+
const remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
}).trim();
|
|
98
|
+
return remote.includes('github.com');
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch workflow runs from GitHub
|
|
106
|
+
*/
|
|
107
|
+
function fetchWorkflowRuns(hours) {
|
|
108
|
+
try {
|
|
109
|
+
const output = execFileSync(
|
|
110
|
+
'gh',
|
|
111
|
+
[
|
|
112
|
+
'run',
|
|
113
|
+
'list',
|
|
114
|
+
'--limit',
|
|
115
|
+
'50',
|
|
116
|
+
'--json',
|
|
117
|
+
'databaseId,name,status,conclusion,createdAt,headBranch,event,url',
|
|
118
|
+
],
|
|
119
|
+
{ stdio: 'pipe', encoding: 'utf8', timeout: 30000 }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const runs = JSON.parse(output);
|
|
123
|
+
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
124
|
+
|
|
125
|
+
return runs.filter(run => new Date(run.createdAt) >= cutoff);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw new Error(`Failed to fetch workflow runs: ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate summary from runs
|
|
133
|
+
*/
|
|
134
|
+
function generateSummary(runs, hours) {
|
|
135
|
+
const summary = {
|
|
136
|
+
period_hours: hours,
|
|
137
|
+
total: runs.length,
|
|
138
|
+
successful: 0,
|
|
139
|
+
failed: 0,
|
|
140
|
+
cancelled: 0,
|
|
141
|
+
in_progress: 0,
|
|
142
|
+
failures: [],
|
|
143
|
+
workflows: {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (const run of runs) {
|
|
147
|
+
const workflowName = run.name || 'Unknown';
|
|
148
|
+
|
|
149
|
+
if (!summary.workflows[workflowName]) {
|
|
150
|
+
summary.workflows[workflowName] = { total: 0, passed: 0, failed: 0 };
|
|
151
|
+
}
|
|
152
|
+
summary.workflows[workflowName].total++;
|
|
153
|
+
|
|
154
|
+
if (run.conclusion === 'success') {
|
|
155
|
+
summary.successful++;
|
|
156
|
+
summary.workflows[workflowName].passed++;
|
|
157
|
+
} else if (run.conclusion === 'failure') {
|
|
158
|
+
summary.failed++;
|
|
159
|
+
summary.workflows[workflowName].failed++;
|
|
160
|
+
summary.failures.push({
|
|
161
|
+
workflow: workflowName,
|
|
162
|
+
branch: run.headBranch,
|
|
163
|
+
event: run.event,
|
|
164
|
+
created: run.createdAt,
|
|
165
|
+
url: run.url,
|
|
166
|
+
});
|
|
167
|
+
} else if (run.conclusion === 'cancelled') {
|
|
168
|
+
summary.cancelled++;
|
|
169
|
+
} else if (run.status === 'in_progress' || run.status === 'queued') {
|
|
170
|
+
summary.in_progress++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return summary;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format summary for console output
|
|
179
|
+
*/
|
|
180
|
+
function formatSummary(summary, quiet) {
|
|
181
|
+
const lines = [];
|
|
182
|
+
|
|
183
|
+
if (!quiet) {
|
|
184
|
+
lines.push(`${c.bold}CI Summary (last ${summary.period_hours}h)${c.reset}`);
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (summary.total === 0) {
|
|
189
|
+
lines.push(`${c.dim}No workflow runs in the past ${summary.period_hours} hours.${c.reset}`);
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!quiet) {
|
|
194
|
+
lines.push(` Total runs: ${summary.total}`);
|
|
195
|
+
lines.push(` ${c.green}Successful:${c.reset} ${summary.successful}`);
|
|
196
|
+
if (summary.failed > 0) {
|
|
197
|
+
lines.push(` ${c.red}Failed:${c.reset} ${summary.failed}`);
|
|
198
|
+
}
|
|
199
|
+
if (summary.cancelled > 0) {
|
|
200
|
+
lines.push(` ${c.yellow}Cancelled:${c.reset} ${summary.cancelled}`);
|
|
201
|
+
}
|
|
202
|
+
if (summary.in_progress > 0) {
|
|
203
|
+
lines.push(` ${c.cyan}In progress:${c.reset} ${summary.in_progress}`);
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Show failures
|
|
209
|
+
if (summary.failures.length > 0) {
|
|
210
|
+
lines.push(`${c.red}${c.bold}Failures:${c.reset}`);
|
|
211
|
+
for (const failure of summary.failures) {
|
|
212
|
+
lines.push(
|
|
213
|
+
` ${c.red}x${c.reset} ${failure.workflow} ${c.dim}(${failure.branch}, ${failure.event})${c.reset}`
|
|
214
|
+
);
|
|
215
|
+
if (failure.url) {
|
|
216
|
+
lines.push(` ${c.dim}${failure.url}${c.reset}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Workflow breakdown (non-quiet)
|
|
223
|
+
if (!quiet) {
|
|
224
|
+
const workflowNames = Object.keys(summary.workflows);
|
|
225
|
+
if (workflowNames.length > 0) {
|
|
226
|
+
lines.push(`${c.bold}Workflows:${c.reset}`);
|
|
227
|
+
for (const name of workflowNames) {
|
|
228
|
+
const w = summary.workflows[name];
|
|
229
|
+
const status = w.failed > 0 ? c.red : c.green;
|
|
230
|
+
lines.push(` ${status}${name}${c.reset}: ${w.passed}/${w.total} passed`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Main entry point
|
|
240
|
+
*/
|
|
241
|
+
function main() {
|
|
242
|
+
const options = parseArgs(process.argv.slice(2));
|
|
243
|
+
|
|
244
|
+
if (options.help) {
|
|
245
|
+
showHelp();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Pre-flight checks
|
|
250
|
+
if (!hasGhCli()) {
|
|
251
|
+
if (options.json) {
|
|
252
|
+
console.log(JSON.stringify({ error: 'GitHub CLI (gh) not installed', runs: [] }));
|
|
253
|
+
} else {
|
|
254
|
+
console.log(
|
|
255
|
+
`${c.dim}CI Summary: GitHub CLI (gh) not available. Install from https://cli.github.com/${c.reset}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!hasGitHubRemote()) {
|
|
262
|
+
if (options.json) {
|
|
263
|
+
console.log(JSON.stringify({ error: 'No GitHub remote found', runs: [] }));
|
|
264
|
+
} else {
|
|
265
|
+
console.log(`${c.dim}CI Summary: No GitHub remote detected. Skipping.${c.reset}`);
|
|
266
|
+
}
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Fetch and summarize
|
|
271
|
+
try {
|
|
272
|
+
const runs = fetchWorkflowRuns(options.hours);
|
|
273
|
+
const summary = generateSummary(runs, options.hours);
|
|
274
|
+
|
|
275
|
+
if (options.json) {
|
|
276
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
277
|
+
} else {
|
|
278
|
+
console.log(formatSummary(summary, options.quiet));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Always exit 0 when summary was generated successfully.
|
|
282
|
+
// The automation runner treats non-zero as script failure.
|
|
283
|
+
process.exit(0);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
if (options.json) {
|
|
286
|
+
console.log(JSON.stringify({ error: e.message, runs: [] }));
|
|
287
|
+
} else {
|
|
288
|
+
console.error(`${c.red}CI Summary error:${c.reset} ${e.message}`);
|
|
289
|
+
}
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
main();
|
package/scripts/claude-smart.sh
CHANGED
|
@@ -50,6 +50,24 @@ CMD+=("${ARGS[@]}")
|
|
|
50
50
|
"${CMD[@]}"
|
|
51
51
|
EXIT_CODE=$?
|
|
52
52
|
|
|
53
|
+
# ── Auto-retry on expired session ────────────────────────────────────────
|
|
54
|
+
# Exit code 1 with a stored UUID means Claude couldn't find the session
|
|
55
|
+
# in its internal index (even though the .jsonl file exists on disk).
|
|
56
|
+
if [ $EXIT_CODE -eq 1 ] && [ -n "$STORED_UUID" ]; then
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Session expired. Starting fresh..."
|
|
59
|
+
echo ""
|
|
60
|
+
# Clear stale UUID from tmux pane
|
|
61
|
+
if [ -n "$TMUX" ]; then
|
|
62
|
+
tmux set-option -p -u @claude_uuid 2>/dev/null || true
|
|
63
|
+
fi
|
|
64
|
+
# Retry without --resume
|
|
65
|
+
STORED_UUID=""
|
|
66
|
+
CMD=(claude "${ARGS[@]}")
|
|
67
|
+
"${CMD[@]}"
|
|
68
|
+
EXIT_CODE=$?
|
|
69
|
+
fi
|
|
70
|
+
|
|
53
71
|
# ── Capture UUID after exit ────────────────────────────────────────────────
|
|
54
72
|
# Store the most recent non-agent .jsonl as the pane's conversation UUID
|
|
55
73
|
if [ -n "$TMUX" ]; then
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -252,32 +252,32 @@ configure_tmux_session() {
|
|
|
252
252
|
tmux bind-key -n M-k run-shell "tmux send-keys C-c; sleep 0.5; tmux send-keys C-c"
|
|
253
253
|
|
|
254
254
|
# ─── Help Panel ──────────────────────────────────────────────────────────
|
|
255
|
-
# Alt+h to show
|
|
256
|
-
tmux bind-key -n M-h display-popup -E -w 52 -h
|
|
255
|
+
# Alt+h to show all Alt keybindings in a popup
|
|
256
|
+
tmux bind-key -n M-h display-popup -E -w 52 -h 26 "\
|
|
257
257
|
printf '\\n';\
|
|
258
258
|
printf ' \\033[1;38;5;208mSESSIONS\\033[0m\\n';\
|
|
259
|
-
printf ' Alt+s
|
|
260
|
-
printf ' Alt+l
|
|
261
|
-
printf ' Alt+q
|
|
259
|
+
printf ' Alt+s New Claude session\\n';\
|
|
260
|
+
printf ' Alt+l Switch session (picker)\\n';\
|
|
261
|
+
printf ' Alt+q Detach (af to resume)\\n';\
|
|
262
262
|
printf '\\n';\
|
|
263
263
|
printf ' \\033[1;38;5;208mWINDOWS\\033[0m\\n';\
|
|
264
|
-
printf ' Alt+
|
|
265
|
-
printf ' Alt+
|
|
266
|
-
printf ' Alt+n/p
|
|
267
|
-
printf ' Alt+r
|
|
268
|
-
printf ' Alt+w
|
|
264
|
+
printf ' Alt+c New empty window\\n';\
|
|
265
|
+
printf ' Alt+1-9 Switch to window N\\n';\
|
|
266
|
+
printf ' Alt+n/p Next / previous window\\n';\
|
|
267
|
+
printf ' Alt+r Rename window\\n';\
|
|
268
|
+
printf ' Alt+w Close window\\n';\
|
|
269
269
|
printf '\\n';\
|
|
270
270
|
printf ' \\033[1;38;5;208mPANES\\033[0m\\n';\
|
|
271
|
-
printf ' Alt+d
|
|
272
|
-
printf ' Alt+v
|
|
273
|
-
printf ' Alt
|
|
274
|
-
printf ' Alt+z
|
|
275
|
-
printf ' Alt+x
|
|
271
|
+
printf ' Alt+d Split side by side\\n';\
|
|
272
|
+
printf ' Alt+v Split top / bottom\\n';\
|
|
273
|
+
printf ' Alt+arrows Navigate panes\\n';\
|
|
274
|
+
printf ' Alt+z Zoom / unzoom\\n';\
|
|
275
|
+
printf ' Alt+x Close pane\\n';\
|
|
276
276
|
printf '\\n';\
|
|
277
277
|
printf ' \\033[1;38;5;208mOTHER\\033[0m\\n';\
|
|
278
|
-
printf ' Alt+[
|
|
279
|
-
printf ' Alt+k
|
|
280
|
-
printf ' Alt+h
|
|
278
|
+
printf ' Alt+[ Scroll mode\\n';\
|
|
279
|
+
printf ' Alt+k Unfreeze (Ctrl+C x2)\\n';\
|
|
280
|
+
printf ' Alt+h This help\\n';\
|
|
281
281
|
printf '\\n';\
|
|
282
282
|
read -n 1 -s -r -p ' Press any key to close'"
|
|
283
283
|
}
|
|
@@ -402,7 +402,31 @@ if [ "$FORCE_NEW" = false ]; then
|
|
|
402
402
|
fi
|
|
403
403
|
fi
|
|
404
404
|
|
|
405
|
-
#
|
|
405
|
+
# ── Reuse existing attached session ──────────────────────────────────────
|
|
406
|
+
# If a session exists but is attached in another terminal, attach to it
|
|
407
|
+
# as a second client rather than creating a duplicate session (e.g. -2).
|
|
408
|
+
# Use --new to force creating a separate session.
|
|
409
|
+
if [ "$FORCE_NEW" = false ]; then
|
|
410
|
+
EXISTING=()
|
|
411
|
+
while IFS= read -r sid; do
|
|
412
|
+
[ -n "$sid" ] && EXISTING+=("$sid")
|
|
413
|
+
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)")
|
|
414
|
+
|
|
415
|
+
if [ "${#EXISTING[@]}" -gt 0 ]; then
|
|
416
|
+
# Prefer the base session, otherwise pick the first one
|
|
417
|
+
TARGET="${EXISTING[0]}"
|
|
418
|
+
for sid in "${EXISTING[@]}"; do
|
|
419
|
+
if [ "$sid" = "$SESSION_BASE" ]; then
|
|
420
|
+
TARGET="$sid"
|
|
421
|
+
break
|
|
422
|
+
fi
|
|
423
|
+
done
|
|
424
|
+
echo "Attaching to existing session: $TARGET"
|
|
425
|
+
exec tmux attach-session -t "$TARGET"
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# No existing session found — create a new one
|
|
406
430
|
SESSION_NAME="$SESSION_BASE"
|
|
407
431
|
SESSION_NUM=1
|
|
408
432
|
while tmux has-session -t "$SESSION_NAME" 2>/dev/null; do
|
|
@@ -6,9 +6,51 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const { execFileSync } = require('child_process');
|
|
10
11
|
const { c, log, header, readJSON } = require('./configure-utils');
|
|
11
12
|
const { tryOptional } = require('../../lib/errors');
|
|
13
|
+
const { FEATURES } = require('./configure-features');
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// CONTENT HASH HELPERS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hash a file's content using SHA-256 (first 16 hex chars)
|
|
21
|
+
* @param {string} filePath - Path to the file
|
|
22
|
+
* @returns {string|null} 16-char hex hash, or null if file can't be read
|
|
23
|
+
*/
|
|
24
|
+
function hashFile(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
27
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find the directory containing package source scripts.
|
|
35
|
+
* Checks require.resolve first, then common fallback locations.
|
|
36
|
+
* @returns {string|null} Path to the scripts directory, or null
|
|
37
|
+
*/
|
|
38
|
+
function findPackageScriptDir() {
|
|
39
|
+
try {
|
|
40
|
+
const pkgPath = require.resolve('agileflow/package.json');
|
|
41
|
+
return path.join(path.dirname(pkgPath), 'scripts');
|
|
42
|
+
} catch {
|
|
43
|
+
// Fallback: check common locations
|
|
44
|
+
const candidates = [
|
|
45
|
+
path.join(process.cwd(), 'node_modules', 'agileflow', 'scripts'),
|
|
46
|
+
path.join(process.cwd(), 'packages', 'cli', 'scripts'), // monorepo dev
|
|
47
|
+
];
|
|
48
|
+
for (const dir of candidates) {
|
|
49
|
+
if (fs.existsSync(dir)) return dir;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
12
54
|
|
|
13
55
|
// ============================================================================
|
|
14
56
|
// DETECTION
|
|
@@ -256,17 +298,51 @@ function detectMetadata(status, version) {
|
|
|
256
298
|
status.features.tmuxautospawn.enabled = true; // Default enabled
|
|
257
299
|
}
|
|
258
300
|
|
|
259
|
-
// Read feature versions and check if outdated
|
|
301
|
+
// Read feature versions and check if outdated (content-based)
|
|
260
302
|
if (meta.features) {
|
|
261
303
|
const featureKeyMap = { askUserQuestion: 'askuserquestion', tmuxAutoSpawn: 'tmuxautospawn' };
|
|
304
|
+
const packageScriptDir = findPackageScriptDir();
|
|
305
|
+
|
|
262
306
|
Object.entries(meta.features).forEach(([feature, data]) => {
|
|
263
307
|
const statusKey = featureKeyMap[feature] || feature.toLowerCase();
|
|
264
308
|
if (status.features[statusKey] && data.version) {
|
|
265
309
|
status.features[statusKey].version = data.version;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
|
|
311
|
+
if (!status.features[statusKey].enabled) return;
|
|
312
|
+
|
|
313
|
+
// Content-based outdated detection
|
|
314
|
+
const featureConfig = FEATURES[statusKey];
|
|
315
|
+
const scriptsToCheck = featureConfig?.scripts
|
|
316
|
+
|| (featureConfig?.script ? [featureConfig.script] : []);
|
|
317
|
+
|
|
318
|
+
if (scriptsToCheck.length > 0 && packageScriptDir) {
|
|
319
|
+
// Compare installed scripts against package source
|
|
320
|
+
let isOutdated = false;
|
|
321
|
+
for (const scriptName of scriptsToCheck) {
|
|
322
|
+
const packageScript = path.join(packageScriptDir, scriptName);
|
|
323
|
+
const installedScript = path.join(
|
|
324
|
+
process.cwd(), '.agileflow', 'scripts', scriptName
|
|
325
|
+
);
|
|
326
|
+
const packageHash = hashFile(packageScript);
|
|
327
|
+
const installedHash = hashFile(installedScript);
|
|
328
|
+
|
|
329
|
+
if (packageHash && installedHash && packageHash !== installedHash) {
|
|
330
|
+
isOutdated = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (isOutdated) {
|
|
335
|
+
status.features[statusKey].outdated = true;
|
|
336
|
+
status.hasOutdated = true;
|
|
337
|
+
}
|
|
338
|
+
} else if (featureConfig?.metadataOnly) {
|
|
339
|
+
// Metadata-only features: use version comparison (no scripts to hash)
|
|
340
|
+
if (data.version !== version) {
|
|
341
|
+
status.features[statusKey].outdated = true;
|
|
342
|
+
status.hasOutdated = true;
|
|
343
|
+
}
|
|
269
344
|
}
|
|
345
|
+
// If no package source found or no scripts, don't mark outdated (fail open)
|
|
270
346
|
}
|
|
271
347
|
});
|
|
272
348
|
}
|
|
@@ -402,4 +478,6 @@ module.exports = {
|
|
|
402
478
|
detectPreToolUseHooks,
|
|
403
479
|
detectStatusLine,
|
|
404
480
|
detectMetadata,
|
|
481
|
+
hashFile,
|
|
482
|
+
findPackageScriptDir,
|
|
405
483
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const os = require('os');
|
|
10
11
|
const {
|
|
11
12
|
c,
|
|
@@ -53,12 +54,12 @@ const FEATURES = {
|
|
|
53
54
|
description: 'Auto-kill duplicate Claude processes in same directory to prevent freezing',
|
|
54
55
|
},
|
|
55
56
|
claudeflags: {
|
|
56
|
-
metadataOnly:
|
|
57
|
-
description: 'Default flags for Claude CLI (
|
|
57
|
+
metadataOnly: false,
|
|
58
|
+
description: 'Default flags for Claude CLI (sets permissions.defaultMode in .claude/settings.json)',
|
|
58
59
|
},
|
|
59
60
|
agentteams: {
|
|
60
|
-
metadataOnly:
|
|
61
|
-
description: 'Enable Claude Code native Agent Teams (
|
|
61
|
+
metadataOnly: false,
|
|
62
|
+
description: 'Enable Claude Code native Agent Teams (sets env var in .claude/settings.json)',
|
|
62
63
|
},
|
|
63
64
|
};
|
|
64
65
|
|
|
@@ -152,6 +153,20 @@ const SCRIPTS_DIR = path.join(process.cwd(), '.agileflow', 'scripts');
|
|
|
152
153
|
const scriptExists = scriptName => fs.existsSync(path.join(SCRIPTS_DIR, scriptName));
|
|
153
154
|
const getScriptPath = scriptName => `.agileflow/scripts/${scriptName}`;
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Hash a file's content using SHA-256 (first 16 hex chars)
|
|
158
|
+
* @param {string} filePath - Path to the file
|
|
159
|
+
* @returns {string|null} 16-char hex hash, or null if file can't be read
|
|
160
|
+
*/
|
|
161
|
+
function hashFile(filePath) {
|
|
162
|
+
try {
|
|
163
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
// ============================================================================
|
|
156
171
|
// METADATA MANAGEMENT
|
|
157
172
|
// ============================================================================
|
|
@@ -308,8 +323,24 @@ function enableFeature(feature, options = {}, version) {
|
|
|
308
323
|
}
|
|
309
324
|
|
|
310
325
|
// Handle claude flags (e.g., --dangerously-skip-permissions)
|
|
326
|
+
// Also sets permissions.defaultMode in .claude/settings.json
|
|
311
327
|
if (feature === 'claudeflags') {
|
|
312
328
|
const defaultFlags = options.flags || '--dangerously-skip-permissions';
|
|
329
|
+
|
|
330
|
+
// Map CLI flags to settings.json defaultMode values
|
|
331
|
+
const flagToMode = {
|
|
332
|
+
'--dangerously-skip-permissions': 'bypassPermissions',
|
|
333
|
+
'--permission-mode acceptEdits': 'acceptEdits',
|
|
334
|
+
};
|
|
335
|
+
const defaultMode = flagToMode[defaultFlags];
|
|
336
|
+
|
|
337
|
+
if (defaultMode) {
|
|
338
|
+
settings.permissions = settings.permissions || {};
|
|
339
|
+
settings.permissions.defaultMode = defaultMode;
|
|
340
|
+
writeJSON('.claude/settings.json', settings);
|
|
341
|
+
info(`Set permissions.defaultMode = "${defaultMode}" in .claude/settings.json`);
|
|
342
|
+
}
|
|
343
|
+
|
|
313
344
|
updateMetadata(
|
|
314
345
|
{
|
|
315
346
|
features: {
|
|
@@ -325,11 +356,17 @@ function enableFeature(feature, options = {}, version) {
|
|
|
325
356
|
);
|
|
326
357
|
success(`Default Claude flags configured: ${defaultFlags}`);
|
|
327
358
|
info('These flags will be passed to Claude when launched via "af" or "agileflow"');
|
|
359
|
+
if (defaultMode) {
|
|
360
|
+
info('Restart Claude Code for the new default mode to take effect');
|
|
361
|
+
}
|
|
328
362
|
return true;
|
|
329
363
|
}
|
|
330
364
|
|
|
331
|
-
// Handle agent teams
|
|
365
|
+
// Handle agent teams - set env var in .claude/settings.json
|
|
332
366
|
if (feature === 'agentteams') {
|
|
367
|
+
settings.env = settings.env || {};
|
|
368
|
+
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
369
|
+
writeJSON('.claude/settings.json', settings);
|
|
333
370
|
updateMetadata(
|
|
334
371
|
{
|
|
335
372
|
features: {
|
|
@@ -343,8 +380,8 @@ function enableFeature(feature, options = {}, version) {
|
|
|
343
380
|
version
|
|
344
381
|
);
|
|
345
382
|
success('Native Agent Teams enabled');
|
|
346
|
-
info('
|
|
347
|
-
info('
|
|
383
|
+
info('Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json');
|
|
384
|
+
info('Claude Code will use native TeamCreate/SendMessage tools');
|
|
348
385
|
info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
|
|
349
386
|
return true;
|
|
350
387
|
}
|
|
@@ -416,9 +453,18 @@ function enableFeature(feature, options = {}, version) {
|
|
|
416
453
|
return enableDamageControl(settings, options, version);
|
|
417
454
|
}
|
|
418
455
|
|
|
456
|
+
const featureConfig = FEATURES[feature];
|
|
457
|
+
const contentHash = featureConfig?.script
|
|
458
|
+
? hashFile(path.join(SCRIPTS_DIR, featureConfig.script))
|
|
459
|
+
: null;
|
|
419
460
|
writeJSON('.claude/settings.json', settings);
|
|
420
461
|
updateMetadata(
|
|
421
|
-
{ features: { [feature]: {
|
|
462
|
+
{ features: { [feature]: {
|
|
463
|
+
enabled: true,
|
|
464
|
+
version,
|
|
465
|
+
...(contentHash ? { contentHash } : {}),
|
|
466
|
+
at: new Date().toISOString(),
|
|
467
|
+
} } },
|
|
422
468
|
version
|
|
423
469
|
);
|
|
424
470
|
updateGitignore();
|
|
@@ -583,6 +629,7 @@ function enableDamageControl(settings, options, version) {
|
|
|
583
629
|
|
|
584
630
|
success('Damage control PreToolUse hooks enabled');
|
|
585
631
|
|
|
632
|
+
const primaryHash = hashFile(path.join(SCRIPTS_DIR, 'damage-control-bash.js'));
|
|
586
633
|
updateMetadata(
|
|
587
634
|
{
|
|
588
635
|
features: {
|
|
@@ -590,6 +637,7 @@ function enableDamageControl(settings, options, version) {
|
|
|
590
637
|
enabled: true,
|
|
591
638
|
protectionLevel: level,
|
|
592
639
|
version,
|
|
640
|
+
...(primaryHash ? { contentHash: primaryHash } : {}),
|
|
593
641
|
at: new Date().toISOString(),
|
|
594
642
|
},
|
|
595
643
|
},
|
|
@@ -735,8 +783,13 @@ function disableFeature(feature, version) {
|
|
|
735
783
|
return true;
|
|
736
784
|
}
|
|
737
785
|
|
|
738
|
-
// Disable claude flags
|
|
786
|
+
// Disable claude flags - also reset permissions.defaultMode in settings.json
|
|
739
787
|
if (feature === 'claudeflags') {
|
|
788
|
+
if (settings.permissions?.defaultMode) {
|
|
789
|
+
delete settings.permissions.defaultMode;
|
|
790
|
+
writeJSON('.claude/settings.json', settings);
|
|
791
|
+
info('Removed permissions.defaultMode from .claude/settings.json');
|
|
792
|
+
}
|
|
740
793
|
updateMetadata(
|
|
741
794
|
{
|
|
742
795
|
features: {
|
|
@@ -752,11 +805,19 @@ function disableFeature(feature, version) {
|
|
|
752
805
|
);
|
|
753
806
|
success('Default Claude flags disabled');
|
|
754
807
|
info('Claude will launch with default permissions (prompts for each action)');
|
|
808
|
+
info('Restart Claude Code for the change to take effect');
|
|
755
809
|
return true;
|
|
756
810
|
}
|
|
757
811
|
|
|
758
|
-
// Disable agent teams
|
|
812
|
+
// Disable agent teams - remove env var from .claude/settings.json
|
|
759
813
|
if (feature === 'agentteams') {
|
|
814
|
+
if (settings.env) {
|
|
815
|
+
delete settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
816
|
+
if (Object.keys(settings.env).length === 0) {
|
|
817
|
+
delete settings.env;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
writeJSON('.claude/settings.json', settings);
|
|
760
821
|
updateMetadata(
|
|
761
822
|
{
|
|
762
823
|
features: {
|
|
@@ -770,6 +831,7 @@ function disableFeature(feature, version) {
|
|
|
770
831
|
version
|
|
771
832
|
);
|
|
772
833
|
success('Native Agent Teams disabled');
|
|
834
|
+
info('Removed CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from .claude/settings.json');
|
|
773
835
|
info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
|
|
774
836
|
return true;
|
|
775
837
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
# Track start time for hook metrics
|
|
12
12
|
HOOK_START_TIME=$(date +%s%3N 2>/dev/null || date +%s)
|
|
13
|
+
# macOS date doesn't support %N - outputs literal "3N" instead of millis
|
|
14
|
+
[[ ! "$HOOK_START_TIME" =~ ^[0-9]+$ ]] && HOOK_START_TIME="$(date +%s)000"
|
|
13
15
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
16
|
|
|
15
17
|
# Get current version from package.json
|
|
@@ -286,6 +288,7 @@ fi
|
|
|
286
288
|
# Record hook metrics
|
|
287
289
|
if command -v node &> /dev/null && [[ -f "$SCRIPT_DIR/lib/hook-metrics.js" ]]; then
|
|
288
290
|
HOOK_END_TIME=$(date +%s%3N 2>/dev/null || date +%s)
|
|
291
|
+
[[ ! "$HOOK_END_TIME" =~ ^[0-9]+$ ]] && HOOK_END_TIME="$(date +%s)000"
|
|
289
292
|
HOOK_DURATION=$((HOOK_END_TIME - HOOK_START_TIME))
|
|
290
293
|
HOOK_DURATION="$HOOK_DURATION" node -e '
|
|
291
294
|
try {
|
|
@@ -411,6 +411,8 @@ Present the Sessions sub-menu:
|
|
|
411
411
|
|
|
412
412
|
Configure default startup mode for new sessions created with `/agileflow:session:new`.
|
|
413
413
|
|
|
414
|
+
**How it works**: When skip-permissions is selected, `/configure` sets `permissions.defaultMode = "bypassPermissions"` in `.claude/settings.json`. Claude Code reads this on startup and skips all permission prompts automatically. No CLI flags needed.
|
|
415
|
+
|
|
414
416
|
First, read current setting:
|
|
415
417
|
```bash
|
|
416
418
|
cat docs/00-meta/agileflow-metadata.json | grep '"defaultStartupMode"' 2>/dev/null || echo "normal"
|
|
@@ -441,38 +443,45 @@ Map selection to value:
|
|
|
441
443
|
| Accept edits only | `accept-edits` |
|
|
442
444
|
| Don't start Claude | `no-claude` |
|
|
443
445
|
|
|
444
|
-
Update the metadata file:
|
|
445
|
-
```bash
|
|
446
|
-
# Read current metadata
|
|
447
|
-
metadata=$(cat docs/00-meta/agileflow-metadata.json)
|
|
446
|
+
Update the metadata file AND configure the CLI flags:
|
|
448
447
|
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
1. Update `docs/00-meta/agileflow-metadata.json`:
|
|
449
|
+
- Find: `"defaultStartupMode": "..."`
|
|
450
|
+
- Replace with: `"defaultStartupMode": "{selected_value}"`
|
|
451
|
+
|
|
452
|
+
2. **Also run** the configure script to set Claude Code's permission mode:
|
|
453
|
+
```bash
|
|
454
|
+
node .agileflow/scripts/agileflow-configure.js --enable=claudeflags --flags="{cli_flag}"
|
|
451
455
|
```
|
|
452
456
|
|
|
453
|
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
457
|
+
Where `{cli_flag}` is:
|
|
458
|
+
- `skip-permissions` → `--dangerously-skip-permissions` (sets `permissions.defaultMode = "bypassPermissions"`)
|
|
459
|
+
- `accept-edits` → `--permission-mode acceptEdits` (sets `permissions.defaultMode = "acceptEdits"`)
|
|
460
|
+
- `normal` → (disable claudeflags instead: `--disable=claudeflags`, removes `defaultMode`)
|
|
456
461
|
|
|
457
462
|
Display confirmation:
|
|
458
463
|
```
|
|
459
464
|
✅ Default session startup mode set to: {selected_value}
|
|
460
465
|
|
|
461
|
-
|
|
462
|
-
|
|
466
|
+
What was configured:
|
|
467
|
+
• .claude/settings.json: permissions.defaultMode = "{defaultMode}"
|
|
468
|
+
• Metadata: defaultStartupMode = "{selected_value}"
|
|
469
|
+
• AgileFlow commands (af, /session:new) will also use this mode
|
|
470
|
+
|
|
471
|
+
⚠️ Restart Claude Code for the new permission mode to take effect.
|
|
463
472
|
```
|
|
464
473
|
|
|
465
|
-
**If user selected "Skip permissions" or "Accept edits only"**, offer
|
|
474
|
+
**If user selected "Skip permissions" or "Accept edits only"**, offer shell alias:
|
|
466
475
|
|
|
467
476
|
```xml
|
|
468
477
|
<invoke name="AskUserQuestion">
|
|
469
478
|
<parameter name="questions">[{
|
|
470
|
-
"question": "Also
|
|
471
|
-
"header": "
|
|
479
|
+
"question": "Also add a shell alias so 'claude' always uses this mode?",
|
|
480
|
+
"header": "Shell alias",
|
|
472
481
|
"multiSelect": false,
|
|
473
482
|
"options": [
|
|
474
|
-
{"label": "
|
|
475
|
-
{"label": "Also add shell alias", "description": "Add alias to
|
|
483
|
+
{"label": "AgileFlow commands only (Recommended)", "description": "Only 'af' and /session:new use this mode. Direct 'claude' command unchanged."},
|
|
484
|
+
{"label": "Also add shell alias", "description": "Add 'alias claude=\"claude {flag}\"' to shell profile"},
|
|
476
485
|
{"label": "No thanks", "description": "Keep current setup"}
|
|
477
486
|
]
|
|
478
487
|
}]</parameter>
|
|
@@ -511,10 +520,10 @@ Run `source {SHELL_RC}` or open a new terminal for the alias to take effect.
|
|
|
511
520
|
To remove later: edit {SHELL_RC} and remove the 'alias claude=' line.
|
|
512
521
|
```
|
|
513
522
|
|
|
514
|
-
**If "
|
|
523
|
+
**If "AgileFlow commands only" or "No thanks" selected:**
|
|
515
524
|
```
|
|
516
|
-
|
|
517
|
-
|
|
525
|
+
AgileFlow commands (af, /session:new) will use the configured mode automatically.
|
|
526
|
+
Direct 'claude' command remains unchanged.
|
|
518
527
|
```
|
|
519
528
|
|
|
520
529
|
#### If "Agent Teams" selected
|
|
@@ -547,16 +556,19 @@ Then present options:
|
|
|
547
556
|
node .agileflow/scripts/agileflow-configure.js --enable=agentteams
|
|
548
557
|
```
|
|
549
558
|
|
|
559
|
+
This sets `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in `.claude/settings.json` under the `env` key.
|
|
560
|
+
Claude Code reads this automatically on startup - no manual env var setup needed.
|
|
561
|
+
|
|
550
562
|
Display:
|
|
551
563
|
```
|
|
552
564
|
✅ Native Agent Teams enabled
|
|
553
565
|
|
|
554
|
-
|
|
555
|
-
•
|
|
556
|
-
•
|
|
557
|
-
•
|
|
566
|
+
What was configured:
|
|
567
|
+
• Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json
|
|
568
|
+
• Claude Code will use native TeamCreate/SendMessage tools
|
|
569
|
+
• Fallback: subagent mode (Task/TaskOutput) when native is unavailable
|
|
558
570
|
|
|
559
|
-
|
|
571
|
+
⚠️ Restart Claude Code for the env var to take effect.
|
|
560
572
|
```
|
|
561
573
|
|
|
562
574
|
**If "Disable" selected:**
|
|
@@ -564,12 +576,17 @@ When the flag is not set, AgileFlow falls back to subagent mode automatically.
|
|
|
564
576
|
node .agileflow/scripts/agileflow-configure.js --disable=agentteams
|
|
565
577
|
```
|
|
566
578
|
|
|
579
|
+
This removes `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` from `.claude/settings.json`.
|
|
580
|
+
|
|
567
581
|
Display:
|
|
568
582
|
```
|
|
569
583
|
✅ Native Agent Teams disabled
|
|
570
584
|
|
|
571
|
-
|
|
572
|
-
|
|
585
|
+
What was configured:
|
|
586
|
+
• Removed CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from .claude/settings.json
|
|
587
|
+
• AgileFlow will use subagent mode (Task/TaskOutput) for all multi-agent orchestration
|
|
588
|
+
|
|
589
|
+
⚠️ Restart Claude Code for the change to take effect.
|
|
573
590
|
```
|
|
574
591
|
|
|
575
592
|
**If "What is this?" selected:**
|
|
@@ -592,7 +609,7 @@ How AgileFlow integrates:
|
|
|
592
609
|
4. Subagent mode remains available as automatic fallback
|
|
593
610
|
|
|
594
611
|
Requirements:
|
|
595
|
-
•
|
|
612
|
+
• /configure sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json automatically
|
|
596
613
|
• Feature is opt-in per project via this configuration
|
|
597
614
|
• CI/CD always uses subagent mode (safety gate)
|
|
598
615
|
```
|