agileflow 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +8 -8
- package/lib/feedback.js +9 -2
- package/lib/lazy-require.js +59 -0
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +12 -0
- package/scripts/agileflow-welcome.js +89 -73
- package/scripts/claude-tmux.sh +33 -3
- package/scripts/lib/configure-features.js +116 -0
- package/scripts/team-manager.js +85 -0
- package/scripts/tmux-task-name.sh +48 -18
- package/scripts/tmux-task-watcher.sh +173 -6
- package/src/core/agents/legal-analyzer-a11y.md +110 -0
- package/src/core/agents/legal-analyzer-ai.md +117 -0
- package/src/core/agents/legal-analyzer-consumer.md +108 -0
- package/src/core/agents/legal-analyzer-content.md +113 -0
- package/src/core/agents/legal-analyzer-international.md +115 -0
- package/src/core/agents/legal-analyzer-licensing.md +115 -0
- package/src/core/agents/legal-analyzer-privacy.md +108 -0
- package/src/core/agents/legal-analyzer-security.md +112 -0
- package/src/core/agents/legal-analyzer-terms.md +111 -0
- package/src/core/agents/legal-consensus.md +242 -0
- package/src/core/agents/team-lead.md +50 -13
- package/src/core/commands/configure.md +10 -25
- package/src/core/commands/legal/audit.md +446 -0
- package/src/core/commands/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- package/tools/cli/installers/ide/claude-code.js +3 -1
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.1.0] - 2026-02-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Legal audit system, native Agent Teams integration, and startup performance improvements
|
|
14
|
+
|
|
10
15
|
## [3.0.2] - 2026-02-14
|
|
11
16
|
|
|
12
17
|
### Added
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](docs/04-architecture/commands.md)
|
|
7
|
+
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](docs/04-architecture/skills.md)
|
|
9
9
|
|
|
10
10
|
**AI-driven agile development for Claude Code, Cursor, Windsurf, OpenAI Codex CLI, and more.** Combining Scrum, Kanban, ADRs, and docs-as-code principles into one framework-agnostic system.
|
|
@@ -39,9 +39,9 @@ npx agileflow@latest update
|
|
|
39
39
|
| IDE | Status | Config Location |
|
|
40
40
|
|-----|--------|-----------------|
|
|
41
41
|
| Claude Code | Supported | `.claude/commands/agileflow/` |
|
|
42
|
-
| Cursor | Supported | `.cursor/
|
|
42
|
+
| Cursor | Supported | `.cursor/commands/agileflow/` |
|
|
43
43
|
| Windsurf | Supported | `.windsurf/workflows/agileflow/` |
|
|
44
|
-
| OpenAI Codex CLI | Supported | `.codex/skills/` |
|
|
44
|
+
| OpenAI Codex CLI | Supported | `.codex/skills/` and `~/.codex/prompts/` |
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -65,8 +65,8 @@ AgileFlow combines three proven methodologies:
|
|
|
65
65
|
|
|
66
66
|
| Component | Count | Description |
|
|
67
67
|
|-----------|-------|-------------|
|
|
68
|
-
| [Commands](docs/04-architecture/commands.md) |
|
|
69
|
-
| [Agents/Experts](docs/04-architecture/subagents.md) |
|
|
68
|
+
| [Commands](docs/04-architecture/commands.md) | 94 | Slash commands for agile workflows |
|
|
69
|
+
| [Agents/Experts](docs/04-architecture/subagents.md) | 55 | 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
|
|
|
72
72
|
---
|
|
@@ -76,8 +76,8 @@ 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
|
|
80
|
-
- [Agents/Experts](docs/04-architecture/subagents.md) -
|
|
79
|
+
- [Commands](docs/04-architecture/commands.md) - All 94 slash commands
|
|
80
|
+
- [Agents/Experts](docs/04-architecture/subagents.md) - 55 specialized agents with self-improving knowledge
|
|
81
81
|
- [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
|
|
82
82
|
|
|
83
83
|
### Architecture
|
package/lib/feedback.js
CHANGED
|
@@ -34,7 +34,14 @@
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
const { c, BRAND_HEX } = require('./colors');
|
|
37
|
-
const
|
|
37
|
+
const { lazyRequire } = require('./lazy-require');
|
|
38
|
+
|
|
39
|
+
// Lazy-load chalk: feedback.js is imported by scripts that run as hooks in
|
|
40
|
+
// user projects (.agileflow/scripts/). If chalk isn't resolvable from the
|
|
41
|
+
// user's node_modules the eager require() would crash every hook that
|
|
42
|
+
// imports feedback. Deferring the require() to the single call-site that
|
|
43
|
+
// actually needs chalk (brand()) avoids the crash entirely.
|
|
44
|
+
const getChalk = lazyRequire('chalk');
|
|
38
45
|
|
|
39
46
|
// Symbols for consistent output
|
|
40
47
|
const SYMBOLS = {
|
|
@@ -182,7 +189,7 @@ class Feedback {
|
|
|
182
189
|
brand(message) {
|
|
183
190
|
if (this.quiet) return this;
|
|
184
191
|
const prefix = this._indent();
|
|
185
|
-
console.log(`${prefix}${
|
|
192
|
+
console.log(`${prefix}${getChalk().hex(BRAND_HEX)(message)}`);
|
|
186
193
|
return this;
|
|
187
194
|
}
|
|
188
195
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lazy-require.js - Reusable Lazy-Loading Utility
|
|
3
|
+
*
|
|
4
|
+
* AgileFlow scripts are copied to user projects (.agileflow/scripts/) and run
|
|
5
|
+
* as hooks. When these scripts eagerly require() npm dependencies at module
|
|
6
|
+
* load time, they crash if the dependency isn't resolvable from the user's
|
|
7
|
+
* project directory. This utility standardizes the lazy-loading pattern used
|
|
8
|
+
* ad-hoc in yaml-utils.js and dashboard-server.js.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { lazyRequire } = require('./lazy-require');
|
|
12
|
+
*
|
|
13
|
+
* // Returns a getter function; require() is deferred until first call
|
|
14
|
+
* const getChalk = lazyRequire('chalk');
|
|
15
|
+
*
|
|
16
|
+
* // With fallback resolution paths
|
|
17
|
+
* const getYaml = lazyRequire('js-yaml',
|
|
18
|
+
* path.join(__dirname, '..', 'node_modules', 'js-yaml')
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* // Later, when actually needed:
|
|
22
|
+
* const chalk = getChalk();
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a lazy-loading getter for an npm module.
|
|
29
|
+
*
|
|
30
|
+
* The returned function defers require() until first invocation, then caches
|
|
31
|
+
* the result. Multiple resolution paths are tried in order, so the module can
|
|
32
|
+
* be found from the user's node_modules, AgileFlow's own node_modules, or any
|
|
33
|
+
* other location.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} name - Primary module name (passed to require())
|
|
36
|
+
* @param {...string} fallbackPaths - Additional paths to try if primary fails
|
|
37
|
+
* @returns {function(): any} Getter that returns the loaded module
|
|
38
|
+
*/
|
|
39
|
+
function lazyRequire(name, ...fallbackPaths) {
|
|
40
|
+
let cached = null;
|
|
41
|
+
return () => {
|
|
42
|
+
if (cached) return cached;
|
|
43
|
+
const paths = [name, ...fallbackPaths];
|
|
44
|
+
for (const p of paths) {
|
|
45
|
+
try {
|
|
46
|
+
cached = require(p);
|
|
47
|
+
return cached;
|
|
48
|
+
} catch (_e) {
|
|
49
|
+
// Continue to next path
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${name} not found. Run: npm install ${name}\n` +
|
|
54
|
+
'Or reinstall AgileFlow: npx agileflow setup --force'
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { lazyRequire };
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* --enable=<features> Enable specific features
|
|
19
19
|
* --disable=<features> Disable specific features
|
|
20
20
|
* --archival-days=<N> Set archival threshold
|
|
21
|
+
* --startup-mode=<MODE> Set default startup mode (atomic)
|
|
21
22
|
* --migrate Fix old formats without changing features
|
|
22
23
|
* --validate Check for issues
|
|
23
24
|
* --detect Show current status
|
|
@@ -42,6 +43,7 @@ const {
|
|
|
42
43
|
listStatuslineComponents,
|
|
43
44
|
migrateSettings,
|
|
44
45
|
upgradeFeatures,
|
|
46
|
+
enableStartupMode,
|
|
45
47
|
} = require('./lib/configure-features');
|
|
46
48
|
const { listScripts, showVersionInfo, repairScripts } = require('./lib/configure-repair');
|
|
47
49
|
const { feedback } = require('../lib/feedback');
|
|
@@ -144,6 +146,7 @@ ${c.cyan}Statusline Components:${c.reset}
|
|
|
144
146
|
|
|
145
147
|
${c.cyan}Settings:${c.reset}
|
|
146
148
|
--archival-days=N Set archival threshold (default: 30)
|
|
149
|
+
--startup-mode=MODE Set default startup mode (skip-permissions, accept-edits, normal, no-claude)
|
|
147
150
|
|
|
148
151
|
${c.cyan}Maintenance:${c.reset}
|
|
149
152
|
--migrate Fix old/invalid formats
|
|
@@ -201,6 +204,7 @@ function main() {
|
|
|
201
204
|
let repairFeature = null;
|
|
202
205
|
let showVersion = false;
|
|
203
206
|
let listScriptsMode = false;
|
|
207
|
+
let startupMode = null;
|
|
204
208
|
|
|
205
209
|
args.forEach(arg => {
|
|
206
210
|
if (arg.startsWith('--profile=')) profile = arg.split('=')[1];
|
|
@@ -237,6 +241,8 @@ function main() {
|
|
|
237
241
|
repairFeature = arg.split('=')[1].trim().toLowerCase();
|
|
238
242
|
} else if (arg === '--version' || arg === '-v') showVersion = true;
|
|
239
243
|
else if (arg === '--list-scripts' || arg === '--scripts') listScriptsMode = true;
|
|
244
|
+
else if (arg.startsWith('--startup-mode='))
|
|
245
|
+
startupMode = arg.split('=')[1].trim().toLowerCase();
|
|
240
246
|
});
|
|
241
247
|
|
|
242
248
|
// Help mode
|
|
@@ -289,6 +295,12 @@ function main() {
|
|
|
289
295
|
spinner.succeed('Configuration detected');
|
|
290
296
|
const { hasIssues, hasOutdated } = printStatus(status);
|
|
291
297
|
|
|
298
|
+
// Startup mode (atomic - sets BOTH metadata AND settings.json)
|
|
299
|
+
if (startupMode) {
|
|
300
|
+
enableStartupMode(startupMode, VERSION);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
// Detect only mode
|
|
293
305
|
if (detect && !migrate && !upgrade && !profile && enable.length === 0 && disable.length === 0) {
|
|
294
306
|
return;
|
|
@@ -30,6 +30,20 @@ const { readJSONCached, readFileCached } = require('../lib/file-cache');
|
|
|
30
30
|
// Session manager path (relative to script location)
|
|
31
31
|
const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
|
|
32
32
|
|
|
33
|
+
// PERFORMANCE OPTIMIZATION: Lazy-loaded session-manager module
|
|
34
|
+
// Importing directly avoids ~50-150ms subprocess overhead per call.
|
|
35
|
+
let _sessionManager;
|
|
36
|
+
function getSessionManager() {
|
|
37
|
+
if (_sessionManager === undefined) {
|
|
38
|
+
try {
|
|
39
|
+
_sessionManager = require('./session-manager.js');
|
|
40
|
+
} catch (e) {
|
|
41
|
+
_sessionManager = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return _sessionManager;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// Hook metrics module (kept at top level - needed early for timer)
|
|
34
48
|
let hookMetrics;
|
|
35
49
|
try {
|
|
@@ -43,6 +57,12 @@ try {
|
|
|
43
57
|
* Uses file-cache module for automatic caching with 15s TTL.
|
|
44
58
|
* Files are cached across script invocations within TTL window.
|
|
45
59
|
* Estimated savings: 60-120ms on cache hits
|
|
60
|
+
*
|
|
61
|
+
* Additional optimizations in this file (US-0356):
|
|
62
|
+
* - Git batching: 3 subprocess calls → 1 (~20-40ms savings)
|
|
63
|
+
* - Session-manager inline: subprocess → direct require() (~50-150ms savings)
|
|
64
|
+
* - Tmux cache: subprocess → session-state lookup (~10-20ms after first run)
|
|
65
|
+
* Total estimated savings: ~130-260ms
|
|
46
66
|
*/
|
|
47
67
|
function loadProjectFiles(rootDir) {
|
|
48
68
|
const paths = {
|
|
@@ -109,32 +129,57 @@ function detectPlatform() {
|
|
|
109
129
|
|
|
110
130
|
/**
|
|
111
131
|
* Check if tmux is installed
|
|
132
|
+
* PERFORMANCE OPTIMIZATION: Caches result in session-state.json (~10-20ms savings on subsequent runs)
|
|
112
133
|
* Returns object with availability info and platform-specific install suggestion
|
|
113
134
|
*/
|
|
114
|
-
function checkTmuxAvailability() {
|
|
135
|
+
function checkTmuxAvailability(cache) {
|
|
136
|
+
// Check session state cache first (tmux availability doesn't change within a session)
|
|
137
|
+
if (cache?.sessionState?.tmux_available !== undefined) {
|
|
138
|
+
if (cache.sessionState.tmux_available) return { available: true };
|
|
139
|
+
return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Actually check (first run or no cache)
|
|
115
143
|
const result = executeCommandSync('which', ['tmux'], { fallback: null });
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
const available = result.data !== null;
|
|
145
|
+
|
|
146
|
+
// Cache in session state for next invocation
|
|
147
|
+
try {
|
|
148
|
+
const rootDir = getProjectRoot();
|
|
149
|
+
const sessionStatePath = getSessionStatePath(rootDir);
|
|
150
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
151
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
152
|
+
state.tmux_available = available;
|
|
153
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Cache write failed, non-critical
|
|
118
157
|
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
platform,
|
|
123
|
-
noSudoCmd: 'conda install -c conda-forge tmux',
|
|
124
|
-
};
|
|
158
|
+
|
|
159
|
+
if (available) return { available: true };
|
|
160
|
+
return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
|
|
125
161
|
}
|
|
126
162
|
|
|
127
163
|
/**
|
|
128
164
|
* PERFORMANCE OPTIMIZATION: Batch git commands into single call
|
|
129
|
-
*
|
|
130
|
-
*
|
|
165
|
+
* Uses `git log -1 --format=%D%n%h%n%s` to get branch, short hash, and subject
|
|
166
|
+
* in a single subprocess instead of 3 separate calls.
|
|
167
|
+
* Savings: ~20-40ms (eliminates 2 subprocess spawns)
|
|
131
168
|
*/
|
|
132
169
|
function getGitInfo(rootDir) {
|
|
133
|
-
const opts = { cwd: rootDir, timeout: 5000, fallback: '
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
const opts = { cwd: rootDir, timeout: 5000, fallback: '' };
|
|
171
|
+
const result = git(['log', '-1', '--format=%D%n%h%n%s'], opts);
|
|
172
|
+
if (result.data) {
|
|
173
|
+
const lines = result.data.split('\n');
|
|
174
|
+
// %D gives decorations like "HEAD -> main, origin/main, tag: v3.0.0"
|
|
175
|
+
const branchMatch = (lines[0] || '').match(/HEAD -> ([^,\s]+)/);
|
|
176
|
+
return {
|
|
177
|
+
branch: branchMatch ? branchMatch[1] : 'detached',
|
|
178
|
+
commit: (lines[1] || 'unknown').trim(),
|
|
179
|
+
lastCommit: lines.slice(2).join('\n').trim(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { branch: 'unknown', commit: 'unknown', lastCommit: '' };
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
function getProjectInfo(rootDir, cache = null) {
|
|
@@ -403,18 +448,36 @@ function checkParallelSessions(rootDir) {
|
|
|
403
448
|
};
|
|
404
449
|
|
|
405
450
|
try {
|
|
406
|
-
//
|
|
451
|
+
// PERFORMANCE OPTIMIZATION: Import session-manager directly instead of subprocess
|
|
452
|
+
// Saves ~50-150ms by avoiding Node subprocess spawn overhead
|
|
453
|
+
const sm = getSessionManager();
|
|
454
|
+
if (sm && sm.fullStatus) {
|
|
455
|
+
result.available = true;
|
|
456
|
+
const data = sm.fullStatus();
|
|
457
|
+
result.registered = data.registered;
|
|
458
|
+
result.currentId = data.id;
|
|
459
|
+
result.otherActive = data.otherActive || 0;
|
|
460
|
+
result.cleaned = data.cleaned || 0;
|
|
461
|
+
result.cleanedSessions = data.cleanedSessions || [];
|
|
462
|
+
|
|
463
|
+
if (data.current) {
|
|
464
|
+
result.isMain = data.current.is_main === true;
|
|
465
|
+
result.nickname = data.current.nickname;
|
|
466
|
+
result.branch = data.current.branch;
|
|
467
|
+
result.sessionPath = data.current.path;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fallback: check if session manager script exists for subprocess call
|
|
407
473
|
const managerPath = path.join(getAgileflowDir(rootDir), 'scripts', 'session-manager.js');
|
|
408
474
|
if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
|
|
409
475
|
return result;
|
|
410
476
|
}
|
|
411
477
|
|
|
412
478
|
result.available = true;
|
|
413
|
-
|
|
414
|
-
// Try to use combined full-status command (saves ~200ms vs 3 separate calls)
|
|
415
479
|
const scriptPath = fs.existsSync(managerPath) ? managerPath : SESSION_MANAGER_PATH;
|
|
416
480
|
|
|
417
|
-
// PERFORMANCE: Single subprocess call instead of 3 (register + count + status)
|
|
418
481
|
const fullStatusResult = executeCommandSync('node', [scriptPath, 'full-status'], {
|
|
419
482
|
cwd: rootDir,
|
|
420
483
|
fallback: null,
|
|
@@ -435,52 +498,7 @@ function checkParallelSessions(rootDir) {
|
|
|
435
498
|
result.branch = data.current.branch;
|
|
436
499
|
result.sessionPath = data.current.path;
|
|
437
500
|
}
|
|
438
|
-
} catch (e) {
|
|
439
|
-
// JSON parse failed, fall through to individual calls
|
|
440
|
-
fullStatusResult.data = null;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!fullStatusResult.data) {
|
|
445
|
-
// Fall back to individual calls if full-status not available (older version)
|
|
446
|
-
const registerResult = executeCommandSync('node', [scriptPath, 'register'], {
|
|
447
|
-
cwd: rootDir,
|
|
448
|
-
fallback: null,
|
|
449
|
-
});
|
|
450
|
-
if (registerResult.data) {
|
|
451
|
-
try {
|
|
452
|
-
const registerData = JSON.parse(registerResult.data);
|
|
453
|
-
result.registered = true;
|
|
454
|
-
result.currentId = registerData.id;
|
|
455
|
-
} catch (e) {}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const countResult = executeCommandSync('node', [scriptPath, 'count'], {
|
|
459
|
-
cwd: rootDir,
|
|
460
|
-
fallback: null,
|
|
461
|
-
});
|
|
462
|
-
if (countResult.data) {
|
|
463
|
-
try {
|
|
464
|
-
const countData = JSON.parse(countResult.data);
|
|
465
|
-
result.otherActive = countData.count || 0;
|
|
466
|
-
} catch (e) {}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const statusCmdResult = executeCommandSync('node', [scriptPath, 'status'], {
|
|
470
|
-
cwd: rootDir,
|
|
471
|
-
fallback: null,
|
|
472
|
-
});
|
|
473
|
-
if (statusCmdResult.data) {
|
|
474
|
-
try {
|
|
475
|
-
const statusData = JSON.parse(statusCmdResult.data);
|
|
476
|
-
if (statusData.current) {
|
|
477
|
-
result.isMain = statusData.current.is_main === true;
|
|
478
|
-
result.nickname = statusData.current.nickname;
|
|
479
|
-
result.branch = statusData.current.branch;
|
|
480
|
-
result.sessionPath = statusData.current.path;
|
|
481
|
-
}
|
|
482
|
-
} catch (e) {}
|
|
483
|
-
}
|
|
501
|
+
} catch (e) {}
|
|
484
502
|
}
|
|
485
503
|
} catch (e) {
|
|
486
504
|
// Session system not available
|
|
@@ -1752,7 +1770,7 @@ async function main() {
|
|
|
1752
1770
|
let tmuxCheck = { available: true };
|
|
1753
1771
|
const tmuxAutoSpawnEnabled = cache?.metadata?.features?.tmuxAutoSpawn?.enabled !== false;
|
|
1754
1772
|
if (tmuxAutoSpawnEnabled) {
|
|
1755
|
-
tmuxCheck = checkTmuxAvailability();
|
|
1773
|
+
tmuxCheck = checkTmuxAvailability(cache);
|
|
1756
1774
|
}
|
|
1757
1775
|
|
|
1758
1776
|
// Show session banner FIRST if in a non-main session
|
|
@@ -1897,14 +1915,12 @@ async function main() {
|
|
|
1897
1915
|
|
|
1898
1916
|
// === SESSION HEALTH WARNINGS ===
|
|
1899
1917
|
// Check for forgotten sessions with uncommitted changes, stale sessions, orphaned entries
|
|
1918
|
+
// PERFORMANCE OPTIMIZATION: Direct function call instead of subprocess (~50-100ms savings)
|
|
1900
1919
|
try {
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
fallback: null,
|
|
1904
|
-
});
|
|
1920
|
+
const sm = getSessionManager();
|
|
1921
|
+
const health = sm ? sm.getSessionsHealth({ staleDays: 7 }) : null;
|
|
1905
1922
|
|
|
1906
|
-
if (
|
|
1907
|
-
const health = JSON.parse(healthResult.data);
|
|
1923
|
+
if (health) {
|
|
1908
1924
|
const hasIssues =
|
|
1909
1925
|
health.uncommitted.length > 0 ||
|
|
1910
1926
|
health.stale.length > 0 ||
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -360,7 +360,7 @@ fi
|
|
|
360
360
|
# Silently remove sessions where all panes have exited (dead/empty shells).
|
|
361
361
|
# This prevents accumulation of orphan sessions over time.
|
|
362
362
|
SESSION_BASE="claude-${DIR_NAME}"
|
|
363
|
-
for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}
|
|
363
|
+
for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)"); do
|
|
364
364
|
# Count alive panes (pane_dead=0 means alive)
|
|
365
365
|
ALIVE=$(tmux list-panes -t "$sid" -F '#{pane_dead}' 2>/dev/null | grep -c '^0$' || true)
|
|
366
366
|
if [ "$ALIVE" = "0" ]; then
|
|
@@ -368,6 +368,36 @@ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESS
|
|
|
368
368
|
fi
|
|
369
369
|
done
|
|
370
370
|
|
|
371
|
+
# ── Consolidate duplicate sessions ───────────────────────────────────────
|
|
372
|
+
# Kill numbered duplicates (e.g. claude-Acuide-2, -3) that were created by
|
|
373
|
+
# a previous bug. If the base session exists, duplicates are unnecessary.
|
|
374
|
+
# If only numbered sessions remain, promote the lowest to the base name.
|
|
375
|
+
if [ "$FORCE_NEW" = false ]; then
|
|
376
|
+
HAS_BASE=false
|
|
377
|
+
NUMBERED=()
|
|
378
|
+
if tmux has-session -t "$SESSION_BASE" 2>/dev/null; then
|
|
379
|
+
HAS_BASE=true
|
|
380
|
+
fi
|
|
381
|
+
while IFS= read -r sid; do
|
|
382
|
+
[ -n "$sid" ] && NUMBERED+=("$sid")
|
|
383
|
+
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}-[0-9]+$" | sort -t- -k3 -n)
|
|
384
|
+
|
|
385
|
+
if [ "$HAS_BASE" = true ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
|
|
386
|
+
# Base exists — kill all numbered duplicates
|
|
387
|
+
for sid in "${NUMBERED[@]}"; do
|
|
388
|
+
tmux kill-session -t "$sid" 2>/dev/null || true
|
|
389
|
+
done
|
|
390
|
+
elif [ "$HAS_BASE" = false ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
|
|
391
|
+
# No base — promote lowest numbered session to base name
|
|
392
|
+
PROMOTE="${NUMBERED[0]}"
|
|
393
|
+
tmux rename-session -t "$PROMOTE" "$SESSION_BASE" 2>/dev/null || true
|
|
394
|
+
# Kill remaining duplicates
|
|
395
|
+
for sid in "${NUMBERED[@]:1}"; do
|
|
396
|
+
tmux kill-session -t "$sid" 2>/dev/null || true
|
|
397
|
+
done
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
|
|
371
401
|
# ── Auto-reattach to detached session ──────────────────────────────────────
|
|
372
402
|
# When user does Alt+Q (detach) and then runs `af` again, reattach to the
|
|
373
403
|
# existing session instead of creating a new one. This preserves tmux windows,
|
|
@@ -377,7 +407,7 @@ if [ "$FORCE_NEW" = false ]; then
|
|
|
377
407
|
DETACHED=()
|
|
378
408
|
while IFS= read -r sid; do
|
|
379
409
|
[ -n "$sid" ] && DETACHED+=("$sid")
|
|
380
|
-
done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep "^${SESSION_BASE}
|
|
410
|
+
done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
|
|
381
411
|
|
|
382
412
|
if [ "${#DETACHED[@]}" -eq 1 ]; then
|
|
383
413
|
# Single detached session — just reattach
|
|
@@ -416,7 +446,7 @@ if [ "$FORCE_NEW" = false ]; then
|
|
|
416
446
|
EXISTING=()
|
|
417
447
|
while IFS= read -r sid; do
|
|
418
448
|
[ -n "$sid" ] && EXISTING+=("$sid")
|
|
419
|
-
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}
|
|
449
|
+
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
|
|
420
450
|
|
|
421
451
|
if [ "${#EXISTING[@]}" -gt 0 ]; then
|
|
422
452
|
# Prefer the base session, otherwise pick the first one
|
|
@@ -1215,6 +1215,119 @@ function upgradeFeatures(status, version) {
|
|
|
1215
1215
|
return upgraded > 0;
|
|
1216
1216
|
}
|
|
1217
1217
|
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
// STARTUP MODE (atomic command)
|
|
1220
|
+
// ============================================================================
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Valid startup modes and their mappings
|
|
1224
|
+
*/
|
|
1225
|
+
const STARTUP_MODES = {
|
|
1226
|
+
'skip-permissions': {
|
|
1227
|
+
flags: '--dangerously-skip-permissions',
|
|
1228
|
+
defaultMode: 'bypassPermissions',
|
|
1229
|
+
description: 'Skip all permission prompts (trusted mode)',
|
|
1230
|
+
},
|
|
1231
|
+
'accept-edits': {
|
|
1232
|
+
flags: '--permission-mode acceptEdits',
|
|
1233
|
+
defaultMode: 'acceptEdits',
|
|
1234
|
+
description: 'Auto-accept file edits, prompt for other actions',
|
|
1235
|
+
},
|
|
1236
|
+
normal: {
|
|
1237
|
+
flags: null,
|
|
1238
|
+
defaultMode: null,
|
|
1239
|
+
description: 'Standard Claude with permission prompts',
|
|
1240
|
+
},
|
|
1241
|
+
'no-claude': {
|
|
1242
|
+
flags: null,
|
|
1243
|
+
defaultMode: null,
|
|
1244
|
+
description: 'Create worktree only, start Claude manually',
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Set startup mode atomically - updates BOTH metadata AND .claude/settings.json
|
|
1250
|
+
* This replaces the fragile two-step process of updating metadata + running --enable=claudeflags
|
|
1251
|
+
*
|
|
1252
|
+
* @param {string} mode - One of: skip-permissions, accept-edits, normal, no-claude
|
|
1253
|
+
* @param {string} version - Current version string
|
|
1254
|
+
* @returns {boolean} Success
|
|
1255
|
+
*/
|
|
1256
|
+
function enableStartupMode(mode, version) {
|
|
1257
|
+
const modeConfig = STARTUP_MODES[mode];
|
|
1258
|
+
if (!modeConfig) {
|
|
1259
|
+
error(`Unknown startup mode: ${mode}`);
|
|
1260
|
+
log(` Valid modes: ${Object.keys(STARTUP_MODES).join(', ')}`, c.dim);
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
ensureDir('.claude');
|
|
1265
|
+
const settings = readJSON('.claude/settings.json') || {};
|
|
1266
|
+
settings.permissions = settings.permissions || { allow: [], deny: [], ask: [] };
|
|
1267
|
+
|
|
1268
|
+
if (mode === 'normal' || mode === 'no-claude') {
|
|
1269
|
+
// Remove defaultMode from settings
|
|
1270
|
+
if (settings.permissions.defaultMode) {
|
|
1271
|
+
delete settings.permissions.defaultMode;
|
|
1272
|
+
}
|
|
1273
|
+
writeJSON('.claude/settings.json', settings);
|
|
1274
|
+
|
|
1275
|
+
// Disable claudeflags + set defaultStartupMode in metadata (single write)
|
|
1276
|
+
updateMetadata(
|
|
1277
|
+
{
|
|
1278
|
+
features: {
|
|
1279
|
+
claudeFlags: {
|
|
1280
|
+
enabled: false,
|
|
1281
|
+
defaultFlags: '',
|
|
1282
|
+
version,
|
|
1283
|
+
at: new Date().toISOString(),
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
version
|
|
1288
|
+
);
|
|
1289
|
+
} else {
|
|
1290
|
+
// Set defaultMode in settings.json
|
|
1291
|
+
settings.permissions.defaultMode = modeConfig.defaultMode;
|
|
1292
|
+
writeJSON('.claude/settings.json', settings);
|
|
1293
|
+
|
|
1294
|
+
// Enable claudeflags + set defaultStartupMode in metadata (single write)
|
|
1295
|
+
updateMetadata(
|
|
1296
|
+
{
|
|
1297
|
+
features: {
|
|
1298
|
+
claudeFlags: {
|
|
1299
|
+
enabled: true,
|
|
1300
|
+
defaultFlags: modeConfig.flags,
|
|
1301
|
+
version,
|
|
1302
|
+
at: new Date().toISOString(),
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
},
|
|
1306
|
+
version
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Set defaultStartupMode in metadata (updateMetadata already created file if missing)
|
|
1311
|
+
const metaPath = 'docs/00-meta/agileflow-metadata.json';
|
|
1312
|
+
const meta = readJSON(metaPath) || {};
|
|
1313
|
+
meta.defaultStartupMode = mode;
|
|
1314
|
+
meta.updated = new Date().toISOString();
|
|
1315
|
+
writeJSON(metaPath, meta);
|
|
1316
|
+
|
|
1317
|
+
success(`Default startup mode set to: ${mode}`);
|
|
1318
|
+
if (modeConfig.defaultMode) {
|
|
1319
|
+
info(`Set permissions.defaultMode = "${modeConfig.defaultMode}" in .claude/settings.json`);
|
|
1320
|
+
} else {
|
|
1321
|
+
info('Removed permissions.defaultMode from .claude/settings.json');
|
|
1322
|
+
}
|
|
1323
|
+
info(`Metadata: defaultStartupMode = "${mode}"`);
|
|
1324
|
+
if (mode !== 'normal') {
|
|
1325
|
+
info('Restart Claude Code for the new mode to take effect');
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1218
1331
|
// ============================================================================
|
|
1219
1332
|
// SHELL ALIASES
|
|
1220
1333
|
// ============================================================================
|
|
@@ -1473,6 +1586,9 @@ module.exports = {
|
|
|
1473
1586
|
// Helpers
|
|
1474
1587
|
scriptExists,
|
|
1475
1588
|
getScriptPath,
|
|
1589
|
+
// Startup mode
|
|
1590
|
+
enableStartupMode,
|
|
1591
|
+
STARTUP_MODES,
|
|
1476
1592
|
// Shell aliases
|
|
1477
1593
|
enableShellAliases,
|
|
1478
1594
|
disableShellAliases,
|