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.87.0",
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, parseSimpleYAML, defaultConfig),
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, parseSimpleYAML, defaultConfig),
96
- validate: validatePath,
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, parseSimpleYAML, defaultConfig),
96
- validate: validatePath,
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
  };