agileflow 2.76.0 → 2.78.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.
Files changed (124) hide show
  1. package/README.md +3 -3
  2. package/package.json +6 -1
  3. package/scripts/agileflow-configure.js +185 -13
  4. package/scripts/agileflow-statusline.sh +266 -27
  5. package/scripts/agileflow-welcome.js +160 -52
  6. package/scripts/auto-self-improve.js +63 -20
  7. package/scripts/check-update.js +1 -4
  8. package/scripts/damage-control-bash.js +232 -0
  9. package/scripts/damage-control-edit.js +243 -0
  10. package/scripts/damage-control-write.js +243 -0
  11. package/scripts/get-env.js +15 -7
  12. package/scripts/lib/frontmatter-parser.js +4 -1
  13. package/scripts/obtain-context.js +59 -48
  14. package/scripts/ralph-loop.js +25 -13
  15. package/scripts/validate-expertise.sh +19 -15
  16. package/src/core/agents/accessibility.md +124 -53
  17. package/src/core/agents/adr-writer.md +192 -52
  18. package/src/core/agents/analytics.md +139 -60
  19. package/src/core/agents/api.md +173 -63
  20. package/src/core/agents/ci.md +139 -57
  21. package/src/core/agents/compliance.md +159 -68
  22. package/src/core/agents/configuration/damage-control.md +356 -0
  23. package/src/core/agents/database.md +162 -61
  24. package/src/core/agents/datamigration.md +179 -66
  25. package/src/core/agents/design.md +179 -57
  26. package/src/core/agents/devops.md +160 -3
  27. package/src/core/agents/documentation.md +204 -60
  28. package/src/core/agents/epic-planner.md +147 -55
  29. package/src/core/agents/integrations.md +197 -69
  30. package/src/core/agents/mentor.md +158 -57
  31. package/src/core/agents/mobile.md +159 -67
  32. package/src/core/agents/monitoring.md +154 -65
  33. package/src/core/agents/multi-expert.md +115 -43
  34. package/src/core/agents/orchestrator.md +77 -24
  35. package/src/core/agents/performance.md +130 -75
  36. package/src/core/agents/product.md +151 -55
  37. package/src/core/agents/qa.md +162 -74
  38. package/src/core/agents/readme-updater.md +178 -76
  39. package/src/core/agents/refactor.md +148 -95
  40. package/src/core/agents/research.md +143 -72
  41. package/src/core/agents/security.md +154 -65
  42. package/src/core/agents/testing.md +176 -97
  43. package/src/core/agents/ui.md +170 -79
  44. package/src/core/commands/adr/list.md +171 -0
  45. package/src/core/commands/adr/update.md +235 -0
  46. package/src/core/commands/adr/view.md +252 -0
  47. package/src/core/commands/adr.md +207 -50
  48. package/src/core/commands/agent.md +16 -0
  49. package/src/core/commands/assign.md +148 -44
  50. package/src/core/commands/auto.md +18 -1
  51. package/src/core/commands/babysit.md +361 -36
  52. package/src/core/commands/baseline.md +14 -0
  53. package/src/core/commands/blockers.md +170 -51
  54. package/src/core/commands/board.md +144 -66
  55. package/src/core/commands/changelog.md +15 -0
  56. package/src/core/commands/ci.md +179 -69
  57. package/src/core/commands/compress.md +18 -0
  58. package/src/core/commands/configure.md +16 -0
  59. package/src/core/commands/context/export.md +193 -4
  60. package/src/core/commands/context/full.md +191 -18
  61. package/src/core/commands/context/note.md +248 -4
  62. package/src/core/commands/debt.md +17 -0
  63. package/src/core/commands/deploy.md +208 -65
  64. package/src/core/commands/deps.md +15 -0
  65. package/src/core/commands/diagnose.md +16 -0
  66. package/src/core/commands/docs.md +196 -64
  67. package/src/core/commands/epic/list.md +170 -0
  68. package/src/core/commands/epic/view.md +242 -0
  69. package/src/core/commands/epic.md +192 -69
  70. package/src/core/commands/feedback.md +191 -71
  71. package/src/core/commands/handoff.md +162 -48
  72. package/src/core/commands/help.md +9 -0
  73. package/src/core/commands/ideate.md +446 -0
  74. package/src/core/commands/impact.md +16 -0
  75. package/src/core/commands/metrics.md +141 -37
  76. package/src/core/commands/multi-expert.md +77 -0
  77. package/src/core/commands/packages.md +16 -0
  78. package/src/core/commands/pr.md +161 -67
  79. package/src/core/commands/readme-sync.md +16 -0
  80. package/src/core/commands/research/analyze.md +568 -0
  81. package/src/core/commands/research/ask.md +345 -20
  82. package/src/core/commands/research/import.md +562 -19
  83. package/src/core/commands/research/list.md +173 -5
  84. package/src/core/commands/research/view.md +181 -8
  85. package/src/core/commands/retro.md +135 -48
  86. package/src/core/commands/review.md +219 -47
  87. package/src/core/commands/session/end.md +209 -0
  88. package/src/core/commands/session/history.md +210 -0
  89. package/src/core/commands/session/init.md +116 -0
  90. package/src/core/commands/session/new.md +296 -0
  91. package/src/core/commands/session/resume.md +166 -0
  92. package/src/core/commands/session/status.md +166 -0
  93. package/src/core/commands/skill/create.md +115 -17
  94. package/src/core/commands/skill/delete.md +117 -0
  95. package/src/core/commands/skill/edit.md +104 -0
  96. package/src/core/commands/skill/list.md +128 -0
  97. package/src/core/commands/skill/test.md +135 -0
  98. package/src/core/commands/skill/upgrade.md +542 -0
  99. package/src/core/commands/sprint.md +17 -1
  100. package/src/core/commands/status.md +133 -21
  101. package/src/core/commands/story/list.md +176 -0
  102. package/src/core/commands/story/view.md +265 -0
  103. package/src/core/commands/story-validate.md +101 -1
  104. package/src/core/commands/story.md +204 -51
  105. package/src/core/commands/template.md +16 -1
  106. package/src/core/commands/tests.md +226 -64
  107. package/src/core/commands/update.md +17 -1
  108. package/src/core/commands/validate-expertise.md +16 -0
  109. package/src/core/commands/velocity.md +140 -36
  110. package/src/core/commands/verify.md +14 -0
  111. package/src/core/commands/whats-new.md +30 -0
  112. package/src/core/skills/_learnings/README.md +91 -0
  113. package/src/core/skills/_learnings/_template.yaml +106 -0
  114. package/src/core/skills/_learnings/commit.yaml +69 -0
  115. package/src/core/templates/damage-control-patterns.yaml +234 -0
  116. package/src/core/templates/skill-template.md +53 -11
  117. package/tools/cli/commands/list.js +3 -1
  118. package/tools/cli/commands/start.js +180 -0
  119. package/tools/cli/commands/uninstall.js +4 -5
  120. package/tools/cli/commands/update.js +11 -3
  121. package/tools/cli/lib/content-injector.js +6 -1
  122. package/tools/cli/tui/Dashboard.js +66 -0
  123. package/tools/cli/tui/StoryList.js +69 -0
  124. package/tools/cli/tui/index.js +16 -0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * damage-control-bash.js - PreToolUse hook for Bash tool
