agileflow 2.94.1 → 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 +15 -0
- 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 +111 -5
- 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 +653 -10
- 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 +74 -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/ideate.md +74 -0
- 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 +155 -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/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
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
|
|
@@ -102,7 +197,8 @@ tmux set-option -t "$SESSION_NAME" status 2
|
|
|
102
197
|
tmux set-option -t "$SESSION_NAME" status-style "bg=#1a1b26,fg=#a9b1d6"
|
|
103
198
|
|
|
104
199
|
# Line 0 (top): Session name (stripped of claude- prefix) + Keybinds + Git branch
|
|
105
|
-
|
|
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 "
|
|
106
202
|
|
|
107
203
|
# Line 1 (bottom): Window tabs with smart truncation and brand color
|
|
108
204
|
# - Active window: full name (max 15 chars), brand orange highlight
|
|
@@ -163,6 +259,16 @@ tmux bind-key -n M-z resize-pane -Z
|
|
|
163
259
|
# Alt+[ to enter copy mode (for scrolling)
|
|
164
260
|
tmux bind-key -n M-[ copy-mode
|
|
165
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
|
+
|
|
166
272
|
# Send the claude command to the first window
|
|
167
273
|
CLAUDE_CMD="claude"
|
|
168
274
|
if [ $# -gt 0 ]; then
|
|
@@ -1,258 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* bash-tool-damage-control.js -
|
|
4
|
+
* bash-tool-damage-control.js - PreToolUse hook for Bash tool
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* confirmation on dangerous commands.
|
|
6
|
+
* Validates bash commands against patterns in damage-control-patterns.yaml
|
|
7
|
+
* before execution. Part of AgileFlow's damage control system.
|
|
9
8
|
*
|
|
10
9
|
* Exit codes:
|
|
11
|
-
* 0
|
|
12
|
-
* 2
|
|
10
|
+
* 0 - Allow command (or ask via JSON output)
|
|
11
|
+
* 2 - Block command
|
|
13
12
|
*
|
|
14
|
-
* For ask
|
|
15
|
-
* {"result": "ask", "message": "
|
|
13
|
+
* For "ask" response, output JSON to stdout:
|
|
14
|
+
* { "result": "ask", "message": "Confirm this action?" }
|
|
16
15
|
*
|
|
17
|
-
* Usage
|
|
18
|
-
* node .claude/hooks/damage-control/bash-tool-damage-control.js
|
|
19
|
-
*
|
|
20
|
-
* Environment:
|
|
21
|
-
* CLAUDE_TOOL_INPUT - JSON string with tool input (contains "command")
|
|
22
|
-
* CLAUDE_PROJECT_DIR - Project root directory
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const fs = require('fs');
|
|
26
|
-
const path = require('path');
|
|
27
|
-
|
|
28
|
-
// ANSI colors for output
|
|
29
|
-
const c = {
|
|
30
|
-
reset: '\x1b[0m',
|
|
31
|
-
bold: '\x1b[1m',
|
|
32
|
-
red: '\x1b[31m',
|
|
33
|
-
yellow: '\x1b[33m',
|
|
34
|
-
cyan: '\x1b[36m',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Exit codes
|
|
38
|
-
const EXIT_ALLOW = 0;
|
|
39
|
-
const EXIT_BLOCK = 2;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Load patterns from YAML file
|
|
43
|
-
* Falls back to built-in patterns if YAML parsing fails
|
|
44
|
-
*/
|
|
45
|
-
function loadPatterns(projectDir) {
|
|
46
|
-
const locations = [
|
|
47
|
-
path.join(projectDir, '.claude/hooks/damage-control/patterns.yaml'),
|
|
48
|
-
path.join(projectDir, '.agileflow/hooks/damage-control/patterns.yaml'),
|
|
49
|
-
path.join(projectDir, 'patterns.yaml'),
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
for (const loc of locations) {
|
|
53
|
-
if (fs.existsSync(loc)) {
|
|
54
|
-
try {
|
|
55
|
-
const content = fs.readFileSync(loc, 'utf8');
|
|
56
|
-
// Simple YAML parsing for our specific structure
|
|
57
|
-
return parseSimpleYaml(content);
|
|
58
|
-
} catch (e) {
|
|
59
|
-
console.error(`Warning: Could not parse ${loc}: ${e.message}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Return built-in defaults if no file found
|
|
65
|
-
return getDefaultPatterns();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Simple YAML parser for patterns.yaml structure
|
|
70
|
-
* Only handles the specific structure we use (arrays of objects with pattern/reason/ask)
|
|
71
|
-
*/
|
|
72
|
-
function parseSimpleYaml(content) {
|
|
73
|
-
const patterns = {
|
|
74
|
-
bashToolPatterns: [],
|
|
75
|
-
askPatterns: [],
|
|
76
|
-
agileflowPatterns: [],
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
let currentSection = null;
|
|
80
|
-
let currentItem = null;
|
|
81
|
-
|
|
82
|
-
const lines = content.split('\n');
|
|
83
|
-
|
|
84
|
-
for (const line of lines) {
|
|
85
|
-
// Skip comments and empty lines
|
|
86
|
-
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
87
|
-
|
|
88
|
-
// Check for section headers
|
|
89
|
-
if (line.match(/^bashToolPatterns:/)) {
|
|
90
|
-
currentSection = 'bashToolPatterns';
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (line.match(/^askPatterns:/)) {
|
|
94
|
-
currentSection = 'askPatterns';
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (line.match(/^agileflowPatterns:/)) {
|
|
98
|
-
currentSection = 'agileflowPatterns';
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (line.match(/^(zeroAccessPaths|readOnlyPaths|noDeletePaths|config):/)) {
|
|
102
|
-
currentSection = null; // Skip non-pattern sections
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Parse pattern items
|
|
107
|
-
if (currentSection && patterns[currentSection]) {
|
|
108
|
-
const patternMatch = line.match(/^\s+-\s*pattern:\s*['"]?(.+?)['"]?\s*$/);
|
|
109
|
-
if (patternMatch) {
|
|
110
|
-
currentItem = { pattern: patternMatch[1] };
|
|
111
|
-
patterns[currentSection].push(currentItem);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const reasonMatch = line.match(/^\s+reason:\s*['"]?(.+?)['"]?\s*$/);
|
|
116
|
-
if (reasonMatch && currentItem) {
|
|
117
|
-
currentItem.reason = reasonMatch[1];
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const askMatch = line.match(/^\s+ask:\s*(true|false)\s*$/);
|
|
122
|
-
if (askMatch && currentItem) {
|
|
123
|
-
currentItem.ask = askMatch[1] === 'true';
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return patterns;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Built-in default patterns (used if patterns.yaml not found)
|
|
134
|
-
*/
|
|
135
|
-
function getDefaultPatterns() {
|
|
136
|
-
return {
|
|
137
|
-
bashToolPatterns: [
|
|
138
|
-
{ pattern: '\\brm\\s+-[rRf]', reason: 'rm with recursive or force flags' },
|
|
139
|
-
{ pattern: 'DROP\\s+(TABLE|DATABASE)', reason: 'DROP commands are destructive' },
|
|
140
|
-
{ pattern: 'DELETE\\s+FROM\\s+\\w+\\s*;', reason: 'DELETE without WHERE clause' },
|
|
141
|
-
{ pattern: 'TRUNCATE\\s+(TABLE\\s+)?\\w+', reason: 'TRUNCATE removes all data' },
|
|
142
|
-
{
|
|
143
|
-
pattern: 'git\\s+push\\s+.*--force',
|
|
144
|
-
reason: 'Force push can overwrite history',
|
|
145
|
-
ask: true,
|
|
146
|
-
},
|
|
147
|
-
{ pattern: 'git\\s+reset\\s+--hard', reason: 'Hard reset discards changes', ask: true },
|
|
148
|
-
],
|
|
149
|
-
askPatterns: [
|
|
150
|
-
{ pattern: 'DELETE\\s+FROM\\s+\\w+\\s+WHERE', reason: 'Confirm record deletion' },
|
|
151
|
-
{ pattern: 'npm\\s+publish', reason: 'Publishing to npm is permanent' },
|
|
152
|
-
],
|
|
153
|
-
agileflowPatterns: [
|
|
154
|
-
{ pattern: 'rm.*\\.agileflow', reason: 'Deleting .agileflow breaks installation' },
|
|
155
|
-
{ pattern: 'rm.*\\.claude', reason: 'Deleting .claude breaks configuration' },
|
|
156
|
-
],
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check command against patterns
|
|
162
|
-
* Returns: { blocked: boolean, ask: boolean, reason: string }
|
|
163
|
-
*/
|
|
164
|
-
function checkCommand(command, patterns) {
|
|
165
|
-
// Combine all pattern sources
|
|
166
|
-
const allPatterns = [...(patterns.bashToolPatterns || []), ...(patterns.agileflowPatterns || [])];
|
|
167
|
-
|
|
168
|
-
// Check block/ask patterns
|
|
169
|
-
for (const p of allPatterns) {
|
|
170
|
-
try {
|
|
171
|
-
const regex = new RegExp(p.pattern, 'i');
|
|
172
|
-
if (regex.test(command)) {
|
|
173
|
-
if (p.ask) {
|
|
174
|
-
return { blocked: false, ask: true, reason: p.reason };
|
|
175
|
-
}
|
|
176
|
-
return { blocked: true, ask: false, reason: p.reason };
|
|
177
|
-
}
|
|
178
|
-
} catch (e) {
|
|
179
|
-
// Invalid regex, skip
|
|
180
|
-
console.error(`Warning: Invalid regex pattern: ${p.pattern}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Check ask-only patterns
|
|
185
|
-
for (const p of patterns.askPatterns || []) {
|
|
186
|
-
try {
|
|
187
|
-
const regex = new RegExp(p.pattern, 'i');
|
|
188
|
-
if (regex.test(command)) {
|
|
189
|
-
return { blocked: false, ask: true, reason: p.reason };
|
|
190
|
-
}
|
|
191
|
-
} catch (e) {
|
|
192
|
-
// Invalid regex, skip
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { blocked: false, ask: false, reason: null };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Main entry point
|
|
16
|
+
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
201
17
|
*/
|
|
202
|
-
function main() {
|
|
203
|
-
// Get tool input from environment
|
|
204
|
-
const toolInput = process.env.CLAUDE_TOOL_INPUT;
|
|
205
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
206
|
-
|
|
207
|
-
if (!toolInput) {
|
|
208
|
-
// No input, allow by default
|
|
209
|
-
process.exit(EXIT_ALLOW);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
let input;
|
|
213
|
-
try {
|
|
214
|
-
input = JSON.parse(toolInput);
|
|
215
|
-
} catch (e) {
|
|
216
|
-
console.error('Error parsing CLAUDE_TOOL_INPUT:', e.message);
|
|
217
|
-
process.exit(EXIT_ALLOW);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const command = input.command;
|
|
221
|
-
if (!command) {
|
|
222
|
-
process.exit(EXIT_ALLOW);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Load patterns
|
|
226
|
-
const patterns = loadPatterns(projectDir);
|
|
227
|
-
|
|
228
|
-
// Check command
|
|
229
|
-
const result = checkCommand(command, patterns);
|
|
230
|
-
|
|
231
|
-
if (result.blocked) {
|
|
232
|
-
// Block the command
|
|
233
|
-
console.error(`${c.red}${c.bold}BLOCKED${c.reset}: ${result.reason}`);
|
|
234
|
-
console.error(`${c.yellow}Command: ${command}${c.reset}`);
|
|
235
|
-
console.error(`${c.cyan}This command was blocked by damage control.${c.reset}`);
|
|
236
|
-
process.exit(EXIT_BLOCK);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (result.ask) {
|
|
240
|
-
// Ask for confirmation
|
|
241
|
-
const response = {
|
|
242
|
-
result: 'ask',
|
|
243
|
-
message: `${result.reason}\n\nCommand: ${command}\n\nProceed with this command?`,
|
|
244
|
-
};
|
|
245
|
-
console.log(JSON.stringify(response));
|
|
246
|
-
process.exit(EXIT_ALLOW);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Allow the command
|
|
250
|
-
process.exit(EXIT_ALLOW);
|
|
251
|
-
}
|
|
252
18
|
|
|
253
|
-
|
|
254
|
-
if (require.main === module) {
|
|
255
|
-
main();
|
|
256
|
-
}
|
|
19
|
+
const { createBashHook } = require('../lib/damage-control-utils');
|
|
257
20
|
|
|
258
|
-
|
|
21
|
+
// Run the hook using factory
|
|
22
|
+
createBashHook()();
|
|
@@ -1,259 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* edit-tool-damage-control.js -
|
|
4
|
+
* edit-tool-damage-control.js - PreToolUse hook for Edit tool
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* to protected paths.
|
|
9
|
-
*
|
|
10
|
-
* Path protection levels:
|
|
11
|
-
* zeroAccessPaths: Cannot read, write, edit, or delete
|
|
12
|
-
* readOnlyPaths: Can read, cannot modify or delete
|
|
13
|
-
* noDeletePaths: Can read and modify, cannot delete (Edit is allowed)
|
|
6
|
+
* Validates file paths against access control patterns in damage-control-patterns.yaml
|
|
7
|
+
* before allowing file edits. Part of AgileFlow's damage control system.
|
|
14
8
|
*
|
|
15
9
|
* Exit codes:
|
|
16
|
-
* 0
|
|
17
|
-
* 2
|
|
18
|
-
*
|
|
19
|
-
* Usage (as PreToolUse hook):
|
|
20
|
-
* node .claude/hooks/damage-control/edit-tool-damage-control.js
|
|
10
|
+
* 0 - Allow operation
|
|
11
|
+
* 2 - Block operation
|
|
21
12
|
*
|
|
22
|
-
*
|
|
23
|
-
* CLAUDE_TOOL_INPUT - JSON string with tool input (contains "file_path")
|
|
24
|
-
* CLAUDE_PROJECT_DIR - Project root directory
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const fs = require('fs');
|
|
28
|
-
const path = require('path');
|
|
29
|
-
|
|
30
|
-
// ANSI colors for output
|
|
31
|
-
const c = {
|
|
32
|
-
reset: '\x1b[0m',
|
|
33
|
-
bold: '\x1b[1m',
|
|
34
|
-
red: '\x1b[31m',
|
|
35
|
-
yellow: '\x1b[33m',
|
|
36
|
-
cyan: '\x1b[36m',
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Exit codes
|
|
40
|
-
const EXIT_ALLOW = 0;
|
|
41
|
-
const EXIT_BLOCK = 2;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Load path protection rules from patterns.yaml
|
|
45
|
-
*/
|
|
46
|
-
function loadPathRules(projectDir) {
|
|
47
|
-
const locations = [
|
|
48
|
-
path.join(projectDir, '.claude/hooks/damage-control/patterns.yaml'),
|
|
49
|
-
path.join(projectDir, '.agileflow/hooks/damage-control/patterns.yaml'),
|
|
50
|
-
path.join(projectDir, 'patterns.yaml'),
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
for (const loc of locations) {
|
|
54
|
-
if (fs.existsSync(loc)) {
|
|
55
|
-
try {
|
|
56
|
-
const content = fs.readFileSync(loc, 'utf8');
|
|
57
|
-
return parsePathRules(content);
|
|
58
|
-
} catch (e) {
|
|
59
|
-
console.error(`Warning: Could not parse ${loc}: ${e.message}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return getDefaultPathRules();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Parse path rules from YAML content
|
|
13
|
+
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
69
14
|
*/
|
|
70
|
-
function parsePathRules(content) {
|
|
71
|
-
const rules = {
|
|
72
|
-
zeroAccessPaths: [],
|
|
73
|
-
readOnlyPaths: [],
|
|
74
|
-
noDeletePaths: [],
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
let currentSection = null;
|
|
78
|
-
|
|
79
|
-
const lines = content.split('\n');
|
|
80
|
-
|
|
81
|
-
for (const line of lines) {
|
|
82
|
-
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
83
|
-
|
|
84
|
-
if (line.match(/^zeroAccessPaths:/)) {
|
|
85
|
-
currentSection = 'zeroAccessPaths';
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (line.match(/^readOnlyPaths:/)) {
|
|
89
|
-
currentSection = 'readOnlyPaths';
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (line.match(/^noDeletePaths:/)) {
|
|
93
|
-
currentSection = 'noDeletePaths';
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (line.match(/^(bashToolPatterns|askPatterns|agileflowPatterns|config):/)) {
|
|
97
|
-
currentSection = null;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (currentSection && rules[currentSection]) {
|
|
102
|
-
const pathMatch = line.match(/^\s+-\s*['"]?(.+?)['"]?\s*$/);
|
|
103
|
-
if (pathMatch) {
|
|
104
|
-
rules[currentSection].push(pathMatch[1]);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return rules;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Default path rules if patterns.yaml not found
|
|
114
|
-
*/
|
|
115
|
-
function getDefaultPathRules() {
|
|
116
|
-
return {
|
|
117
|
-
zeroAccessPaths: ['~/.ssh/', '~/.aws/credentials', '.env', '.env.local', '.env.production'],
|
|
118
|
-
readOnlyPaths: ['/etc/', '~/.bashrc', '~/.zshrc', 'package-lock.json', 'yarn.lock', '.git/'],
|
|
119
|
-
noDeletePaths: ['.agileflow/', '.claude/', 'docs/09-agents/status.json', 'CLAUDE.md'],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Expand home directory in path
|
|
125
|
-
*/
|
|
126
|
-
function expandHome(filePath) {
|
|
127
|
-
if (filePath.startsWith('~/')) {
|
|
128
|
-
return path.join(process.env.HOME || '', filePath.slice(2));
|
|
129
|
-
}
|
|
130
|
-
return filePath;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Check if a file path matches a pattern
|
|
135
|
-
* Supports:
|
|
136
|
-
* - Exact paths
|
|
137
|
-
* - Directory prefixes (ending with /)
|
|
138
|
-
* - Glob wildcards (**)
|
|
139
|
-
*/
|
|
140
|
-
function pathMatches(filePath, pattern) {
|
|
141
|
-
const expandedPattern = expandHome(pattern);
|
|
142
|
-
const normalizedFile = path.normalize(filePath);
|
|
143
|
-
const normalizedPattern = path.normalize(expandedPattern);
|
|
144
|
-
|
|
145
|
-
// Exact match
|
|
146
|
-
if (normalizedFile === normalizedPattern) return true;
|
|
147
|
-
|
|
148
|
-
// Directory prefix match (pattern ends with /)
|
|
149
|
-
if (pattern.endsWith('/')) {
|
|
150
|
-
if (normalizedFile.startsWith(normalizedPattern)) return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Glob pattern (**/)
|
|
154
|
-
if (pattern.includes('**/')) {
|
|
155
|
-
const globPart = pattern.split('**/')[1];
|
|
156
|
-
if (normalizedFile.includes(globPart)) return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Simple wildcard at end
|
|
160
|
-
if (pattern.endsWith('*')) {
|
|
161
|
-
const prefix = normalizedPattern.slice(0, -1);
|
|
162
|
-
if (normalizedFile.startsWith(prefix)) return true;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Basename match (for patterns like .env)
|
|
166
|
-
if (!pattern.includes('/') && !pattern.includes(path.sep)) {
|
|
167
|
-
const basename = path.basename(normalizedFile);
|
|
168
|
-
if (basename === pattern) return true;
|
|
169
|
-
// Pattern like .env* matching .env.local
|
|
170
|
-
if (pattern.endsWith('*')) {
|
|
171
|
-
const patternBase = pattern.slice(0, -1);
|
|
172
|
-
if (basename.startsWith(patternBase)) return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Check if file path is protected
|
|
181
|
-
* Returns: { blocked: boolean, reason: string, level: string }
|
|
182
|
-
*/
|
|
183
|
-
function checkPath(filePath, rules) {
|
|
184
|
-
// Check zero access paths (blocked completely)
|
|
185
|
-
for (const pattern of rules.zeroAccessPaths) {
|
|
186
|
-
if (pathMatches(filePath, pattern)) {
|
|
187
|
-
return {
|
|
188
|
-
blocked: true,
|
|
189
|
-
reason: `Path is in zero-access protected list: ${pattern}`,
|
|
190
|
-
level: 'zero-access',
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Check read-only paths (cannot edit)
|
|
196
|
-
for (const pattern of rules.readOnlyPaths) {
|
|
197
|
-
if (pathMatches(filePath, pattern)) {
|
|
198
|
-
return {
|
|
199
|
-
blocked: true,
|
|
200
|
-
reason: `Path is read-only: ${pattern}`,
|
|
201
|
-
level: 'read-only',
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// noDeletePaths allows editing, so we don't block here
|
|
207
|
-
// (deletion is handled by a different mechanism or file watcher)
|
|
208
|
-
|
|
209
|
-
return { blocked: false, reason: null, level: null };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Main entry point
|
|
214
|
-
*/
|
|
215
|
-
function main() {
|
|
216
|
-
const toolInput = process.env.CLAUDE_TOOL_INPUT;
|
|
217
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
218
|
-
|
|
219
|
-
if (!toolInput) {
|
|
220
|
-
process.exit(EXIT_ALLOW);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
let input;
|
|
224
|
-
try {
|
|
225
|
-
input = JSON.parse(toolInput);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
console.error('Error parsing CLAUDE_TOOL_INPUT:', e.message);
|
|
228
|
-
process.exit(EXIT_ALLOW);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const filePath = input.file_path;
|
|
232
|
-
if (!filePath) {
|
|
233
|
-
process.exit(EXIT_ALLOW);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Resolve to absolute path
|
|
237
|
-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
238
|
-
|
|
239
|
-
// Load rules
|
|
240
|
-
const rules = loadPathRules(projectDir);
|
|
241
|
-
|
|
242
|
-
// Check path
|
|
243
|
-
const result = checkPath(absolutePath, rules);
|
|
244
|
-
|
|
245
|
-
if (result.blocked) {
|
|
246
|
-
console.error(`${c.red}${c.bold}BLOCKED${c.reset}: ${result.reason}`);
|
|
247
|
-
console.error(`${c.yellow}File: ${filePath}${c.reset}`);
|
|
248
|
-
console.error(`${c.cyan}This file is protected by damage control (${result.level}).${c.reset}`);
|
|
249
|
-
process.exit(EXIT_BLOCK);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
process.exit(EXIT_ALLOW);
|
|
253
|
-
}
|
|
254
15
|
|
|
255
|
-
|
|
256
|
-
main();
|
|
257
|
-
}
|
|
16
|
+
const { createPathHook } = require('../lib/damage-control-utils');
|
|
258
17
|
|
|
259
|
-
|
|
18
|
+
// Run the hook using factory
|
|
19
|
+
createPathHook('edit')();
|