agileflow 2.78.0 → 2.80.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/agileflow-configure.js +126 -17
- package/scripts/agileflow-welcome.js +77 -98
- package/scripts/auto-self-improve.js +23 -45
- package/scripts/check-update.js +35 -42
- package/scripts/damage-control/bash-tool-damage-control.js +258 -0
- package/scripts/damage-control/edit-tool-damage-control.js +259 -0
- package/scripts/damage-control/patterns.yaml +227 -0
- package/scripts/damage-control/write-tool-damage-control.js +254 -0
- package/scripts/damage-control-bash.js +28 -22
- package/scripts/damage-control-edit.js +6 -12
- package/scripts/damage-control-write.js +6 -12
- package/scripts/get-env.js +6 -6
- package/scripts/obtain-context.js +67 -37
- package/scripts/ralph-loop.js +199 -63
- package/scripts/screenshot-verifier.js +215 -0
- package/scripts/session-manager.js +12 -33
- package/src/core/agents/configuration-damage-control.md +248 -0
- package/src/core/commands/babysit.md +30 -2
- package/src/core/commands/configure.md +46 -9
- package/src/core/commands/setup/visual-e2e.md +462 -0
- package/src/core/experts/documentation/expertise.yaml +25 -0
- package/src/core/skills/_learnings/code-review.yaml +118 -0
- package/src/core/skills/_learnings/story-writer.yaml +71 -0
- package/tools/cli/commands/start.js +19 -21
- package/tools/cli/installers/ide/claude-code.js +140 -0
- package/tools/cli/tui/Dashboard.js +3 -4
- package/tools/postinstall.js +1 -9
package/scripts/check-update.js
CHANGED
|
@@ -24,6 +24,10 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const https = require('https');
|
|
26
26
|
|
|
27
|
+
// Shared utilities
|
|
28
|
+
const { getProjectRoot } = require('../lib/paths');
|
|
29
|
+
const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
|
|
30
|
+
|
|
27
31
|
// Debug mode
|
|
28
32
|
const DEBUG = process.env.DEBUG_UPDATE === '1';
|
|
29
33
|
|
|
@@ -33,35 +37,23 @@ function debugLog(message, data = null) {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
// Find project root (has .agileflow directory)
|
|
37
|
-
function getProjectRoot() {
|
|
38
|
-
let dir = process.cwd();
|
|
39
|
-
while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
|
|
40
|
-
dir = path.dirname(dir);
|
|
41
|
-
}
|
|
42
|
-
return dir !== '/' ? dir : process.cwd();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
// Get installed AgileFlow version
|
|
46
41
|
function getInstalledVersion(rootDir) {
|
|
47
42
|
// First check .agileflow/package.json (installed version)
|
|
48
43
|
const agileflowPkg = path.join(rootDir, '.agileflow', 'package.json');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
44
|
+
const agileflowResult = safeReadJSON(agileflowPkg);
|
|
45
|
+
if (agileflowResult.ok && agileflowResult.data?.version) {
|
|
46
|
+
return agileflowResult.data.version;
|
|
47
|
+
}
|
|
48
|
+
if (!agileflowResult.ok && agileflowResult.error) {
|
|
49
|
+
debugLog('Error reading .agileflow/package.json', agileflowResult.error);
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
// Fallback: check if this is the AgileFlow dev repo
|
|
59
53
|
const cliPkg = path.join(rootDir, 'packages/cli/package.json');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (pkg.name === 'agileflow' && pkg.version) return pkg.version;
|
|
64
|
-
} catch (e) {}
|
|
54
|
+
const cliResult = safeReadJSON(cliPkg);
|
|
55
|
+
if (cliResult.ok && cliResult.data?.name === 'agileflow' && cliResult.data?.version) {
|
|
56
|
+
return cliResult.data.version;
|
|
65
57
|
}
|
|
66
58
|
|
|
67
59
|
return null;
|
|
@@ -78,16 +70,16 @@ function getUpdateConfig(rootDir) {
|
|
|
78
70
|
latestVersion: null,
|
|
79
71
|
};
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
74
|
+
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
75
|
+
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
debugLog('Error reading update config', result.error);
|
|
78
|
+
return defaults;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.data?.updates) {
|
|
82
|
+
return { ...defaults, ...result.data.updates };
|
|
91
83
|
}
|
|
92
84
|
|
|
93
85
|
return defaults;
|
|
@@ -95,21 +87,22 @@ function getUpdateConfig(rootDir) {
|
|
|
95
87
|
|
|
96
88
|
// Save update configuration
|
|
97
89
|
function saveUpdateConfig(rootDir, config) {
|
|
98
|
-
|
|
99
|
-
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
100
|
-
let metadata = {};
|
|
90
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
101
91
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
// Read existing metadata
|
|
93
|
+
const readResult = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
94
|
+
const metadata = readResult.ok ? readResult.data : {};
|
|
95
|
+
|
|
96
|
+
// Update and write
|
|
97
|
+
metadata.updates = config;
|
|
98
|
+
const writeResult = safeWriteJSON(metadataPath, metadata, { createDir: true });
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return true;
|
|
109
|
-
} catch (e) {
|
|
110
|
-
debugLog('Error saving update config', e.message);
|
|
100
|
+
if (!writeResult.ok) {
|
|
101
|
+
debugLog('Error saving update config', writeResult.error);
|
|
111
102
|
return false;
|
|
112
103
|
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
// Check if cache is still valid
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bash-tool-damage-control.js - Validate bash commands against security patterns
|
|
5
|
+
*
|
|
6
|
+
* This PreToolUse hook runs before every Bash tool execution.
|
|
7
|
+
* It checks the command against patterns.yaml to block or ask for
|
|
8
|
+
* confirmation on dangerous commands.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = Allow command to proceed (or ask with JSON output)
|
|
12
|
+
* 2 = Block command
|
|
13
|
+
*
|
|
14
|
+
* For ask confirmation, outputs JSON:
|
|
15
|
+
* {"result": "ask", "message": "Reason for asking"}
|
|
16
|
+
*
|
|
17
|
+
* Usage (as PreToolUse hook):
|
|
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
|
|
201
|
+
*/
|
|
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
|
+
|
|
253
|
+
// Run if called directly
|
|
254
|
+
if (require.main === module) {
|
|
255
|
+
main();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { checkCommand, loadPatterns, parseSimpleYaml };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* edit-tool-damage-control.js - Enforce path protection for Edit tool
|
|
5
|
+
*
|
|
6
|
+
* This PreToolUse hook runs before every Edit tool execution.
|
|
7
|
+
* It checks the file path against patterns.yaml to block edits
|
|
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)
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 = Allow edit to proceed
|
|
17
|
+
* 2 = Block edit
|
|
18
|
+
*
|
|
19
|
+
* Usage (as PreToolUse hook):
|
|
20
|
+
* node .claude/hooks/damage-control/edit-tool-damage-control.js
|
|
21
|
+
*
|
|
22
|
+
* Environment:
|
|
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
|
|
69
|
+
*/
|
|
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
|
+
|
|
255
|
+
if (require.main === module) {
|
|
256
|
+
main();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { checkPath, loadPathRules, pathMatches };
|