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 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
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
- [![Commands](https://img.shields.io/badge/commands-91-blue)](docs/04-architecture/commands.md)
6
+ [![Commands](https://img.shields.io/badge/commands-93-blue)](docs/04-architecture/commands.md)
7
7
  [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-47-orange)](docs/04-architecture/subagents.md)
8
8
  [![Skills](https://img.shields.io/badge/skills-dynamic-purple)](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) | 91 | Slash commands for agile workflows |
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 91 slash commands
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
 
@@ -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() { if (!_http) _http = require('http'); return _http; }
30
- function getCrypto() { if (!_crypto) _crypto = require('crypto'); return _crypto; }
31
- function getProtocol() { if (!_protocol) _protocol = require('./dashboard-protocol'); return _protocol; }
32
- function getPaths() { if (!_paths) _paths = require('./paths'); return _paths; }
33
- function getValidatePaths() { if (!_validatePaths) _validatePaths = require('./validate-paths'); return _validatePaths; }
34
- function getChildProcess() { if (!_childProcess) _childProcess = require('child_process'); return _childProcess; }
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(getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`));
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(getProtocol().createAutomationStatus(automationId, 'error', { error: result.error }));
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 = crypto
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(getProtocol().createError('RATE_LIMITED', 'Too many messages, please slow down'));
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], { cwd: this.projectRoot });
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', '.'], { cwd: this.projectRoot });
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], { cwd: this.projectRoot });
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], { cwd: this.projectRoot });
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(getProtocol().createNotification('success', 'Git', `${type.replace('git_', '')} completed`));
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().execFileSync('git', ['branch', '--show-current'], {
1119
- cwd: this.projectRoot,
1120
- encoding: 'utf8',
1121
- stdio: ['pipe', 'pipe', 'pipe'],
1122
- }).trim();
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, { allowSymlinks: true });
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().execFileSync('git', ['status', '--porcelain', '--', filePath], {
1228
- cwd: this.projectRoot,
1229
- encoding: 'utf8',
1230
- }).trim();
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().execFileSync('git', ['branch', '--show-current'], {
1402
- cwd,
1403
- encoding: 'utf8',
1404
- stdio: ['pipe', 'pipe', 'pipe'],
1405
- }).trim();
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().execFileSync(
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
- ).trim();
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, { allowSymlinks: true });
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().spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
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('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
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(getProtocol().createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
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, { allowSymlinks: true });
1561
+ const cwdResult = getValidatePaths().validatePath(cwd, this.projectRoot, {
1562
+ allowSymlinks: true,
1563
+ });
1512
1564
  if (!cwdResult.ok) {
1513
1565
  session.send(
1514
- getProtocol().createError('TERMINAL_ERROR', 'Working directory must be within project root')
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(getProtocol().createError('AUTOMATION_ERROR', 'Automation runner not initialized'));
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('warning', 'Automation', `${automationId} is already running`)
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(getProtocol().createNotification('info', 'Automation', `Starting ${automationId}...`));
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('success', 'Automation', `${automationId} completed successfully`)
1771
+ getProtocol().createNotification(
1772
+ 'success',
1773
+ 'Automation',
1774
+ `${automationId} completed successfully`
1775
+ )
1709
1776
  );
1710
1777
  } else {
1711
1778
  session.send(
1712
- getProtocol().createNotification('error', 'Automation', `${automationId} failed: ${result.error}`)
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(getProtocol().createAutomationStatus(automationId, result.success ? 'idle' : 'error', result));
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(getProtocol().createAutomationStatus(automationId, 'error', { error: 'Execution failed' }));
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(getProtocol().createNotification('success', 'Inbox', `Accepted: ${item.title}`));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -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();
@@ -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
@@ -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 keybind cheat sheet in a popup
256
- tmux bind-key -n M-h display-popup -E -w 52 -h 24 "\
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 New Claude window\\n';\
260
- printf ' Alt+l Switch session\\n';\
261
- printf ' Alt+q Detach (af to resume)\\n';\
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+1-9 Switch to window\\n';\
265
- printf ' Alt+c New empty window\\n';\
266
- printf ' Alt+n/p Next / previous\\n';\
267
- printf ' Alt+r Rename window\\n';\
268
- printf ' Alt+w Close window\\n';\
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 Split side by side\\n';\
272
- printf ' Alt+v Split top / bottom\\n';\
273
- printf ' Alt+←→↑↓ Navigate\\n';\
274
- printf ' Alt+z Zoom / unzoom\\n';\
275
- printf ' Alt+x Close pane\\n';\
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+[ Scroll mode\\n';\
279
- printf ' Alt+k Unfreeze (Ctrl+C×2)\\n';\
280
- printf ' Alt+h This help\\n';\
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
- # No detached session found create a new one
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
- if (data.version !== version && status.features[statusKey].enabled) {
267
- status.features[statusKey].outdated = true;
268
- status.hasOutdated = true;
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: true,
57
- description: 'Default flags for Claude CLI (e.g., --dangerously-skip-permissions)',
57
+ metadataOnly: false,
58
+ description: 'Default flags for Claude CLI (sets permissions.defaultMode in .claude/settings.json)',
58
59
  },
59
60
  agentteams: {
60
- metadataOnly: true,
61
- description: 'Enable Claude Code native Agent Teams (experimental multi-agent orchestration)',
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 (metadata only)
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('Claude Code will use native TeamCreate/SendMessage tools when available');
347
- info('Requires: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (set by Claude Code)');
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]: { enabled: true, version, at: new Date().toISOString() } } },
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
- # Update defaultStartupMode (using jq if available, or manual edit)
450
- # The value should be one of: normal, skip-permissions, accept-edits, no-claude
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
- Or use the Edit tool to update `docs/00-meta/agileflow-metadata.json`:
454
- - Find: `"defaultStartupMode": "..."`
455
- - Replace with: `"defaultStartupMode": "{selected_value}"`
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
- When creating new sessions with /agileflow:session:new, this will be the
462
- recommended option. You can still choose a different mode per-session.
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 Claude settings integration:
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 configure Claude to default to this mode?",
471
- "header": "Claude settings",
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": "af wrapper only (Recommended)", "description": "The 'af' command already uses this setting. No other changes needed."},
475
- {"label": "Also add shell alias", "description": "Add alias to your shell profile so 'claude' also uses this mode"},
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 "af wrapper only" or "No thanks" selected:**
523
+ **If "AgileFlow commands only" or "No thanks" selected:**
515
524
  ```
516
- The 'af' command already reads the configured startup mode automatically.
517
- No additional changes needed.
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
- When Claude Code sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, AgileFlow will:
555
- Use native TeamCreate for parallel agent spawning
556
- Send messages via both native SendMessage AND JSONL bus
557
- Track team lifecycle events in session-state.json
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
- When the flag is not set, AgileFlow falls back to subagent mode automatically.
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
- AgileFlow will use subagent mode (Task/TaskOutput) for all multi-agent
572
- orchestration. This is the stable, well-tested mode.
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
- Claude Code must set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
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
  ```