agileflow 2.80.0 → 2.82.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 +3 -1
- package/scripts/agileflow-welcome.js +65 -0
- package/scripts/damage-control-bash.js +22 -115
- package/scripts/damage-control-edit.js +19 -156
- package/scripts/damage-control-write.js +19 -156
- package/scripts/lib/damage-control-utils.js +251 -0
- package/scripts/obtain-context.js +57 -2
- package/scripts/ralph-loop.js +516 -32
- package/scripts/session-manager.js +434 -20
- package/src/core/agents/configuration-visual-e2e.md +300 -0
- package/src/core/agents/orchestrator.md +301 -6
- package/src/core/commands/babysit.md +193 -15
- package/src/core/commands/batch.md +362 -0
- package/src/core/commands/choose.md +337 -0
- package/src/core/commands/configure.md +372 -100
- package/src/core/commands/session/end.md +332 -103
- package/src/core/commands/workflow.md +344 -0
- package/src/core/commands/setup/visual-e2e.md +0 -462
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const path = require('path');
|
|
31
31
|
const { execSync } = require('child_process');
|
|
32
|
+
const { isValidProfileName, isValidFeatureName, parseIntBounded } = require('../lib/validate');
|
|
32
33
|
|
|
33
34
|
// ============================================================================
|
|
34
35
|
// CONFIGURATION
|
|
@@ -1623,7 +1624,8 @@ function main() {
|
|
|
1623
1624
|
.split('=')[1]
|
|
1624
1625
|
.split(',')
|
|
1625
1626
|
.map(s => s.trim().toLowerCase());
|
|
1626
|
-
else if (arg.startsWith('--archival-days='))
|
|
1627
|
+
else if (arg.startsWith('--archival-days='))
|
|
1628
|
+
archivalDays = parseIntBounded(arg.split('=')[1], 30, 1, 365);
|
|
1627
1629
|
else if (arg === '--migrate') migrate = true;
|
|
1628
1630
|
else if (arg === '--detect' || arg === '--validate') detect = true;
|
|
1629
1631
|
else if (arg === '--upgrade') upgrade = true;
|
|
@@ -207,6 +207,12 @@ function checkParallelSessions(rootDir) {
|
|
|
207
207
|
otherActive: 0,
|
|
208
208
|
currentId: null,
|
|
209
209
|
cleaned: 0,
|
|
210
|
+
// Extended session info for non-main sessions
|
|
211
|
+
isMain: true,
|
|
212
|
+
nickname: null,
|
|
213
|
+
branch: null,
|
|
214
|
+
sessionPath: null,
|
|
215
|
+
mainPath: rootDir,
|
|
210
216
|
};
|
|
211
217
|
|
|
212
218
|
try {
|
|
@@ -247,6 +253,24 @@ function checkParallelSessions(rootDir) {
|
|
|
247
253
|
} catch (e) {
|
|
248
254
|
// Count failed
|
|
249
255
|
}
|
|
256
|
+
|
|
257
|
+
// Get detailed status for current session (for banner display)
|
|
258
|
+
try {
|
|
259
|
+
const statusOutput = execSync(`node "${scriptPath}" status`, {
|
|
260
|
+
cwd: rootDir,
|
|
261
|
+
encoding: 'utf8',
|
|
262
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
263
|
+
});
|
|
264
|
+
const statusData = JSON.parse(statusOutput);
|
|
265
|
+
if (statusData.current) {
|
|
266
|
+
result.isMain = statusData.current.is_main === true;
|
|
267
|
+
result.nickname = statusData.current.nickname;
|
|
268
|
+
result.branch = statusData.current.branch;
|
|
269
|
+
result.sessionPath = statusData.current.path;
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
// Status failed
|
|
273
|
+
}
|
|
250
274
|
} catch (e) {
|
|
251
275
|
// Session system not available
|
|
252
276
|
}
|
|
@@ -849,6 +873,41 @@ function formatTable(
|
|
|
849
873
|
return lines.join('\n');
|
|
850
874
|
}
|
|
851
875
|
|
|
876
|
+
// Format session banner for non-main sessions
|
|
877
|
+
function formatSessionBanner(parallelSessions) {
|
|
878
|
+
if (!parallelSessions.available || parallelSessions.isMain) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const W = 62; // banner width
|
|
883
|
+
const lines = [];
|
|
884
|
+
|
|
885
|
+
// Get display name
|
|
886
|
+
const sessionName = parallelSessions.nickname
|
|
887
|
+
? `SESSION ${parallelSessions.currentId} "${parallelSessions.nickname}"`
|
|
888
|
+
: `SESSION ${parallelSessions.currentId}`;
|
|
889
|
+
|
|
890
|
+
lines.push(`${c.dim}${box.tl}${box.h.repeat(W)}${box.tr}${c.reset}`);
|
|
891
|
+
lines.push(
|
|
892
|
+
`${c.dim}${box.v}${c.reset} ${c.teal}${c.bold}${pad(sessionName, W - 2)}${c.reset} ${c.dim}${box.v}${c.reset}`
|
|
893
|
+
);
|
|
894
|
+
lines.push(
|
|
895
|
+
`${c.dim}${box.v}${c.reset} ${c.slate}Branch:${c.reset} ${pad(parallelSessions.branch || 'unknown', W - 13)} ${c.dim}${box.v}${c.reset}`
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
// Show relative path to main
|
|
899
|
+
if (parallelSessions.sessionPath) {
|
|
900
|
+
const relPath = path.relative(parallelSessions.sessionPath, parallelSessions.mainPath) || '.';
|
|
901
|
+
lines.push(
|
|
902
|
+
`${c.dim}${box.v}${c.reset} ${c.slate}Main at:${c.reset} ${pad(relPath, W - 14)} ${c.dim}${box.v}${c.reset}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
lines.push(`${c.dim}${box.bl}${box.h.repeat(W)}${box.br}${c.reset}`);
|
|
907
|
+
|
|
908
|
+
return lines.join('\n');
|
|
909
|
+
}
|
|
910
|
+
|
|
852
911
|
// Main
|
|
853
912
|
async function main() {
|
|
854
913
|
const rootDir = getProjectRoot();
|
|
@@ -882,6 +941,12 @@ async function main() {
|
|
|
882
941
|
// Update check failed - continue without it
|
|
883
942
|
}
|
|
884
943
|
|
|
944
|
+
// Show session banner FIRST if in a non-main session
|
|
945
|
+
const sessionBanner = formatSessionBanner(parallelSessions);
|
|
946
|
+
if (sessionBanner) {
|
|
947
|
+
console.log(sessionBanner);
|
|
948
|
+
}
|
|
949
|
+
|
|
885
950
|
console.log(
|
|
886
951
|
formatTable(
|
|
887
952
|
info,
|
|
@@ -15,23 +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
|
-
function findProjectRoot() {
|
|
26
|
-
let dir = process.cwd();
|
|
27
|
-
while (dir !== '/') {
|
|
28
|
-
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
29
|
-
return dir;
|
|
30
|
-
}
|
|
31
|
-
dir = path.dirname(dir);
|
|
32
|
-
}
|
|
33
|
-
return process.cwd();
|
|
34
|
-
}
|
|
18
|
+
const {
|
|
19
|
+
findProjectRoot,
|
|
20
|
+
loadPatterns,
|
|
21
|
+
outputBlocked,
|
|
22
|
+
runDamageControlHook,
|
|
23
|
+
c,
|
|
24
|
+
} = require('./lib/damage-control-utils');
|
|
35
25
|
|
|
36
26
|
/**
|
|
37
27
|
* Parse simplified YAML for damage control patterns
|
|
@@ -91,31 +81,6 @@ function parseSimpleYAML(content) {
|
|
|
91
81
|
return config;
|
|
92
82
|
}
|
|
93
83
|
|
|
94
|
-
/**
|
|
95
|
-
* Load patterns configuration from YAML file
|
|
96
|
-
*/
|
|
97
|
-
function loadPatterns(projectRoot) {
|
|
98
|
-
const configPaths = [
|
|
99
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
100
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
101
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
for (const configPath of configPaths) {
|
|
105
|
-
if (fs.existsSync(configPath)) {
|
|
106
|
-
try {
|
|
107
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
108
|
-
return parseSimpleYAML(content);
|
|
109
|
-
} catch (e) {
|
|
110
|
-
// Continue to next path
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Return empty config if no file found (fail-open)
|
|
116
|
-
return { bashToolPatterns: [], askPatterns: [], agileflowProtections: [] };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
84
|
/**
|
|
120
85
|
* Test command against a single pattern rule
|
|
121
86
|
*/
|
|
@@ -163,76 +128,18 @@ function validateCommand(command, config) {
|
|
|
163
128
|
return { action: 'allow' };
|
|
164
129
|
}
|
|
165
130
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Parse tool input from Claude Code
|
|
182
|
-
const input = JSON.parse(inputData);
|
|
183
|
-
const command = input.command || input.tool_input?.command || '';
|
|
184
|
-
|
|
185
|
-
if (!command) {
|
|
186
|
-
// No command to validate - allow
|
|
187
|
-
process.exit(0);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Load patterns and validate
|
|
191
|
-
const config = loadPatterns(projectRoot);
|
|
192
|
-
const result = validateCommand(command, config);
|
|
193
|
-
|
|
194
|
-
switch (result.action) {
|
|
195
|
-
case 'block':
|
|
196
|
-
// Output error message and block
|
|
197
|
-
console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
|
|
198
|
-
console.error(
|
|
199
|
-
`${c.dim}Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}${c.reset}`
|
|
200
|
-
);
|
|
201
|
-
process.exit(2);
|
|
202
|
-
break;
|
|
203
|
-
|
|
204
|
-
case 'ask':
|
|
205
|
-
// Output JSON to trigger user confirmation
|
|
206
|
-
console.log(
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
result: 'ask',
|
|
209
|
-
message: result.reason,
|
|
210
|
-
})
|
|
211
|
-
);
|
|
212
|
-
process.exit(0);
|
|
213
|
-
break;
|
|
214
|
-
|
|
215
|
-
case 'allow':
|
|
216
|
-
default:
|
|
217
|
-
// Allow command to proceed
|
|
218
|
-
process.exit(0);
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// Parse error or other issue - fail open
|
|
222
|
-
// This ensures broken config doesn't block all commands
|
|
223
|
-
process.exit(0);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Handle no stdin (direct invocation)
|
|
228
|
-
process.stdin.on('error', () => {
|
|
229
|
-
process.exit(0);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Set timeout to prevent hanging
|
|
233
|
-
setTimeout(() => {
|
|
234
|
-
process.exit(0);
|
|
235
|
-
}, 4000);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
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,34 +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
|
-
*/
|
|
23
|
-
function findProjectRoot() {
|
|
24
|
-
let dir = process.cwd();
|
|
25
|
-
while (dir !== '/') {
|
|
26
|
-
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
27
|
-
return dir;
|
|
28
|
-
}
|
|
29
|
-
dir = path.dirname(dir);
|
|
30
|
-
}
|
|
31
|
-
return process.cwd();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Expand ~ to home directory
|
|
36
|
-
*/
|
|
37
|
-
function expandPath(p) {
|
|
38
|
-
if (p.startsWith('~/')) {
|
|
39
|
-
return path.join(os.homedir(), p.slice(2));
|
|
40
|
-
}
|
|
41
|
-
return p;
|
|
42
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
findProjectRoot,
|
|
17
|
+
loadPatterns,
|
|
18
|
+
pathMatches,
|
|
19
|
+
outputBlocked,
|
|
20
|
+
runDamageControlHook,
|
|
21
|
+
} = require('./lib/damage-control-utils');
|
|
43
22
|
|
|
44
23
|
/**
|
|
45
24
|
* Parse simplified YAML for path patterns
|
|
@@ -79,79 +58,6 @@ function parseSimpleYAML(content) {
|
|
|
79
58
|
return config;
|
|
80
59
|
}
|
|
81
60
|
|
|
82
|
-
/**
|
|
83
|
-
* Load patterns configuration from YAML file
|
|
84
|
-
*/
|
|
85
|
-
function loadPatterns(projectRoot) {
|
|
86
|
-
const configPaths = [
|
|
87
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
88
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
89
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
for (const configPath of configPaths) {
|
|
93
|
-
if (fs.existsSync(configPath)) {
|
|
94
|
-
try {
|
|
95
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
96
|
-
return parseSimpleYAML(content);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// Continue to next path
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Return empty config if no file found (fail-open)
|
|
104
|
-
return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Check if a file path matches any of the protected patterns
|
|
109
|
-
*/
|
|
110
|
-
function pathMatches(filePath, patterns) {
|
|
111
|
-
if (!filePath) return null;
|
|
112
|
-
|
|
113
|
-
const normalizedPath = path.resolve(filePath);
|
|
114
|
-
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
115
|
-
|
|
116
|
-
for (const pattern of patterns) {
|
|
117
|
-
const expandedPattern = expandPath(pattern);
|
|
118
|
-
|
|
119
|
-
// Check if pattern is a directory prefix
|
|
120
|
-
if (pattern.endsWith('/')) {
|
|
121
|
-
const patternDir = expandedPattern.slice(0, -1);
|
|
122
|
-
if (normalizedPath.startsWith(patternDir)) {
|
|
123
|
-
return pattern;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Check exact match
|
|
128
|
-
if (normalizedPath === expandedPattern) {
|
|
129
|
-
return pattern;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
133
|
-
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
134
|
-
return pattern;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check if pattern appears in path (for patterns like "*.pem")
|
|
138
|
-
if (pattern.startsWith('*')) {
|
|
139
|
-
const ext = pattern.slice(1);
|
|
140
|
-
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
141
|
-
return pattern;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Check if path contains pattern (for things like ".env.production")
|
|
146
|
-
const patternBase = path.basename(pattern);
|
|
147
|
-
if (path.basename(normalizedPath) === patternBase) {
|
|
148
|
-
return pattern;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
61
|
/**
|
|
156
62
|
* Validate file path for edit operation
|
|
157
63
|
*/
|
|
@@ -180,58 +86,15 @@ function validatePath(filePath, config) {
|
|
|
180
86
|
return { action: 'allow' };
|
|
181
87
|
}
|
|
182
88
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
process.stdin.on('end', () => {
|
|
197
|
-
try {
|
|
198
|
-
// Parse tool input from Claude Code
|
|
199
|
-
const input = JSON.parse(inputData);
|
|
200
|
-
const filePath = input.file_path || input.tool_input?.file_path || '';
|
|
201
|
-
|
|
202
|
-
if (!filePath) {
|
|
203
|
-
// No path to validate - allow
|
|
204
|
-
process.exit(0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Load patterns and validate
|
|
208
|
-
const config = loadPatterns(projectRoot);
|
|
209
|
-
const result = validatePath(filePath, config);
|
|
210
|
-
|
|
211
|
-
if (result.action === 'block') {
|
|
212
|
-
console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
|
|
213
|
-
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
214
|
-
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
215
|
-
process.exit(2);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Allow
|
|
219
|
-
process.exit(0);
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// Parse error or other issue - fail open
|
|
222
|
-
process.exit(0);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Handle no stdin
|
|
227
|
-
process.stdin.on('error', () => {
|
|
228
|
-
process.exit(0);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Set timeout to prevent hanging
|
|
232
|
-
setTimeout(() => {
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}, 4000);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
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
|
+
});
|
|
@@ -12,34 +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
|
-
*/
|
|
23
|
-
function findProjectRoot() {
|
|
24
|
-
let dir = process.cwd();
|
|
25
|
-
while (dir !== '/') {
|
|
26
|
-
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
27
|
-
return dir;
|
|
28
|
-
}
|
|
29
|
-
dir = path.dirname(dir);
|
|
30
|
-
}
|
|
31
|
-
return process.cwd();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Expand ~ to home directory
|
|
36
|
-
*/
|
|
37
|
-
function expandPath(p) {
|
|
38
|
-
if (p.startsWith('~/')) {
|
|
39
|
-
return path.join(os.homedir(), p.slice(2));
|
|
40
|
-
}
|
|
41
|
-
return p;
|
|
42
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
findProjectRoot,
|
|
17
|
+
loadPatterns,
|
|
18
|
+
pathMatches,
|
|
19
|
+
outputBlocked,
|
|
20
|
+
runDamageControlHook,
|
|
21
|
+
} = require('./lib/damage-control-utils');
|
|
43
22
|
|
|
44
23
|
/**
|
|
45
24
|
* Parse simplified YAML for path patterns
|
|
@@ -79,79 +58,6 @@ function parseSimpleYAML(content) {
|
|
|
79
58
|
return config;
|
|
80
59
|
}
|
|
81
60
|
|
|
82
|
-
/**
|
|
83
|
-
* Load patterns configuration from YAML file
|
|
84
|
-
*/
|
|
85
|
-
function loadPatterns(projectRoot) {
|
|
86
|
-
const configPaths = [
|
|
87
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
|
|
88
|
-
path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
|
|
89
|
-
path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
for (const configPath of configPaths) {
|
|
93
|
-
if (fs.existsSync(configPath)) {
|
|
94
|
-
try {
|
|
95
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
96
|
-
return parseSimpleYAML(content);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// Continue to next path
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Return empty config if no file found (fail-open)
|
|
104
|
-
return { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Check if a file path matches any of the protected patterns
|
|
109
|
-
*/
|
|
110
|
-
function pathMatches(filePath, patterns) {
|
|
111
|
-
if (!filePath) return null;
|
|
112
|
-
|
|
113
|
-
const normalizedPath = path.resolve(filePath);
|
|
114
|
-
const relativePath = path.relative(process.cwd(), normalizedPath);
|
|
115
|
-
|
|
116
|
-
for (const pattern of patterns) {
|
|
117
|
-
const expandedPattern = expandPath(pattern);
|
|
118
|
-
|
|
119
|
-
// Check if pattern is a directory prefix
|
|
120
|
-
if (pattern.endsWith('/')) {
|
|
121
|
-
const patternDir = expandedPattern.slice(0, -1);
|
|
122
|
-
if (normalizedPath.startsWith(patternDir)) {
|
|
123
|
-
return pattern;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Check exact match
|
|
128
|
-
if (normalizedPath === expandedPattern) {
|
|
129
|
-
return pattern;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check if normalized path ends with pattern (for filenames like "id_rsa")
|
|
133
|
-
if (normalizedPath.endsWith(pattern) || relativePath.endsWith(pattern)) {
|
|
134
|
-
return pattern;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check if pattern appears in path (for patterns like "*.pem")
|
|
138
|
-
if (pattern.startsWith('*')) {
|
|
139
|
-
const ext = pattern.slice(1);
|
|
140
|
-
if (normalizedPath.endsWith(ext) || relativePath.endsWith(ext)) {
|
|
141
|
-
return pattern;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Check if path contains pattern (for things like ".env.production")
|
|
146
|
-
const patternBase = path.basename(pattern);
|
|
147
|
-
if (path.basename(normalizedPath) === patternBase) {
|
|
148
|
-
return pattern;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
61
|
/**
|
|
156
62
|
* Validate file path for write operation
|
|
157
63
|
*/
|
|
@@ -180,58 +86,15 @@ function validatePath(filePath, config) {
|
|
|
180
86
|
return { action: 'allow' };
|
|
181
87
|
}
|
|
182
88
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
process.stdin.on('end', () => {
|
|
197
|
-
try {
|
|
198
|
-
// Parse tool input from Claude Code
|
|
199
|
-
const input = JSON.parse(inputData);
|
|
200
|
-
const filePath = input.file_path || input.tool_input?.file_path || '';
|
|
201
|
-
|
|
202
|
-
if (!filePath) {
|
|
203
|
-
// No path to validate - allow
|
|
204
|
-
process.exit(0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Load patterns and validate
|
|
208
|
-
const config = loadPatterns(projectRoot);
|
|
209
|
-
const result = validatePath(filePath, config);
|
|
210
|
-
|
|
211
|
-
if (result.action === 'block') {
|
|
212
|
-
console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
|
|
213
|
-
console.error(`${c.dim}${result.detail}${c.reset}`);
|
|
214
|
-
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
215
|
-
process.exit(2);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Allow
|
|
219
|
-
process.exit(0);
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// Parse error or other issue - fail open
|
|
222
|
-
process.exit(0);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Handle no stdin
|
|
227
|
-
process.stdin.on('error', () => {
|
|
228
|
-
process.exit(0);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Set timeout to prevent hanging
|
|
232
|
-
setTimeout(() => {
|
|
233
|
-
process.exit(0);
|
|
234
|
-
}, 4000);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
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
|
+
});
|