agileflow 2.87.0 → 2.88.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
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.88.0] - 2026-01-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Security and code quality improvements from EP-0012 ideation
|
|
14
|
+
|
|
10
15
|
## [2.87.0] - 2026-01-13
|
|
11
16
|
|
|
12
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agileflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.0",
|
|
4
4
|
"description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agile",
|
|
@@ -53,8 +53,6 @@
|
|
|
53
53
|
"test:coverage": "jest --coverage"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"blessed": "^0.1.81",
|
|
57
|
-
"blessed-contrib": "^4.10.1",
|
|
58
56
|
"chalk": "^4.1.2",
|
|
59
57
|
"commander": "^12.1.0",
|
|
60
58
|
"fs-extra": "^11.2.0",
|
|
@@ -20,67 +20,10 @@ const {
|
|
|
20
20
|
loadPatterns,
|
|
21
21
|
outputBlocked,
|
|
22
22
|
runDamageControlHook,
|
|
23
|
+
parseBashPatterns,
|
|
23
24
|
c,
|
|
24
25
|
} = require('./lib/damage-control-utils');
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Parse simplified YAML for damage control patterns
|
|
28
|
-
* Only parses the structure we need - not a full YAML parser
|
|
29
|
-
*/
|
|
30
|
-
function parseSimpleYAML(content) {
|
|
31
|
-
const config = {
|
|
32
|
-
bashToolPatterns: [],
|
|
33
|
-
askPatterns: [],
|
|
34
|
-
agileflowProtections: [],
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
let currentSection = null;
|
|
38
|
-
let currentPattern = null;
|
|
39
|
-
|
|
40
|
-
for (const line of content.split('\n')) {
|
|
41
|
-
const trimmed = line.trim();
|
|
42
|
-
|
|
43
|
-
// Skip empty lines and comments
|
|
44
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
45
|
-
|
|
46
|
-
// Detect section headers
|
|
47
|
-
if (trimmed === 'bashToolPatterns:') {
|
|
48
|
-
currentSection = 'bashToolPatterns';
|
|
49
|
-
currentPattern = null;
|
|
50
|
-
} else if (trimmed === 'askPatterns:') {
|
|
51
|
-
currentSection = 'askPatterns';
|
|
52
|
-
currentPattern = null;
|
|
53
|
-
} else if (trimmed === 'agileflowProtections:') {
|
|
54
|
-
currentSection = 'agileflowProtections';
|
|
55
|
-
currentPattern = null;
|
|
56
|
-
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
57
|
-
// Other sections we don't care about for bash validation
|
|
58
|
-
currentSection = null;
|
|
59
|
-
currentPattern = null;
|
|
60
|
-
} else if (trimmed.startsWith('- pattern:') && currentSection) {
|
|
61
|
-
// New pattern entry
|
|
62
|
-
const patternValue = trimmed
|
|
63
|
-
.replace('- pattern:', '')
|
|
64
|
-
.trim()
|
|
65
|
-
.replace(/^["']|["']$/g, '');
|
|
66
|
-
currentPattern = { pattern: patternValue };
|
|
67
|
-
config[currentSection].push(currentPattern);
|
|
68
|
-
} else if (trimmed.startsWith('reason:') && currentPattern) {
|
|
69
|
-
currentPattern.reason = trimmed
|
|
70
|
-
.replace('reason:', '')
|
|
71
|
-
.trim()
|
|
72
|
-
.replace(/^["']|["']$/g, '');
|
|
73
|
-
} else if (trimmed.startsWith('flags:') && currentPattern) {
|
|
74
|
-
currentPattern.flags = trimmed
|
|
75
|
-
.replace('flags:', '')
|
|
76
|
-
.trim()
|
|
77
|
-
.replace(/^["']|["']$/g, '');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return config;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
27
|
/**
|
|
85
28
|
* Test command against a single pattern rule
|
|
86
29
|
*/
|
|
@@ -134,7 +77,7 @@ const defaultConfig = { bashToolPatterns: [], askPatterns: [], agileflowProtecti
|
|
|
134
77
|
|
|
135
78
|
runDamageControlHook({
|
|
136
79
|
getInputValue: input => input.command || input.tool_input?.command,
|
|
137
|
-
loadConfig: () => loadPatterns(projectRoot,
|
|
80
|
+
loadConfig: () => loadPatterns(projectRoot, parseBashPatterns, defaultConfig),
|
|
138
81
|
validate: validateCommand,
|
|
139
82
|
onBlock: (result, command) => {
|
|
140
83
|
outputBlocked(
|
|
@@ -15,85 +15,20 @@
|
|
|
15
15
|
const {
|
|
16
16
|
findProjectRoot,
|
|
17
17
|
loadPatterns,
|
|
18
|
-
pathMatches,
|
|
19
18
|
outputBlocked,
|
|
20
19
|
runDamageControlHook,
|
|
20
|
+
parsePathPatterns,
|
|
21
|
+
validatePathAgainstPatterns,
|
|
21
22
|
} = require('./lib/damage-control-utils');
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Parse simplified YAML for path patterns
|
|
25
|
-
*/
|
|
26
|
-
function parseSimpleYAML(content) {
|
|
27
|
-
const config = {
|
|
28
|
-
zeroAccessPaths: [],
|
|
29
|
-
readOnlyPaths: [],
|
|
30
|
-
noDeletePaths: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let currentSection = null;
|
|
34
|
-
|
|
35
|
-
for (const line of content.split('\n')) {
|
|
36
|
-
const trimmed = line.trim();
|
|
37
|
-
|
|
38
|
-
// Skip empty lines and comments
|
|
39
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
40
|
-
|
|
41
|
-
// Detect section headers
|
|
42
|
-
if (trimmed === 'zeroAccessPaths:') {
|
|
43
|
-
currentSection = 'zeroAccessPaths';
|
|
44
|
-
} else if (trimmed === 'readOnlyPaths:') {
|
|
45
|
-
currentSection = 'readOnlyPaths';
|
|
46
|
-
} else if (trimmed === 'noDeletePaths:') {
|
|
47
|
-
currentSection = 'noDeletePaths';
|
|
48
|
-
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
49
|
-
// Other sections we don't care about for path validation
|
|
50
|
-
currentSection = null;
|
|
51
|
-
} else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
|
|
52
|
-
// Path entry
|
|
53
|
-
const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
54
|
-
config[currentSection].push(pathValue);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return config;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate file path for edit operation
|
|
63
|
-
*/
|
|
64
|
-
function validatePath(filePath, config) {
|
|
65
|
-
// Check zero access paths - completely blocked
|
|
66
|
-
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
67
|
-
if (zeroMatch) {
|
|
68
|
-
return {
|
|
69
|
-
action: 'block',
|
|
70
|
-
reason: `Zero-access path: ${zeroMatch}`,
|
|
71
|
-
detail: 'This file is protected and cannot be accessed',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check read-only paths - cannot edit
|
|
76
|
-
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
77
|
-
if (readOnlyMatch) {
|
|
78
|
-
return {
|
|
79
|
-
action: 'block',
|
|
80
|
-
reason: `Read-only path: ${readOnlyMatch}`,
|
|
81
|
-
detail: 'This file is read-only and cannot be edited',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Allow by default
|
|
86
|
-
return { action: 'allow' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
24
|
// Run the hook
|
|
90
25
|
const projectRoot = findProjectRoot();
|
|
91
26
|
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
92
27
|
|
|
93
28
|
runDamageControlHook({
|
|
94
29
|
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
95
|
-
loadConfig: () => loadPatterns(projectRoot,
|
|
96
|
-
validate:
|
|
30
|
+
loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
|
|
31
|
+
validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'edit'),
|
|
97
32
|
onBlock: (result, filePath) => {
|
|
98
33
|
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
99
34
|
},
|
|
@@ -15,85 +15,20 @@
|
|
|
15
15
|
const {
|
|
16
16
|
findProjectRoot,
|
|
17
17
|
loadPatterns,
|
|
18
|
-
pathMatches,
|
|
19
18
|
outputBlocked,
|
|
20
19
|
runDamageControlHook,
|
|
20
|
+
parsePathPatterns,
|
|
21
|
+
validatePathAgainstPatterns,
|
|
21
22
|
} = require('./lib/damage-control-utils');
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Parse simplified YAML for path patterns
|
|
25
|
-
*/
|
|
26
|
-
function parseSimpleYAML(content) {
|
|
27
|
-
const config = {
|
|
28
|
-
zeroAccessPaths: [],
|
|
29
|
-
readOnlyPaths: [],
|
|
30
|
-
noDeletePaths: [],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let currentSection = null;
|
|
34
|
-
|
|
35
|
-
for (const line of content.split('\n')) {
|
|
36
|
-
const trimmed = line.trim();
|
|
37
|
-
|
|
38
|
-
// Skip empty lines and comments
|
|
39
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
40
|
-
|
|
41
|
-
// Detect section headers
|
|
42
|
-
if (trimmed === 'zeroAccessPaths:') {
|
|
43
|
-
currentSection = 'zeroAccessPaths';
|
|
44
|
-
} else if (trimmed === 'readOnlyPaths:') {
|
|
45
|
-
currentSection = 'readOnlyPaths';
|
|
46
|
-
} else if (trimmed === 'noDeletePaths:') {
|
|
47
|
-
currentSection = 'noDeletePaths';
|
|
48
|
-
} else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
|
49
|
-
// Other sections we don't care about for path validation
|
|
50
|
-
currentSection = null;
|
|
51
|
-
} else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
|
|
52
|
-
// Path entry
|
|
53
|
-
const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
54
|
-
config[currentSection].push(pathValue);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return config;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate file path for write operation
|
|
63
|
-
*/
|
|
64
|
-
function validatePath(filePath, config) {
|
|
65
|
-
// Check zero access paths - completely blocked
|
|
66
|
-
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
67
|
-
if (zeroMatch) {
|
|
68
|
-
return {
|
|
69
|
-
action: 'block',
|
|
70
|
-
reason: `Zero-access path: ${zeroMatch}`,
|
|
71
|
-
detail: 'This file is protected and cannot be accessed',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check read-only paths - cannot write
|
|
76
|
-
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
77
|
-
if (readOnlyMatch) {
|
|
78
|
-
return {
|
|
79
|
-
action: 'block',
|
|
80
|
-
reason: `Read-only path: ${readOnlyMatch}`,
|
|
81
|
-
detail: 'This file is read-only and cannot be written to',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Allow by default
|
|
86
|
-
return { action: 'allow' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
24
|
// Run the hook
|
|
90
25
|
const projectRoot = findProjectRoot();
|
|
91
26
|
const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
92
27
|
|
|
93
28
|
runDamageControlHook({
|
|
94
29
|
getInputValue: input => input.file_path || input.tool_input?.file_path,
|
|
95
|
-
loadConfig: () => loadPatterns(projectRoot,
|
|
96
|
-
validate:
|
|
30
|
+
loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
|
|
31
|
+
validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'write'),
|
|
97
32
|
onBlock: (result, filePath) => {
|
|
98
33
|
outputBlocked(result.reason, result.detail, `File: ${filePath}`);
|
|
99
34
|
},
|
|
@@ -238,6 +238,155 @@ function runDamageControlHook(options) {
|
|
|
238
238
|
}, STDIN_TIMEOUT_MS);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Parse simplified YAML for damage control patterns
|
|
243
|
+
* Handles both pattern-based sections (with pattern/reason/flags objects)
|
|
244
|
+
* and list-based sections (with simple string arrays)
|
|
245
|
+
*
|
|
246
|
+
* @param {string} content - YAML file content
|
|
247
|
+
* @param {object} sectionConfig - Map of section names to their type ('patterns' or 'list')
|
|
248
|
+
* @returns {object} Parsed configuration
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* // For bash patterns (pattern objects):
|
|
252
|
+
* parseSimpleYAML(content, {
|
|
253
|
+
* bashToolPatterns: 'patterns',
|
|
254
|
+
* askPatterns: 'patterns',
|
|
255
|
+
* agileflowProtections: 'patterns',
|
|
256
|
+
* })
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* // For path lists (string arrays):
|
|
260
|
+
* parseSimpleYAML(content, {
|
|
261
|
+
* zeroAccessPaths: 'list',
|
|
262
|
+
* readOnlyPaths: 'list',
|
|
263
|
+
* noDeletePaths: 'list',
|
|
264
|
+
* })
|
|
265
|
+
*/
|
|
266
|
+
function parseSimpleYAML(content, sectionConfig) {
|
|
267
|
+
// Initialize result with empty arrays for each section
|
|
268
|
+
const config = {};
|
|
269
|
+
for (const section of Object.keys(sectionConfig)) {
|
|
270
|
+
config[section] = [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let currentSection = null;
|
|
274
|
+
let currentPattern = null;
|
|
275
|
+
|
|
276
|
+
for (const line of content.split('\n')) {
|
|
277
|
+
const trimmed = line.trim();
|
|
278
|
+
|
|
279
|
+
// Skip empty lines and comments
|
|
280
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
281
|
+
|
|
282
|
+
// Check if this line is a section header we care about
|
|
283
|
+
const sectionMatch = trimmed.match(/^(\w+):$/);
|
|
284
|
+
if (sectionMatch) {
|
|
285
|
+
const sectionName = sectionMatch[1];
|
|
286
|
+
if (sectionConfig[sectionName]) {
|
|
287
|
+
currentSection = sectionName;
|
|
288
|
+
currentPattern = null;
|
|
289
|
+
} else {
|
|
290
|
+
// Section we don't care about
|
|
291
|
+
currentSection = null;
|
|
292
|
+
currentPattern = null;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Skip if we're not in a tracked section
|
|
298
|
+
if (!currentSection) continue;
|
|
299
|
+
|
|
300
|
+
const sectionType = sectionConfig[currentSection];
|
|
301
|
+
|
|
302
|
+
if (sectionType === 'patterns') {
|
|
303
|
+
// Pattern-based section (objects with pattern/reason/flags)
|
|
304
|
+
if (trimmed.startsWith('- pattern:')) {
|
|
305
|
+
const patternValue = trimmed
|
|
306
|
+
.replace('- pattern:', '')
|
|
307
|
+
.trim()
|
|
308
|
+
.replace(/^["']|["']$/g, '');
|
|
309
|
+
currentPattern = { pattern: patternValue };
|
|
310
|
+
config[currentSection].push(currentPattern);
|
|
311
|
+
} else if (trimmed.startsWith('reason:') && currentPattern) {
|
|
312
|
+
currentPattern.reason = trimmed
|
|
313
|
+
.replace('reason:', '')
|
|
314
|
+
.trim()
|
|
315
|
+
.replace(/^["']|["']$/g, '');
|
|
316
|
+
} else if (trimmed.startsWith('flags:') && currentPattern) {
|
|
317
|
+
currentPattern.flags = trimmed
|
|
318
|
+
.replace('flags:', '')
|
|
319
|
+
.trim()
|
|
320
|
+
.replace(/^["']|["']$/g, '');
|
|
321
|
+
}
|
|
322
|
+
} else if (sectionType === 'list') {
|
|
323
|
+
// List-based section (simple string arrays)
|
|
324
|
+
if (trimmed.startsWith('- ')) {
|
|
325
|
+
const value = trimmed.slice(2).replace(/^["']|["']$/g, '');
|
|
326
|
+
config[currentSection].push(value);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return config;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Pre-configured parser for bash tool patterns
|
|
336
|
+
*/
|
|
337
|
+
function parseBashPatterns(content) {
|
|
338
|
+
return parseSimpleYAML(content, {
|
|
339
|
+
bashToolPatterns: 'patterns',
|
|
340
|
+
askPatterns: 'patterns',
|
|
341
|
+
agileflowProtections: 'patterns',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Pre-configured parser for path patterns (edit/write)
|
|
347
|
+
*/
|
|
348
|
+
function parsePathPatterns(content) {
|
|
349
|
+
return parseSimpleYAML(content, {
|
|
350
|
+
zeroAccessPaths: 'list',
|
|
351
|
+
readOnlyPaths: 'list',
|
|
352
|
+
noDeletePaths: 'list',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Validate file path against path patterns
|
|
358
|
+
* Used by both edit and write hooks
|
|
359
|
+
*
|
|
360
|
+
* @param {string} filePath - File path to validate
|
|
361
|
+
* @param {object} config - Parsed path patterns config
|
|
362
|
+
* @param {string} operation - Operation type ('edit' or 'write') for error messages
|
|
363
|
+
* @returns {object} Validation result { action, reason?, detail? }
|
|
364
|
+
*/
|
|
365
|
+
function validatePathAgainstPatterns(filePath, config, operation = 'access') {
|
|
366
|
+
// Check zero access paths - completely blocked
|
|
367
|
+
const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
|
|
368
|
+
if (zeroMatch) {
|
|
369
|
+
return {
|
|
370
|
+
action: 'block',
|
|
371
|
+
reason: `Zero-access path: ${zeroMatch}`,
|
|
372
|
+
detail: 'This file is protected and cannot be accessed',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check read-only paths - cannot edit/write
|
|
377
|
+
const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
|
|
378
|
+
if (readOnlyMatch) {
|
|
379
|
+
return {
|
|
380
|
+
action: 'block',
|
|
381
|
+
reason: `Read-only path: ${readOnlyMatch}`,
|
|
382
|
+
detail: `This file is read-only and cannot be ${operation === 'edit' ? 'edited' : 'written to'}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Allow by default
|
|
387
|
+
return { action: 'allow' };
|
|
388
|
+
}
|
|
389
|
+
|
|
241
390
|
module.exports = {
|
|
242
391
|
c,
|
|
243
392
|
findProjectRoot,
|
|
@@ -246,6 +395,10 @@ module.exports = {
|
|
|
246
395
|
pathMatches,
|
|
247
396
|
outputBlocked,
|
|
248
397
|
runDamageControlHook,
|
|
398
|
+
parseSimpleYAML,
|
|
399
|
+
parseBashPatterns,
|
|
400
|
+
parsePathPatterns,
|
|
401
|
+
validatePathAgainstPatterns,
|
|
249
402
|
CONFIG_PATHS,
|
|
250
403
|
STDIN_TIMEOUT_MS,
|
|
251
404
|
};
|