agileflow 2.79.0 → 2.81.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/README.md +6 -6
- package/package.json +1 -1
- package/scripts/agent-loop.js +765 -0
- package/scripts/agileflow-configure.js +129 -18
- package/scripts/agileflow-welcome.js +113 -16
- package/scripts/damage-control/bash-tool-damage-control.js +7 -6
- package/scripts/damage-control/edit-tool-damage-control.js +4 -24
- package/scripts/damage-control/patterns.yaml +32 -32
- package/scripts/damage-control/write-tool-damage-control.js +4 -24
- package/scripts/damage-control-bash.js +38 -125
- package/scripts/damage-control-edit.js +22 -165
- package/scripts/damage-control-write.js +22 -165
- package/scripts/get-env.js +6 -6
- package/scripts/lib/damage-control-utils.js +251 -0
- package/scripts/obtain-context.js +103 -37
- package/scripts/ralph-loop.js +243 -31
- package/scripts/screenshot-verifier.js +4 -2
- package/scripts/session-manager.js +434 -20
- package/src/core/agents/configuration-visual-e2e.md +300 -0
- package/src/core/agents/orchestrator.md +166 -0
- package/src/core/commands/babysit.md +61 -15
- package/src/core/commands/configure.md +408 -99
- package/src/core/commands/session/end.md +332 -103
- package/src/core/experts/documentation/expertise.yaml +25 -0
- package/tools/cli/commands/start.js +19 -21
- package/tools/cli/installers/ide/claude-code.js +32 -19
- package/tools/cli/tui/Dashboard.js +3 -4
- package/tools/postinstall.js +1 -9
- package/src/core/commands/setup/visual-e2e.md +0 -462
|
@@ -20,87 +20,87 @@
|
|
|
20
20
|
bashToolPatterns:
|
|
21
21
|
# Recursive/force deletion
|
|
22
22
|
- pattern: '\brm\s+-[rRf]'
|
|
23
|
-
reason:
|
|
23
|
+
reason: 'rm with recursive or force flags can destroy entire directories'
|
|
24
24
|
|
|
25
25
|
- pattern: '\brm\s+.*--no-preserve-root'
|
|
26
|
-
reason:
|
|
26
|
+
reason: 'rm with --no-preserve-root is catastrophically dangerous'
|
|
27
27
|
|
|
28
28
|
- pattern: '\brm\s+-rf\s+/'
|
|
29
|
-
reason:
|
|
29
|
+
reason: 'rm -rf on root directory would destroy the entire system'
|
|
30
30
|
|
|
31
31
|
# SQL destructive commands without WHERE clause
|
|
32
32
|
- pattern: 'DELETE\s+FROM\s+\w+\s*;'
|
|
33
|
-
reason:
|
|
33
|
+
reason: 'DELETE without WHERE clause would delete all records'
|
|
34
34
|
|
|
35
35
|
- pattern: 'TRUNCATE\s+(TABLE\s+)?\w+'
|
|
36
|
-
reason:
|
|
36
|
+
reason: 'TRUNCATE removes all data from table'
|
|
37
37
|
|
|
38
38
|
- pattern: 'DROP\s+(TABLE|DATABASE|SCHEMA|INDEX)'
|
|
39
|
-
reason:
|
|
39
|
+
reason: 'DROP commands permanently destroy database objects'
|
|
40
40
|
|
|
41
41
|
# Git force operations
|
|
42
42
|
- pattern: 'git\s+push\s+.*--force'
|
|
43
|
-
reason:
|
|
43
|
+
reason: 'Force push can overwrite remote history'
|
|
44
44
|
ask: true
|
|
45
45
|
|
|
46
46
|
- pattern: 'git\s+push\s+.*-f\b'
|
|
47
|
-
reason:
|
|
47
|
+
reason: 'Force push can overwrite remote history'
|
|
48
48
|
ask: true
|
|
49
49
|
|
|
50
50
|
- pattern: 'git\s+reset\s+--hard'
|
|
51
|
-
reason:
|
|
51
|
+
reason: 'Hard reset discards uncommitted changes'
|
|
52
52
|
ask: true
|
|
53
53
|
|
|
54
54
|
# Format/wipe operations
|
|
55
55
|
- pattern: '\bmkfs\b'
|
|
56
|
-
reason:
|
|
56
|
+
reason: 'mkfs formats filesystems, destroying all data'
|
|
57
57
|
|
|
58
58
|
- pattern: '\bdd\s+.*of=/dev/'
|
|
59
|
-
reason:
|
|
59
|
+
reason: 'dd writing to device can destroy disk data'
|
|
60
60
|
|
|
61
61
|
- pattern: '\bshred\b'
|
|
62
|
-
reason:
|
|
62
|
+
reason: 'shred permanently destroys file data'
|
|
63
63
|
|
|
64
64
|
# Credential/secret exposure
|
|
65
65
|
- pattern: 'cat\s+.*\.env'
|
|
66
|
-
reason:
|
|
66
|
+
reason: 'Displaying .env may expose secrets'
|
|
67
67
|
ask: true
|
|
68
68
|
|
|
69
69
|
- pattern: 'cat\s+.*/\.ssh/'
|
|
70
|
-
reason:
|
|
70
|
+
reason: 'Displaying SSH keys is a security risk'
|
|
71
71
|
|
|
72
72
|
- pattern: 'cat\s+.*/credentials'
|
|
73
|
-
reason:
|
|
73
|
+
reason: 'Displaying credentials files is a security risk'
|
|
74
74
|
|
|
75
75
|
# Cloud CLI destructive operations
|
|
76
76
|
- pattern: 'aws\s+s3\s+rm\s+--recursive'
|
|
77
|
-
reason:
|
|
77
|
+
reason: 'Recursive S3 delete can destroy entire buckets'
|
|
78
78
|
ask: true
|
|
79
79
|
|
|
80
80
|
- pattern: 'aws\s+ec2\s+terminate-instances'
|
|
81
|
-
reason:
|
|
81
|
+
reason: 'Terminating EC2 instances is irreversible'
|
|
82
82
|
ask: true
|
|
83
83
|
|
|
84
84
|
- pattern: 'gcloud\s+.*delete'
|
|
85
|
-
reason:
|
|
85
|
+
reason: 'GCloud delete operations may be destructive'
|
|
86
86
|
ask: true
|
|
87
87
|
|
|
88
88
|
# Docker cleanup commands
|
|
89
89
|
- pattern: 'docker\s+system\s+prune\s+-a'
|
|
90
|
-
reason:
|
|
90
|
+
reason: 'Docker prune -a removes all unused images'
|
|
91
91
|
ask: true
|
|
92
92
|
|
|
93
93
|
- pattern: 'docker\s+volume\s+rm'
|
|
94
|
-
reason:
|
|
94
|
+
reason: 'Docker volume removal may delete persistent data'
|
|
95
95
|
ask: true
|
|
96
96
|
|
|
97
97
|
# npm/package manager dangerous commands
|
|
98
98
|
- pattern: 'npm\s+unpublish'
|
|
99
|
-
reason:
|
|
99
|
+
reason: 'npm unpublish can break dependent packages'
|
|
100
100
|
ask: true
|
|
101
101
|
|
|
102
102
|
- pattern: 'npm\s+deprecate'
|
|
103
|
-
reason:
|
|
103
|
+
reason: 'npm deprecate affects package visibility'
|
|
104
104
|
ask: true
|
|
105
105
|
|
|
106
106
|
# ============================================================================
|
|
@@ -110,19 +110,19 @@ bashToolPatterns:
|
|
|
110
110
|
|
|
111
111
|
askPatterns:
|
|
112
112
|
- pattern: 'DELETE\s+FROM\s+\w+\s+WHERE'
|
|
113
|
-
reason:
|
|
113
|
+
reason: 'Deleting specific records - confirm data is correct'
|
|
114
114
|
|
|
115
115
|
- pattern: 'UPDATE\s+\w+\s+SET'
|
|
116
|
-
reason:
|
|
116
|
+
reason: 'Updating records - confirm scope is correct'
|
|
117
117
|
|
|
118
118
|
- pattern: 'npm\s+publish'
|
|
119
|
-
reason:
|
|
119
|
+
reason: 'Publishing to npm is permanent'
|
|
120
120
|
|
|
121
121
|
- pattern: 'git\s+tag\s+-d'
|
|
122
|
-
reason:
|
|
122
|
+
reason: 'Deleting git tags'
|
|
123
123
|
|
|
124
124
|
- pattern: 'kubectl\s+delete'
|
|
125
|
-
reason:
|
|
125
|
+
reason: 'Kubernetes delete operations'
|
|
126
126
|
|
|
127
127
|
# ============================================================================
|
|
128
128
|
# PATH PROTECTION
|
|
@@ -193,17 +193,17 @@ noDeletePaths:
|
|
|
193
193
|
agileflowPatterns:
|
|
194
194
|
# Protect AgileFlow infrastructure
|
|
195
195
|
- pattern: 'rm.*\.agileflow'
|
|
196
|
-
reason:
|
|
196
|
+
reason: 'Deleting .agileflow would break AgileFlow installation'
|
|
197
197
|
|
|
198
198
|
- pattern: 'rm.*\.claude'
|
|
199
|
-
reason:
|
|
199
|
+
reason: 'Deleting .claude would break Claude Code configuration'
|
|
200
200
|
|
|
201
201
|
- pattern: 'rm.*status\.json'
|
|
202
|
-
reason:
|
|
202
|
+
reason: 'Deleting status.json would lose story tracking data'
|
|
203
203
|
|
|
204
204
|
# Dangerous npm operations in AgileFlow context
|
|
205
205
|
- pattern: 'npm\s+uninstall\s+agileflow'
|
|
206
|
-
reason:
|
|
206
|
+
reason: 'Uninstalling AgileFlow - confirm this is intentional'
|
|
207
207
|
ask: true
|
|
208
208
|
|
|
209
209
|
# ============================================================================
|
|
@@ -224,4 +224,4 @@ config:
|
|
|
224
224
|
|
|
225
225
|
# Enable/disable prompt hooks (AI-based evaluation)
|
|
226
226
|
promptHooksEnabled: false
|
|
227
|
-
promptHookMessage:
|
|
227
|
+
promptHookMessage: 'Evaluate if this command could cause irreversible damage. Block if dangerous.'
|
|
@@ -114,27 +114,9 @@ function parsePathRules(content) {
|
|
|
114
114
|
*/
|
|
115
115
|
function getDefaultPathRules() {
|
|
116
116
|
return {
|
|
117
|
-
zeroAccessPaths: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'.env',
|
|
121
|
-
'.env.local',
|
|
122
|
-
'.env.production',
|
|
123
|
-
],
|
|
124
|
-
readOnlyPaths: [
|
|
125
|
-
'/etc/',
|
|
126
|
-
'~/.bashrc',
|
|
127
|
-
'~/.zshrc',
|
|
128
|
-
'package-lock.json',
|
|
129
|
-
'yarn.lock',
|
|
130
|
-
'.git/',
|
|
131
|
-
],
|
|
132
|
-
noDeletePaths: [
|
|
133
|
-
'.agileflow/',
|
|
134
|
-
'.claude/',
|
|
135
|
-
'docs/09-agents/status.json',
|
|
136
|
-
'CLAUDE.md',
|
|
137
|
-
],
|
|
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'],
|
|
138
120
|
};
|
|
139
121
|
}
|
|
140
122
|
|
|
@@ -247,9 +229,7 @@ function main() {
|
|
|
247
229
|
}
|
|
248
230
|
|
|
249
231
|
// Resolve to absolute path
|
|
250
|
-
const absolutePath = path.isAbsolute(filePath)
|
|
251
|
-
? filePath
|
|
252
|
-
: path.join(projectDir, filePath);
|
|
232
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
253
233
|
|
|
254
234
|
// Load rules
|
|
255
235
|
const rules = loadPathRules(projectDir);
|
|
@@ -15,30 +15,13 @@
|
|
|
15
15
|
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
reset: '\x1b[0m',
|
|
26
|
-
dim: '\x1b[2m'
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Find project root by looking for .agileflow directory
|
|
31
|
-
*/
|
|
32
|
-
function findProjectRoot() {
|
|
33
|
-
let dir = process.cwd();
|
|
34
|
-
while (dir !== '/') {
|
|
35
|
-
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
36
|
-
return dir;
|
|
37
|
-
}
|
|
38
|
-
dir = path.dirname(dir);
|
|
39
|
-
}
|
|
40
|
-
return process.cwd();
|
|
41
|
-
}
|
|
18
|
+
const {
|
|
19
|
+
findProjectRoot,
|
|
20
|
+
loadPatterns,
|
|
21
|
+
outputBlocked,
|
|
22
|
+
runDamageControlHook,
|
|
23
|
+
c,
|
|
24
|
+
} = require('./lib/damage-control-utils');
|
|
42
25
|
|
|
43
26
|
/**
|
|
44
27
|
* Parse simplified YAML for damage control patterns
|
|
@@ -48,7 +31,7 @@ function parseSimpleYAML(content) {
|
|
|
48
31
|
const config = {
|
|
49
32
|
bashToolPatterns: [],
|
|
50
33
|
askPatterns: [],
|
|
51
|
-
agileflowProtections: []
|
|
34
|
+
agileflowProtections: [],
|
|
52
35
|
};
|
|
53
36
|
|
|
54
37
|
let currentSection = null;
|
|
@@ -76,44 +59,28 @@ function parseSimpleYAML(content) {
|
|
|
76
59
|
currentPattern = null;
|
|
77
60
|
} else if (trimmed.startsWith('- pattern:') && currentSection) {
|
|
78
61
|
// New pattern entry
|
|
79
|
-
const patternValue = trimmed
|
|
62
|
+
const patternValue = trimmed
|
|
63
|
+
.replace('- pattern:', '')
|
|
64
|
+
.trim()
|
|
65
|
+
.replace(/^["']|["']$/g, '');
|
|
80
66
|
currentPattern = { pattern: patternValue };
|
|
81
67
|
config[currentSection].push(currentPattern);
|
|
82
68
|
} else if (trimmed.startsWith('reason:') && currentPattern) {
|
|
83
|
-
currentPattern.reason = trimmed
|
|
69
|
+
currentPattern.reason = trimmed
|
|
70
|
+
.replace('reason:', '')
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/^["']|["']$/g, '');
|
|
84
73
|
} else if (trimmed.startsWith('flags:') && currentPattern) {
|
|
85
|
-
currentPattern.flags = trimmed
|
|
74
|
+
currentPattern.flags = trimmed
|
|
75
|
+
.replace('flags:', '')
|
|
76
|
+
.trim()
|
|
77
|
+
.replace(/^["']|["']$/g, '');
|
|
86
78
|
}
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
return config;
|
|
90
82
|
}
|
|
91
83
|
|
|
92
|
-
/**
|
|
93
|
-
* Load patterns configuration from YAML file
|
|
94
|
-
*/
|
|
95
|
-
function loadPatterns(projectRoot) {
|
|
96
|
-
const configPaths = [
|
|
97
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
98
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
99
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
for (const configPath of configPaths) {
|
|
103
|
-
if (fs.existsSync(configPath)) {
|
|
104
|
-
try {
|
|
105
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
106
|
-
return parseSimpleYAML(content);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
// Continue to next path
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Return empty config if no file found (fail-open)
|
|
114
|
-
return { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
84
|
/**
|
|
118
85
|
* Test command against a single pattern rule
|
|
119
86
|
*/
|
|
@@ -135,14 +102,14 @@ function validateCommand(command, config) {
|
|
|
135
102
|
// Check blocked patterns (bashToolPatterns + agileflowProtections)
|
|
136
103
|
const blockedPatterns = [
|
|
137
104
|
...(config.bashToolPatterns || []),
|
|
138
|
-
...(config.agileflowProtections || [])
|
|
105
|
+
...(config.agileflowProtections || []),
|
|
139
106
|
];
|
|
140
107
|
|
|
141
108
|
for (const rule of blockedPatterns) {
|
|
142
109
|
if (matchesPattern(command, rule)) {
|
|
143
110
|
return {
|
|
144
111
|
action: 'block',
|
|
145
|
-
reason: rule.reason || 'Command blocked by damage control'
|
|
112
|
+
reason: rule.reason || 'Command blocked by damage control',
|
|
146
113
|
};
|
|
147
114
|
}
|
|
148
115
|
}
|
|
@@ -152,7 +119,7 @@ function validateCommand(command, config) {
|
|
|
152
119
|
if (matchesPattern(command, rule)) {
|
|
153
120
|
return {
|
|
154
121
|
action: 'ask',
|
|
155
|
-
reason: rule.reason || 'Please confirm this command'
|
|
122
|
+
reason: rule.reason || 'Please confirm this command',
|
|
156
123
|
};
|
|
157
124
|
}
|
|
158
125
|
}
|
|
@@ -161,72 +128,18 @@ function validateCommand(command, config) {
|
|
|
161
128
|
return { action: 'allow' };
|
|
162
129
|
}
|
|
163
130
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// Parse tool input from Claude Code
|
|
180
|
-
const input = JSON.parse(inputData);
|
|
181
|
-
const command = input.command || input.tool_input?.command || '';
|
|
182
|
-
|
|
183
|
-
if (!command) {
|
|
184
|
-
// No command to validate - allow
|
|
185
|
-
process.exit(0);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Load patterns and validate
|
|
189
|
-
const config = loadPatterns(projectRoot);
|
|
190
|
-
const result = validateCommand(command, config);
|
|
191
|
-
|
|
192
|
-
switch (result.action) {
|
|
193
|
-
case 'block':
|
|
194
|
-
// Output error message and block
|
|
195
|
-
console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
|
|
196
|
-
console.error(`${c.dim}Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}${c.reset}`);
|
|
197
|
-
process.exit(2);
|
|
198
|
-
break;
|
|
199
|
-
|
|
200
|
-
case 'ask':
|
|
201
|
-
// Output JSON to trigger user confirmation
|
|
202
|
-
console.log(JSON.stringify({
|
|
203
|
-
result: 'ask',
|
|
204
|
-
message: result.reason
|
|
205
|
-
}));
|
|
206
|
-
process.exit(0);
|
|
207
|
-
break;
|
|
208
|
-
|
|
209
|
-
case 'allow':
|
|
210
|
-
default:
|
|
211
|
-
// Allow command to proceed
|
|
212
|
-
process.exit(0);
|
|
213
|
-
}
|
|
214
|
-
} catch (e) {
|
|
215
|
-
// Parse error or other issue - fail open
|
|
216
|
-
// This ensures broken config doesn't block all commands
|
|
217
|
-
process.exit(0);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Handle no stdin (direct invocation)
|
|
222
|
-
process.stdin.on('error', () => {
|
|
223
|
-
process.exit(0);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Set timeout to prevent hanging
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
process.exit(0);
|
|
229
|
-
}, 4000);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
main();
|
|
131
|
+
// Run the hook
|
|
132
|
+
const projectRoot = findProjectRoot();
|
|
133
|
+
const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
|
|
134
|
+
|
|
135
|
+
runDamageControlHook({
|
|
136
|
+
getInputValue: input => input.command || input.tool_input?.command,
|
|
137
|
+
loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
|
|
138
|
+
validate: validateCommand,
|
|
139
|
+
onBlock: (result, command) => {
|
|
140
|
+
outputBlocked(
|
|
141
|
+
result.reason,
|
|
142
|
+
`Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -12,40 +12,13 @@
|
|
|
12
12
|
* Usage: Configured as PreToolUse hook in .claude/settings.json
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
reset: '\x1b[0m',
|
|
23
|
-
dim: '\x1b[2m'
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Find project root by looking for .agileflow directory
|
|
28
|
-
*/
|
|
29
|
-
function findProjectRoot() {
|
|
30
|
-
let dir = process.cwd();
|
|
31
|
-
while (dir !== '/') {
|
|
32
|
-
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
33
|
-
return dir;
|
|
34
|
-
}
|
|
35
|
-
dir = path.dirname(dir);
|
|
36
|
-
}
|
|
37
|
-
return process.cwd();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Expand ~ to home directory
|
|
42
|
-
*/
|
|
43
|
-
function expandPath(p) {
|
|
44
|
-
if (p.startsWith('~/')) {
|
|
45
|
-
return path.join(os.homedir(), p.slice(2));
|
|
46
|
-
}
|
|
47
|
-
return p;
|
|
48
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
findProjectRoot,
|
|
17
|
+
loadPatterns,
|
|
18
|
+
pathMatches,
|
|
19
|
+
outputBlocked,
|
|
20
|
+
runDamageControlHook,
|
|
21
|
+
} = require('./lib/damage-control-utils');
|
|
49
22
|
|
|
50
23
|
/**
|
|
51
24
|
* Parse simplified YAML for path patterns
|
|
@@ -54,7 +27,7 @@ function parseSimpleYAML(content) {
|
|
|
54
27
|
const config = {
|
|
55
28
|
zeroAccessPaths: [],
|
|
56
29
|
readOnlyPaths: [],
|
|
57
|
-
noDeletePaths: []
|
|
30
|
+
noDeletePaths: [],
|
|
58
31
|
};
|
|
59
32
|
|
|
60
33
|
let currentSection = null;
|
|
@@ -85,79 +58,6 @@ function parseSimpleYAML(content) {
|
|
|
85
58
|
return config;
|
|
86
59
|
}
|
|
87
60
|
|
|
88
|
-
/**
|
|
89
|
-
* Load patterns configuration from YAML file
|
|
90
|
-
*/
|
|
91
|
-
function loadPatterns(projectRoot) {
|
|
92
|
-
const configPaths = [
|
|
93
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
94
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
95
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
for (const configPath of configPaths) {
|
|
99
|
-
if (fs.existsSync(configPath)) {
|
|
100
|
-
try {
|
|
101
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
102
|
-
return parseSimpleYAML(content);
|
|
103
|
-
} catch (e) {
|
|
104
|
-
// Continue to next path
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Return empty config if no file found (fail-open)
|
|
110
|
-
return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Check if a file path matches any of the protected patterns
|
|
115
|
-
*/
|
|
116
|
-
function pathMatches(filePath, patterns) {
|
|
117
|
-
if (!filePath) return null;
|
|
118
|
-
|
|
119
|
-
const normalizedPath = path.resolve(filePath);
|
|
120
|
-
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
121
|
-
|
|
122
|
-
for (const pattern of patterns) {
|
|
123
|
-
const expandedPattern = expandPath(pattern);
|
|
124
|
-
|
|
125
|
-
// Check if pattern is a directory prefix
|
|
126
|
-
if (pattern.endsWith('/')) {
|
|
127
|
-
const patternDir = expandedPattern.slice(0, -1);
|
|
128
|
-
if (normalizedPath.startsWith(patternDir)) {
|
|
129
|
-
return pattern;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check exact match
|
|
134
|
-
if (normalizedPath === expandedPattern) {
|
|
135
|
-
return pattern;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
139
|
-
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
140
|
-
return pattern;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Check if pattern appears in path (for patterns like "*.pem")
|
|
144
|
-
if (pattern.startsWith('*')) {
|
|
145
|
-
const ext = pattern.slice(1);
|
|
146
|
-
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
147
|
-
return pattern;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check if path contains pattern (for things like ".env.production")
|
|
152
|
-
const patternBase = path.basename(pattern);
|
|
153
|
-
if (path.basename(normalizedPath) === patternBase) {
|
|
154
|
-
return pattern;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
61
|
/**
|
|
162
62
|
* Validate file path for edit operation
|
|
163
63
|
*/
|
|
@@ -168,7 +68,7 @@ function validatePath(filePath, config) {
|
|
|
168
68
|
return {
|
|
169
69
|
action: 'block',
|
|
170
70
|
reason: `Zero-access path: ${zeroMatch}`,
|
|
171
|
-
detail: 'This file is protected and cannot be accessed'
|
|
71
|
+
detail: 'This file is protected and cannot be accessed',
|
|
172
72
|
};
|
|
173
73
|
}
|
|
174
74
|
|
|
@@ -178,7 +78,7 @@ function validatePath(filePath, config) {
|
|
|
178
78
|
return {
|
|
179
79
|
action: 'block',
|
|
180
80
|
reason: `Read-only path: ${readOnlyMatch}`,
|
|
181
|
-
detail: 'This file is read-only and cannot be edited'
|
|
81
|
+
detail: 'This file is read-only and cannot be edited',
|
|
182
82
|
};
|
|
183
83
|
}
|
|
184
84
|
|
|
@@ -186,58 +86,15 @@ function validatePath(filePath, config) {
|
|
|
186
86
|
return { action: 'allow' };
|
|
187
87
|
}
|
|
188
88
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
process.stdin.on('end', () => {
|
|
203
|
-
try {
|
|
204
|
-
// Parse tool input from Claude Code
|
|
205
|
-
const input = JSON.parse(inputData);
|
|
206
|
-
const filePath = input.file_path || input.tool_input?.file_path || '';
|
|
207
|
-
|
|
208
|
-
if (!filePath) {
|
|
209
|
-
// No path to validate - allow
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Load patterns and validate
|
|
214
|
-
const config = loadPatterns(projectRoot);
|
|
215
|
-
const result = validatePath(filePath, config);
|
|
216
|
-
|
|
217
|
-
if (result.action === 'block') {
|
|
218
|
-
console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
|
|
219
|
-
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
220
|
-
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
221
|
-
process.exit(2);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Allow
|
|
225
|
-
process.exit(0);
|
|
226
|
-
} catch (e) {
|
|
227
|
-
// Parse error or other issue - fail open
|
|
228
|
-
process.exit(0);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Handle no stdin
|
|
233
|
-
process.stdin.on('error', () => {
|
|
234
|
-
process.exit(0);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Set timeout to prevent hanging
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
process.exit(0);
|
|
240
|
-
}, 4000);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
main();
|
|
89
|
+
// Run the hook
|
|
90
|
+
const projectRoot = findProjectRoot();
|
|
91
|
+
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
92
|
+
|
|
93
|
+
runDamageControlHook({
|
|
94
|
+
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
95
|
+
loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
|
|
96
|
+
validate: validatePath,
|
|
97
|
+
onBlock: (result, filePath) => {
|
|
98
|
+
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
99
|
+
},
|
|
100
|
+
});
|