4
+ *
5
+ * Validates bash commands against patterns in damage-control-patterns.yaml
6
+ * before execution. Part of AgileFlow's damage control system.
7
+ *
8
+ * Exit codes:
9
+ * 0 - Allow command (or ask via JSON output)
10
+ * 2 - Block command
11
+ *
12
+ * For "ask" response, output JSON to stdout:
13
+ * { "result": "ask", "message": "Confirm this action?" }
14
+ *
15
+ * Usage: Configured as PreToolUse hook in .claude/settings.json
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // Color codes for output
22
+ const c = {
23
+ red: '\x1b[38;5;203m',
24
+ yellow: '\x1b[38;5;215m',
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
+ }
42
+
43
+ /**
44
+ * Parse simplified YAML for damage control patterns
45
+ * Only parses the structure we need - not a full YAML parser
46
+ */
47
+ function parseSimpleYAML(content) {
48
+ const config = {
49
+ bashToolPatterns: [],
50
+ askPatterns: [],
51
+ agileflowProtections: []
52
+ };
53
+
54
+ let currentSection = null;
55
+ let currentPattern = null;
56
+
57
+ for (const line of content.split('\n')) {
58
+ const trimmed = line.trim();
59
+
60
+ // Skip empty lines and comments
61
+ if (!trimmed || trimmed.startsWith('#')) continue;
62
+
63
+ // Detect section headers
64
+ if (trimmed === 'bashToolPatterns:') {
65
+ currentSection = 'bashToolPatterns';
66
+ currentPattern = null;
67
+ } else if (trimmed === 'askPatterns:') {
68
+ currentSection = 'askPatterns';
69
+ currentPattern = null;
70
+ } else if (trimmed === 'agileflowProtections:') {
71
+ currentSection = 'agileflowProtections';
72
+ currentPattern = null;
73
+ } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
74
+ // Other sections we don't care about for bash validation
75
+ currentSection = null;
76
+ currentPattern = null;
77
+ } else if (trimmed.startsWith('- pattern:') && currentSection) {
78
+ // New pattern entry
79
+ const patternValue = trimmed.replace('- pattern:', '').trim().replace(/^["']|["']$/g, '');
80
+ currentPattern = { pattern: patternValue };
81
+ config[currentSection].push(currentPattern);
82
+ } else if (trimmed.startsWith('reason:') && currentPattern) {
83
+ currentPattern.reason = trimmed.replace('reason:', '').trim().replace(/^["']|["']$/g, '');
84
+ } else if (trimmed.startsWith('flags:') && currentPattern) {
85
+ currentPattern.flags = trimmed.replace('flags:', '').trim().replace(/^["']|["']$/g, '');
86
+ }
87
+ }
88
+
89
+ return config;
90
+ }
91
+
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
+ /**
118
+ * Test command against a single pattern rule
119
+ */
120
+ function matchesPattern(command, rule) {
121
+ try {
122
+ const flags = rule.flags || '';
123
+ const regex = new RegExp(rule.pattern, flags);
124
+ return regex.test(command);
125
+ } catch (e) {
126
+ // Invalid regex - skip this pattern
127
+ return false;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Validate command against all patterns
133
+ */
134
+ function validateCommand(command, config) {
135
+ // Check blocked patterns (bashToolPatterns + agileflowProtections)
136
+ const blockedPatterns = [
137
+ ...(config.bashToolPatterns || []),
138
+ ...(config.agileflowProtections || [])
139
+ ];
140
+
141
+ for (const rule of blockedPatterns) {
142
+ if (matchesPattern(command, rule)) {
143
+ return {
144
+ action: 'block',
145
+ reason: rule.reason || 'Command blocked by damage control'
146
+ };
147
+ }
148
+ }
149
+
150
+ // Check ask patterns
151
+ for (const rule of config.askPatterns || []) {
152
+ if (matchesPattern(command, rule)) {
153
+ return {
154
+ action: 'ask',
155
+ reason: rule.reason || 'Please confirm this command'
156
+ };
157
+ }
158
+ }
159
+
160
+ // Allow by default
161
+ return { action: 'allow' };
162
+ }
163
+
164
+ /**
165
+ * Main function - read input and validate
166
+ */
167
+ function main() {
168
+ const projectRoot = findProjectRoot();
169
+ let inputData = '';
170
+
171
+ process.stdin.setEncoding('utf8');
172
+
173
+ process.stdin.on('data', chunk => {
174
+ inputData += chunk;
175
+ });
176
+
177
+ process.stdin.on('end', () => {
178
+ try {
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();
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * damage-control-edit.js - PreToolUse hook for Edit tool
4
+ *
5
+ * Validates file paths against access control patterns in damage-control-patterns.yaml
6
+ * before allowing file edits. Part of AgileFlow's damage control system.
7
+ *
8
+ * Exit codes:
9
+ * 0 - Allow operation
10
+ * 2 - Block operation
11
+ *
12
+ * Usage: Configured as PreToolUse hook in .claude/settings.json
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ // Color codes for output
20
+ const c = {
21
+ red: '\x1b[38;5;203m',
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
+ }
49
+
50
+ /**
51
+ * Parse simplified YAML for path patterns
52
+ */
53
+ function parseSimpleYAML(content) {
54
+ const config = {
55
+ zeroAccessPaths: [],
56
+ readOnlyPaths: [],
57
+ noDeletePaths: []
58
+ };
59
+
60
+ let currentSection = null;
61
+
62
+ for (const line of content.split('\n')) {
63
+ const trimmed = line.trim();
64
+
65
+ // Skip empty lines and comments
66
+ if (!trimmed || trimmed.startsWith('#')) continue;
67
+
68
+ // Detect section headers
69
+ if (trimmed === 'zeroAccessPaths:') {
70
+ currentSection = 'zeroAccessPaths';
71
+ } else if (trimmed === 'readOnlyPaths:') {
72
+ currentSection = 'readOnlyPaths';
73
+ } else if (trimmed === 'noDeletePaths:') {
74
+ currentSection = 'noDeletePaths';
75
+ } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
76
+ // Other sections we don't care about for path validation
77
+ currentSection = null;
78
+ } else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
79
+ // Path entry
80
+ const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
81
+ config[currentSection].push(pathValue);
82
+ }
83
+ }
84
+
85
+ return config;
86
+ }
87
+
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
+ /**
162
+ * Validate file path for edit operation
163
+ */
164
+ function validatePath(filePath, config) {
165
+ // Check zero access paths - completely blocked
166
+ const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
167
+ if (zeroMatch) {
168
+ return {
169
+ action: 'block',
170
+ reason: `Zero-access path: ${zeroMatch}`,
171
+ detail: 'This file is protected and cannot be accessed'
172
+ };
173
+ }
174
+
175
+ // Check read-only paths - cannot edit
176
+ const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
177
+ if (readOnlyMatch) {
178
+ return {
179
+ action: 'block',
180
+ reason: `Read-only path: ${readOnlyMatch}`,
181
+ detail: 'This file is read-only and cannot be edited'
182
+ };
183
+ }
184
+
185
+ // Allow by default
186
+ return { action: 'allow' };
187
+ }
188
+
189
+ /**
190
+ * Main function - read input and validate
191
+ */
192
+ function main() {
193
+ const projectRoot = findProjectRoot();
194
+ let inputData = '';
195
+
196
+ process.stdin.setEncoding('utf8');
197
+
198
+ process.stdin.on('data', chunk => {
199
+ inputData += chunk;
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();
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * damage-control-write.js - PreToolUse hook for Write tool
4
+ *
5
+ * Validates file paths against access control patterns in damage-control-patterns.yaml
6
+ * before allowing file writes. Part of AgileFlow's damage control system.
7
+ *
8
+ * Exit codes:
9
+ * 0 - Allow operation
10
+ * 2 - Block operation
11
+ *
12
+ * Usage: Configured as PreToolUse hook in .claude/settings.json
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ // Color codes for output
20
+ const c = {
21
+ red: '\x1b[38;5;203m',
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
+ }
49
+
50
+ /**
51
+ * Parse simplified YAML for path patterns
52
+ */
53
+ function parseSimpleYAML(content) {
54
+ const config = {
55
+ zeroAccessPaths: [],
56
+ readOnlyPaths: [],
57
+ noDeletePaths: []
58
+ };
59
+
60
+ let currentSection = null;
61
+
62
+ for (const line of content.split('\n')) {
63
+ const trimmed = line.trim();
64
+
65
+ // Skip empty lines and comments
66
+ if (!trimmed || trimmed.startsWith('#')) continue;
67
+
68
+ // Detect section headers
69
+ if (trimmed === 'zeroAccessPaths:') {
70
+ currentSection = 'zeroAccessPaths';
71
+ } else if (trimmed === 'readOnlyPaths:') {
72
+ currentSection = 'readOnlyPaths';
73
+ } else if (trimmed === 'noDeletePaths:') {
74
+ currentSection = 'noDeletePaths';
75
+ } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
76
+ // Other sections we don't care about for path validation
77
+ currentSection = null;
78
+ } else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
79
+ // Path entry
80
+ const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
81
+ config[currentSection].push(pathValue);
82
+ }
83
+ }
84
+
85
+ return config;
86
+ }
87
+
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
+ /**
162
+ * Validate file path for write operation
163
+ */
164
+ function validatePath(filePath, config) {
165
+ // Check zero access paths - completely blocked
166
+ const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
167
+ if (zeroMatch) {
168
+ return {
169
+ action: 'block',
170
+ reason: `Zero-access path: ${zeroMatch}`,
171
+ detail: 'This file is protected and cannot be accessed'
172
+ };
173
+ }
174
+
175
+ // Check read-only paths - cannot write
176
+ const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
177
+ if (readOnlyMatch) {
178
+ return {
179
+ action: 'block',
180
+ reason: `Read-only path: ${readOnlyMatch}`,
181
+ detail: 'This file is read-only and cannot be written to'
182
+ };
183
+ }
184
+
185
+ // Allow by default
186
+ return { action: 'allow' };
187
+ }
188
+
189
+ /**
190
+ * Main function - read input and validate
191
+ */
192
+ function main() {
193
+ const projectRoot = findProjectRoot();
194
+ let inputData = '';
195
+
196
+ process.stdin.setEncoding('utf8');
197
+
198
+ process.stdin.on('data', chunk => {
199
+ inputData += chunk;
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();