agileflow 2.94.0 → 2.95.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 +20 -0
- package/README.md +6 -6
- package/lib/colors.generated.js +117 -0
- package/lib/colors.js +59 -109
- package/lib/generator-factory.js +333 -0
- package/lib/path-utils.js +49 -0
- package/lib/session-registry.js +25 -15
- package/lib/smart-json-file.js +40 -32
- package/lib/state-machine.js +286 -0
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +7 -6
- package/scripts/archive-completed-stories.sh +86 -11
- package/scripts/babysit-context-restore.js +89 -0
- package/scripts/claude-tmux.sh +186 -7
- package/scripts/damage-control/bash-tool-damage-control.js +11 -247
- package/scripts/damage-control/edit-tool-damage-control.js +9 -249
- package/scripts/damage-control/write-tool-damage-control.js +9 -244
- package/scripts/generate-colors.js +314 -0
- package/scripts/lib/colors.generated.sh +82 -0
- package/scripts/lib/colors.sh +10 -70
- package/scripts/lib/configure-features.js +401 -0
- package/scripts/lib/context-loader.js +181 -52
- package/scripts/precompact-context.sh +54 -17
- package/scripts/session-coordinator.sh +2 -2
- package/scripts/session-manager.js +677 -11
- package/src/core/agents/council-advocate.md +202 -0
- package/src/core/agents/council-analyst.md +248 -0
- package/src/core/agents/council-optimist.md +166 -0
- package/src/core/commands/audit.md +93 -0
- package/src/core/commands/auto.md +73 -0
- package/src/core/commands/babysit.md +169 -13
- package/src/core/commands/baseline.md +73 -0
- package/src/core/commands/batch.md +64 -0
- package/src/core/commands/blockers.md +60 -0
- package/src/core/commands/board.md +66 -0
- package/src/core/commands/choose.md +77 -0
- package/src/core/commands/ci.md +77 -0
- package/src/core/commands/compress.md +27 -1
- package/src/core/commands/configure.md +126 -10
- package/src/core/commands/council.md +591 -0
- package/src/core/commands/debt.md +72 -0
- package/src/core/commands/deploy.md +73 -0
- package/src/core/commands/deps.md +68 -0
- package/src/core/commands/docs.md +60 -0
- package/src/core/commands/feedback.md +68 -0
- package/src/core/commands/help.md +189 -3
- package/src/core/commands/ideate.md +219 -20
- package/src/core/commands/impact.md +74 -0
- package/src/core/commands/install.md +529 -0
- package/src/core/commands/maintain.md +558 -0
- package/src/core/commands/metrics.md +75 -0
- package/src/core/commands/multi-expert.md +74 -0
- package/src/core/commands/packages.md +69 -0
- package/src/core/commands/readme-sync.md +64 -0
- package/src/core/commands/research/analyze.md +285 -121
- package/src/core/commands/research/import.md +281 -109
- package/src/core/commands/retro.md +76 -0
- package/src/core/commands/review.md +72 -0
- package/src/core/commands/rlm.md +83 -0
- package/src/core/commands/rpi.md +90 -0
- package/src/core/commands/session/cleanup.md +214 -12
- package/src/core/commands/session/end.md +229 -17
- package/src/core/commands/sprint.md +72 -0
- package/src/core/commands/story-validate.md +68 -0
- package/src/core/commands/template.md +69 -0
- package/src/core/commands/tests.md +83 -0
- package/src/core/commands/update.md +59 -0
- package/src/core/commands/validate-expertise.md +76 -0
- package/src/core/commands/velocity.md +74 -0
- package/src/core/commands/verify.md +91 -0
- package/src/core/commands/whats-new.md +69 -0
- package/src/core/commands/workflow.md +88 -0
- package/src/core/council/sessions/.gitkeep +0 -0
- package/src/core/council/shared_reasoning.template.md +106 -0
- package/src/core/templates/command-documentation.md +187 -0
- package/tools/cli/commands/session.js +1171 -0
- package/tools/cli/commands/setup.js +2 -81
- package/tools/cli/installers/core/installer.js +0 -5
- package/tools/cli/installers/ide/claude-code.js +6 -0
- package/tools/cli/lib/config-manager.js +42 -5
|
@@ -105,13 +105,87 @@ echo -e "${BLUE}Cutoff date: $CUTOFF_DATE${NC}"
|
|
|
105
105
|
|
|
106
106
|
# Archive using Node.js (more reliable for JSON manipulation)
|
|
107
107
|
if command -v node &> /dev/null; then
|
|
108
|
-
STATUS_FILE="$STATUS_FILE" ARCHIVE_DIR="$ARCHIVE_DIR" CUTOFF_DATE="$CUTOFF_DATE" node <<'EOF'
|
|
108
|
+
STATUS_FILE="$STATUS_FILE" ARCHIVE_DIR="$ARCHIVE_DIR" CUTOFF_DATE="$CUTOFF_DATE" PROJECT_ROOT="$PROJECT_ROOT" node <<'EOF'
|
|
109
109
|
const fs = require('fs');
|
|
110
110
|
const path = require('path');
|
|
111
111
|
|
|
112
112
|
const statusFile = process.env.STATUS_FILE;
|
|
113
113
|
const archiveDir = process.env.ARCHIVE_DIR;
|
|
114
114
|
const cutoffDate = process.env.CUTOFF_DATE;
|
|
115
|
+
const projectRoot = process.env.PROJECT_ROOT;
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Security: Inline validatePath equivalent (US-0188)
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate a path is safe and within the base directory.
|
|
123
|
+
* Rejects direct symlinks within the path but allows symlinked parent directories
|
|
124
|
+
* (needed for git worktrees where docs/ is often symlinked).
|
|
125
|
+
* @param {string} inputPath - Path to validate
|
|
126
|
+
* @param {string} baseDir - Allowed base directory
|
|
127
|
+
* @returns {{ ok: boolean, resolvedPath?: string, realPath?: string, error?: string }}
|
|
128
|
+
*/
|
|
129
|
+
function validatePath(inputPath, baseDir) {
|
|
130
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
131
|
+
return { ok: false, error: 'Path is required and must be a string' };
|
|
132
|
+
}
|
|
133
|
+
if (!baseDir || typeof baseDir !== 'string') {
|
|
134
|
+
return { ok: false, error: 'Base directory is required' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve to absolute path
|
|
138
|
+
const resolvedPath = path.resolve(baseDir, inputPath);
|
|
139
|
+
const resolvedBase = path.resolve(baseDir);
|
|
140
|
+
|
|
141
|
+
// Check path stays within base directory (path traversal prevention)
|
|
142
|
+
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
143
|
+
return { ok: false, error: `Path traversal detected: ${inputPath} escapes ${baseDir}` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if the final target path itself is a symlink (allowSymlinks: false for target)
|
|
147
|
+
// Note: We allow parent directories to be symlinks (needed for git worktrees)
|
|
148
|
+
try {
|
|
149
|
+
const stats = fs.lstatSync(resolvedPath);
|
|
150
|
+
if (stats.isSymbolicLink()) {
|
|
151
|
+
// The actual file/directory we're writing to is a symlink - reject
|
|
152
|
+
return { ok: false, error: `Target path is a symlink: ${resolvedPath}` };
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Path doesn't exist yet, that's OK for new files
|
|
156
|
+
if (e.code !== 'ENOENT') {
|
|
157
|
+
return { ok: false, error: `Cannot stat path: ${e.message}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Use fs.realpathSync() to get the actual path after symlink resolution
|
|
162
|
+
let realPath = resolvedPath;
|
|
163
|
+
try {
|
|
164
|
+
realPath = fs.realpathSync(resolvedPath);
|
|
165
|
+
// We don't restrict realPath to baseDir because parent directories may be
|
|
166
|
+
// symlinked (e.g., git worktrees). The key protection is:
|
|
167
|
+
// 1. path.resolve() prevents ../../ traversal in the input
|
|
168
|
+
// 2. lstatSync() above prevents the target itself from being a symlink
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// Path doesn't exist yet, use resolved path
|
|
171
|
+
if (e.code !== 'ENOENT') {
|
|
172
|
+
return { ok: false, error: `Cannot resolve real path: ${e.message}` };
|
|
173
|
+
}
|
|
174
|
+
realPath = resolvedPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { ok: true, resolvedPath, realPath };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Validate archive directory (US-0188)
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
const archiveDirValidation = validatePath(archiveDir, projectRoot);
|
|
185
|
+
if (!archiveDirValidation.ok) {
|
|
186
|
+
console.error(`\x1b[31mSecurity: ${archiveDirValidation.error}. Aborting.\x1b[0m`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
115
189
|
|
|
116
190
|
// Read status.json
|
|
117
191
|
const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
@@ -146,7 +220,7 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
146
220
|
const date = new Date(story.completed_at);
|
|
147
221
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
148
222
|
|
|
149
|
-
// Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal
|
|
223
|
+
// Security: Validate monthKey matches expected format (YYYY-MM) to prevent path traversal (US-0188 AC)
|
|
150
224
|
if (!/^\d{4}-\d{2}$/.test(monthKey)) {
|
|
151
225
|
console.error(`\x1b[31mSkipping story ${storyId}: invalid date format\x1b[0m`);
|
|
152
226
|
continue;
|
|
@@ -165,27 +239,28 @@ for (const [storyId, story] of Object.entries(toArchive)) {
|
|
|
165
239
|
|
|
166
240
|
// Write archive files
|
|
167
241
|
for (const [monthKey, archiveData] of Object.entries(byMonth)) {
|
|
168
|
-
const archiveFile =
|
|
242
|
+
const archiveFile = `${monthKey}.json`;
|
|
169
243
|
|
|
170
|
-
// Security:
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.error(`\x1b[31mSecurity: Archive path ${archiveFile} escapes archive directory. Skipping.\x1b[0m`);
|
|
244
|
+
// Security: Use validatePath() with allowSymlinks: false (US-0188 AC)
|
|
245
|
+
const validation = validatePath(archiveFile, archiveDir);
|
|
246
|
+
if (!validation.ok) {
|
|
247
|
+
console.error(`\x1b[31mSecurity: ${validation.error}. Skipping ${monthKey}.\x1b[0m`);
|
|
175
248
|
continue;
|
|
176
249
|
}
|
|
177
250
|
|
|
251
|
+
const finalPath = validation.resolvedPath;
|
|
252
|
+
|
|
178
253
|
// Merge with existing archive if it exists
|
|
179
|
-
if (fs.existsSync(
|
|
254
|
+
if (fs.existsSync(finalPath)) {
|
|
180
255
|
try {
|
|
181
|
-
const existing = JSON.parse(fs.readFileSync(
|
|
256
|
+
const existing = JSON.parse(fs.readFileSync(finalPath, 'utf8'));
|
|
182
257
|
archiveData.stories = { ...existing.stories, ...archiveData.stories };
|
|
183
258
|
} catch (e) {
|
|
184
259
|
console.error(`\x1b[31mWarning: Could not parse existing ${monthKey}.json, will overwrite\x1b[0m`);
|
|
185
260
|
}
|
|
186
261
|
}
|
|
187
262
|
|
|
188
|
-
fs.writeFileSync(
|
|
263
|
+
fs.writeFileSync(finalPath, JSON.stringify(archiveData, null, 2));
|
|
189
264
|
const count = Object.keys(archiveData.stories).length;
|
|
190
265
|
console.log(`\x1b[32m✓ Archived ${count} stories to ${monthKey}.json\x1b[0m`);
|
|
191
266
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* babysit-context-restore.js - UserPromptSubmit hook
|
|
4
|
+
*
|
|
5
|
+
* Backup mechanism to restore babysit context after plan mode clears context.
|
|
6
|
+
* When user selects "Clear context and bypass permissions" after ExitPlanMode,
|
|
7
|
+
* this hook fires on the next user prompt and reminds Claude of babysit rules.
|
|
8
|
+
*
|
|
9
|
+
* The primary mechanism is embedding rules in the plan file (Rule #6).
|
|
10
|
+
* This hook is a backup for edge cases where plan file approach might miss.
|
|
11
|
+
*
|
|
12
|
+
* Usage: Called automatically as UserPromptSubmit hook
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Find session-state.json - try multiple locations
|
|
19
|
+
function findSessionState() {
|
|
20
|
+
const locations = [
|
|
21
|
+
'docs/09-agents/session-state.json',
|
|
22
|
+
path.join(process.env.CLAUDE_PROJECT_DIR || '.', 'docs/09-agents/session-state.json'),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
for (const loc of locations) {
|
|
26
|
+
if (fs.existsSync(loc)) {
|
|
27
|
+
return loc;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
const sessionStatePath = findSessionState();
|
|
35
|
+
if (!sessionStatePath) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
39
|
+
|
|
40
|
+
// Check if restoration is pending
|
|
41
|
+
if (!state.babysit_pending_restore) return;
|
|
42
|
+
|
|
43
|
+
// Output restoration context
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
46
|
+
console.log(
|
|
47
|
+
'\x1b[36m║\x1b[0m \x1b[1m\x1b[33m/babysit CONTEXT RESTORED\x1b[0m \x1b[36m║\x1b[0m'
|
|
48
|
+
);
|
|
49
|
+
console.log('\x1b[36m╠══════════════════════════════════════════════════════════════╣\x1b[0m');
|
|
50
|
+
console.log(
|
|
51
|
+
'\x1b[36m║\x1b[0m /agileflow:babysit was active before context clear. \x1b[36m║\x1b[0m'
|
|
52
|
+
);
|
|
53
|
+
console.log(
|
|
54
|
+
'\x1b[36m║\x1b[0m These rules are MANDATORY: \x1b[36m║\x1b[0m'
|
|
55
|
+
);
|
|
56
|
+
console.log(
|
|
57
|
+
'\x1b[36m║\x1b[0m \x1b[36m║\x1b[0m'
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
'\x1b[36m║\x1b[0m 1. ALWAYS end responses with AskUserQuestion tool \x1b[36m║\x1b[0m'
|
|
61
|
+
);
|
|
62
|
+
console.log(
|
|
63
|
+
'\x1b[36m║\x1b[0m 2. Use EnterPlanMode before non-trivial tasks \x1b[36m║\x1b[0m'
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
'\x1b[36m║\x1b[0m 3. Delegate complex work to domain experts \x1b[36m║\x1b[0m'
|
|
67
|
+
);
|
|
68
|
+
console.log(
|
|
69
|
+
'\x1b[36m║\x1b[0m 4. Track progress with TodoWrite for 3+ step tasks \x1b[36m║\x1b[0m'
|
|
70
|
+
);
|
|
71
|
+
console.log(
|
|
72
|
+
'\x1b[36m║\x1b[0m \x1b[36m║\x1b[0m'
|
|
73
|
+
);
|
|
74
|
+
console.log(
|
|
75
|
+
'\x1b[36m║\x1b[0m For full context: /agileflow:babysit \x1b[36m║\x1b[0m'
|
|
76
|
+
);
|
|
77
|
+
console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m');
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
80
|
+
// Clear the flag (one-time restoration)
|
|
81
|
+
state.babysit_pending_restore = false;
|
|
82
|
+
state.babysit_restored_at = new Date().toISOString();
|
|
83
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Silently fail - don't break user's workflow
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -5,28 +5,125 @@
|
|
|
5
5
|
# ./claude-tmux.sh # Start in tmux with default session
|
|
6
6
|
# ./claude-tmux.sh --no-tmux # Start without tmux (regular claude)
|
|
7
7
|
# ./claude-tmux.sh -n # Same as --no-tmux
|
|
8
|
+
# ./claude-tmux.sh --rescue # Kill frozen session and restart fresh
|
|
9
|
+
# ./claude-tmux.sh --kill # Kill existing session completely
|
|
10
|
+
# ./claude-tmux.sh --help # Show help with keybinds
|
|
8
11
|
#
|
|
9
12
|
# When already in tmux: Just runs claude normally
|
|
10
13
|
# When not in tmux: Creates a tmux session and runs claude inside it
|
|
14
|
+
#
|
|
15
|
+
# FREEZE RECOVERY:
|
|
16
|
+
# If Claude freezes inside tmux, use these keybinds:
|
|
17
|
+
# - Alt+k Send Ctrl+C twice (soft interrupt)
|
|
18
|
+
# - Alt+K Force kill the pane immediately
|
|
19
|
+
# - Alt+R Respawn pane with fresh shell
|
|
20
|
+
# - Alt+q Detach from tmux (session stays alive)
|
|
11
21
|
|
|
12
22
|
set -e
|
|
13
23
|
|
|
14
|
-
#
|
|
24
|
+
# Parse arguments
|
|
15
25
|
NO_TMUX=false
|
|
26
|
+
RESCUE=false
|
|
27
|
+
KILL_SESSION=false
|
|
28
|
+
SHOW_HELP=false
|
|
29
|
+
|
|
16
30
|
for arg in "$@"; do
|
|
17
31
|
case $arg in
|
|
18
32
|
--no-tmux|-n)
|
|
19
33
|
NO_TMUX=true
|
|
20
34
|
shift
|
|
21
35
|
;;
|
|
36
|
+
--rescue|-r)
|
|
37
|
+
RESCUE=true
|
|
38
|
+
shift
|
|
39
|
+
;;
|
|
40
|
+
--kill)
|
|
41
|
+
KILL_SESSION=true
|
|
42
|
+
shift
|
|
43
|
+
;;
|
|
44
|
+
--help|-h)
|
|
45
|
+
SHOW_HELP=true
|
|
46
|
+
shift
|
|
47
|
+
;;
|
|
22
48
|
esac
|
|
23
49
|
done
|
|
24
50
|
|
|
51
|
+
# Show help
|
|
52
|
+
if [ "$SHOW_HELP" = true ]; then
|
|
53
|
+
cat << 'EOF'
|
|
54
|
+
AgileFlow Claude tmux Wrapper
|
|
55
|
+
|
|
56
|
+
USAGE:
|
|
57
|
+
af [options] [claude-args...]
|
|
58
|
+
agileflow [options] [claude-args...]
|
|
59
|
+
|
|
60
|
+
OPTIONS:
|
|
61
|
+
--no-tmux, -n Run claude without tmux
|
|
62
|
+
--rescue, -r Kill frozen session and restart fresh
|
|
63
|
+
--kill Kill existing session completely
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
|
|
66
|
+
TMUX KEYBINDS:
|
|
67
|
+
Alt+1-9 Switch to window N
|
|
68
|
+
Alt+c Create new window
|
|
69
|
+
Alt+n/p Next/previous window
|
|
70
|
+
Alt+d Split horizontally
|
|
71
|
+
Alt+s Split vertically
|
|
72
|
+
Alt+arrows Navigate panes
|
|
73
|
+
Alt+z Zoom/unzoom pane
|
|
74
|
+
Alt+[ Enter copy mode (scroll)
|
|
75
|
+
Alt+r Rename window
|
|
76
|
+
Alt+x Close pane
|
|
77
|
+
Alt+w Close window
|
|
78
|
+
Alt+q Detach from tmux
|
|
79
|
+
|
|
80
|
+
FREEZE RECOVERY:
|
|
81
|
+
Alt+k Send Ctrl+C twice (soft interrupt)
|
|
82
|
+
Alt+K Force kill pane immediately
|
|
83
|
+
Alt+R Respawn pane with fresh shell
|
|
84
|
+
|
|
85
|
+
If Claude is completely frozen and keybinds don't work:
|
|
86
|
+
1. Open a new terminal
|
|
87
|
+
2. Run: af --rescue (kills and restarts)
|
|
88
|
+
3. Or: af --kill (just kills, doesn't restart)
|
|
89
|
+
EOF
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
25
93
|
# If --no-tmux was specified, just run claude directly
|
|
26
94
|
if [ "$NO_TMUX" = true ]; then
|
|
27
95
|
exec claude "$@"
|
|
28
96
|
fi
|
|
29
97
|
|
|
98
|
+
# Generate session name based on current directory (needed for rescue/kill)
|
|
99
|
+
DIR_NAME=$(basename "$(pwd)")
|
|
100
|
+
SESSION_NAME="claude-${DIR_NAME}"
|
|
101
|
+
|
|
102
|
+
# Handle --kill flag
|
|
103
|
+
if [ "$KILL_SESSION" = true ]; then
|
|
104
|
+
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
105
|
+
echo "Killing session: $SESSION_NAME"
|
|
106
|
+
tmux kill-session -t "$SESSION_NAME"
|
|
107
|
+
echo "Session killed."
|
|
108
|
+
else
|
|
109
|
+
echo "No session named '$SESSION_NAME' found."
|
|
110
|
+
fi
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Handle --rescue flag (kill and restart)
|
|
115
|
+
if [ "$RESCUE" = true ]; then
|
|
116
|
+
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
117
|
+
echo "Killing frozen session: $SESSION_NAME"
|
|
118
|
+
tmux kill-session -t "$SESSION_NAME"
|
|
119
|
+
echo "Session killed. Restarting..."
|
|
120
|
+
sleep 0.5
|
|
121
|
+
else
|
|
122
|
+
echo "No existing session to rescue. Starting fresh..."
|
|
123
|
+
fi
|
|
124
|
+
# Continue to create new session below
|
|
125
|
+
fi
|
|
126
|
+
|
|
30
127
|
# Check if tmux auto-spawn is disabled in config
|
|
31
128
|
METADATA_FILE="docs/00-meta/agileflow-metadata.json"
|
|
32
129
|
if [ -f "$METADATA_FILE" ]; then
|
|
@@ -62,9 +159,7 @@ if ! command -v tmux &> /dev/null; then
|
|
|
62
159
|
exec claude "$@"
|
|
63
160
|
fi
|
|
64
161
|
|
|
65
|
-
#
|
|
66
|
-
DIR_NAME=$(basename "$(pwd)")
|
|
67
|
-
SESSION_NAME="claude-${DIR_NAME}"
|
|
162
|
+
# SESSION_NAME already generated above (needed for --rescue and --kill)
|
|
68
163
|
|
|
69
164
|
# Check if session already exists
|
|
70
165
|
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
@@ -78,18 +173,102 @@ echo "Starting Claude in tmux session: $SESSION_NAME"
|
|
|
78
173
|
# Create session in detached mode first
|
|
79
174
|
tmux new-session -d -s "$SESSION_NAME" -n "main"
|
|
80
175
|
|
|
81
|
-
#
|
|
176
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
177
|
+
# TMUX CONFIGURATION - Modern status bar with keybinds
|
|
178
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
# Enable mouse support
|
|
82
181
|
tmux set-option -t "$SESSION_NAME" mouse on
|
|
83
182
|
|
|
84
183
|
# Fix colors - proper terminal support
|
|
85
184
|
tmux set-option -t "$SESSION_NAME" default-terminal "xterm-256color"
|
|
185
|
+
tmux set-option -t "$SESSION_NAME" -ga terminal-overrides ",xterm-256color:Tc"
|
|
186
|
+
|
|
187
|
+
# ─── Status Bar Styling (2-line) ──────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
# Status bar position and refresh
|
|
190
|
+
tmux set-option -t "$SESSION_NAME" status-position bottom
|
|
191
|
+
tmux set-option -t "$SESSION_NAME" status-interval 5
|
|
192
|
+
|
|
193
|
+
# Enable 2-line status bar
|
|
194
|
+
tmux set-option -t "$SESSION_NAME" status 2
|
|
195
|
+
|
|
196
|
+
# Base styling - Tokyo Night inspired dark theme
|
|
197
|
+
tmux set-option -t "$SESSION_NAME" status-style "bg=#1a1b26,fg=#a9b1d6"
|
|
198
|
+
|
|
199
|
+
# Line 0 (top): Session name (stripped of claude- prefix) + Keybinds + Git branch
|
|
200
|
+
# Shows freeze recovery keys: Alt+k (soft kill), Alt+K (hard kill)
|
|
201
|
+
tmux set-option -t "$SESSION_NAME" status-format[0] "#[bg=#1a1b26] #[fg=#e8683a bold]#{s/claude-//:session_name} #[fg=#3b4261]· #[fg=#7aa2f7] #(git branch --show-current 2>/dev/null || echo '-') #[align=right]#[fg=#7a7e8a]Alt+k freeze Alt+x close Alt+q detach "
|
|
202
|
+
|
|
203
|
+
# Line 1 (bottom): Window tabs with smart truncation and brand color
|
|
204
|
+
# - Active window: full name (max 15 chars), brand orange highlight
|
|
205
|
+
# - Inactive windows: truncate to 8 chars with ... suffix, warm gray
|
|
206
|
+
tmux set-option -t "$SESSION_NAME" status-format[1] "#[bg=#1a1b26]#{W:#{?window_active,#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{=15:window_name} #[bg=#1a1b26 fg=#2d2f3a],#[fg=#8a8a8a] #I:#{=|8|...:window_name} }}"
|
|
86
207
|
|
|
87
|
-
#
|
|
208
|
+
# Pane border styling - blue inactive, orange active
|
|
209
|
+
tmux set-option -t "$SESSION_NAME" pane-border-style "fg=#3d59a1"
|
|
210
|
+
tmux set-option -t "$SESSION_NAME" pane-active-border-style "fg=#e8683a"
|
|
211
|
+
|
|
212
|
+
# Message styling - orange highlight
|
|
213
|
+
tmux set-option -t "$SESSION_NAME" message-style "bg=#e8683a,fg=#1a1b26,bold"
|
|
214
|
+
|
|
215
|
+
# ─── Keybindings ──────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
# Window numbering starts at 1 (not 0)
|
|
218
|
+
tmux set-option -t "$SESSION_NAME" base-index 1
|
|
219
|
+
|
|
220
|
+
# Alt+number to switch windows (1-9)
|
|
88
221
|
for i in 1 2 3 4 5 6 7 8 9; do
|
|
89
|
-
tmux bind-key -n "M-$i" select-window -t ":$
|
|
222
|
+
tmux bind-key -n "M-$i" select-window -t ":$i"
|
|
90
223
|
done
|
|
224
|
+
|
|
225
|
+
# Alt+c to create new window
|
|
226
|
+
tmux bind-key -n M-c new-window -c "#{pane_current_path}"
|
|
227
|
+
|
|
228
|
+
# Alt+q to detach
|
|
91
229
|
tmux bind-key -n M-q detach-client
|
|
92
230
|
|
|
231
|
+
# Alt+d to split horizontally (side by side)
|
|
232
|
+
tmux bind-key -n M-d split-window -h -c "#{pane_current_path}"
|
|
233
|
+
|
|
234
|
+
# Alt+s to split vertically (top/bottom)
|
|
235
|
+
tmux bind-key -n M-s split-window -v -c "#{pane_current_path}"
|
|
236
|
+
|
|
237
|
+
# Alt+arrow to navigate panes
|
|
238
|
+
tmux bind-key -n M-Left select-pane -L
|
|
239
|
+
tmux bind-key -n M-Right select-pane -R
|
|
240
|
+
tmux bind-key -n M-Up select-pane -U
|
|
241
|
+
tmux bind-key -n M-Down select-pane -D
|
|
242
|
+
|
|
243
|
+
# Alt+x to close current pane (with confirmation)
|
|
244
|
+
tmux bind-key -n M-x confirm-before -p "Close pane? (y/n)" kill-pane
|
|
245
|
+
|
|
246
|
+
# Alt+w to close current window (with confirmation)
|
|
247
|
+
tmux bind-key -n M-w confirm-before -p "Close window? (y/n)" kill-window
|
|
248
|
+
|
|
249
|
+
# Alt+n/p for next/previous window
|
|
250
|
+
tmux bind-key -n M-n next-window
|
|
251
|
+
tmux bind-key -n M-p previous-window
|
|
252
|
+
|
|
253
|
+
# Alt+r to rename window
|
|
254
|
+
tmux bind-key -n M-r command-prompt -I "#W" "rename-window '%%'"
|
|
255
|
+
|
|
256
|
+
# Alt+z to zoom/unzoom pane (fullscreen toggle)
|
|
257
|
+
tmux bind-key -n M-z resize-pane -Z
|
|
258
|
+
|
|
259
|
+
# Alt+[ to enter copy mode (for scrolling)
|
|
260
|
+
tmux bind-key -n M-[ copy-mode
|
|
261
|
+
|
|
262
|
+
# ─── Freeze Recovery Keybindings ─────────────────────────────────────────────
|
|
263
|
+
# Alt+k to send Ctrl+C twice (soft interrupt for frozen processes)
|
|
264
|
+
tmux bind-key -n M-k run-shell "tmux send-keys C-c; sleep 0.5; tmux send-keys C-c"
|
|
265
|
+
|
|
266
|
+
# Alt+K (shift+k) to force-kill pane immediately (nuclear option for hard freezes)
|
|
267
|
+
tmux bind-key -n M-K kill-pane
|
|
268
|
+
|
|
269
|
+
# Alt+R (shift+r) to respawn the pane (restart with a fresh shell)
|
|
270
|
+
tmux bind-key -n M-R respawn-pane -k
|
|
271
|
+
|
|
93
272
|
# Send the claude command to the first window
|
|
94
273
|
CLAUDE_CMD="claude"
|
|
95
274
|
if [ $# -gt 0 ]; then
|