agileflow 2.77.0 → 2.79.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 +6 -1
- package/scripts/agileflow-configure.js +174 -2
- package/scripts/agileflow-statusline.sh +171 -78
- package/scripts/agileflow-welcome.js +88 -64
- package/scripts/auto-self-improve.js +23 -45
- package/scripts/check-update.js +35 -42
- package/scripts/damage-control/bash-tool-damage-control.js +257 -0
- package/scripts/damage-control/edit-tool-damage-control.js +279 -0
- package/scripts/damage-control/patterns.yaml +227 -0
- package/scripts/damage-control/write-tool-damage-control.js +274 -0
- package/scripts/damage-control-bash.js +232 -0
- package/scripts/damage-control-edit.js +243 -0
- package/scripts/damage-control-write.js +243 -0
- package/scripts/obtain-context.js +22 -3
- package/scripts/ralph-loop.js +191 -63
- package/scripts/screenshot-verifier.js +213 -0
- package/scripts/session-manager.js +12 -33
- package/src/core/agents/accessibility.md +124 -53
- package/src/core/agents/adr-writer.md +192 -52
- package/src/core/agents/analytics.md +139 -60
- package/src/core/agents/api.md +173 -63
- package/src/core/agents/ci.md +139 -57
- package/src/core/agents/compliance.md +159 -68
- package/src/core/agents/configuration/damage-control.md +356 -0
- package/src/core/agents/configuration-damage-control.md +248 -0
- package/src/core/agents/database.md +162 -61
- package/src/core/agents/datamigration.md +179 -66
- package/src/core/agents/design.md +179 -57
- package/src/core/agents/devops.md +160 -3
- package/src/core/agents/documentation.md +204 -60
- package/src/core/agents/epic-planner.md +147 -55
- package/src/core/agents/integrations.md +197 -69
- package/src/core/agents/mentor.md +158 -57
- package/src/core/agents/mobile.md +159 -67
- package/src/core/agents/monitoring.md +154 -65
- package/src/core/agents/multi-expert.md +115 -43
- package/src/core/agents/orchestrator.md +77 -24
- package/src/core/agents/performance.md +130 -75
- package/src/core/agents/product.md +151 -55
- package/src/core/agents/qa.md +162 -74
- package/src/core/agents/readme-updater.md +178 -76
- package/src/core/agents/refactor.md +148 -95
- package/src/core/agents/research.md +143 -72
- package/src/core/agents/security.md +154 -65
- package/src/core/agents/testing.md +176 -97
- package/src/core/agents/ui.md +170 -79
- package/src/core/commands/adr/list.md +171 -0
- package/src/core/commands/adr/update.md +235 -0
- package/src/core/commands/adr/view.md +252 -0
- package/src/core/commands/adr.md +207 -50
- package/src/core/commands/agent.md +16 -0
- package/src/core/commands/assign.md +148 -44
- package/src/core/commands/auto.md +18 -1
- package/src/core/commands/babysit.md +391 -38
- package/src/core/commands/baseline.md +14 -0
- package/src/core/commands/blockers.md +170 -51
- package/src/core/commands/board.md +144 -66
- package/src/core/commands/changelog.md +15 -0
- package/src/core/commands/ci.md +179 -69
- package/src/core/commands/compress.md +18 -0
- package/src/core/commands/configure.md +16 -0
- package/src/core/commands/context/export.md +193 -4
- package/src/core/commands/context/full.md +191 -18
- package/src/core/commands/context/note.md +248 -4
- package/src/core/commands/debt.md +17 -0
- package/src/core/commands/deploy.md +208 -65
- package/src/core/commands/deps.md +15 -0
- package/src/core/commands/diagnose.md +16 -0
- package/src/core/commands/docs.md +196 -64
- package/src/core/commands/epic/list.md +170 -0
- package/src/core/commands/epic/view.md +242 -0
- package/src/core/commands/epic.md +192 -69
- package/src/core/commands/feedback.md +191 -71
- package/src/core/commands/handoff.md +162 -48
- package/src/core/commands/help.md +9 -0
- package/src/core/commands/ideate.md +446 -0
- package/src/core/commands/impact.md +16 -0
- package/src/core/commands/metrics.md +141 -37
- package/src/core/commands/multi-expert.md +77 -0
- package/src/core/commands/packages.md +16 -0
- package/src/core/commands/pr.md +161 -67
- package/src/core/commands/readme-sync.md +16 -0
- package/src/core/commands/research/analyze.md +568 -0
- package/src/core/commands/research/ask.md +345 -20
- package/src/core/commands/research/import.md +562 -19
- package/src/core/commands/research/list.md +173 -5
- package/src/core/commands/research/view.md +181 -8
- package/src/core/commands/retro.md +135 -48
- package/src/core/commands/review.md +219 -47
- package/src/core/commands/session/end.md +209 -0
- package/src/core/commands/session/history.md +210 -0
- package/src/core/commands/session/init.md +116 -0
- package/src/core/commands/session/new.md +296 -0
- package/src/core/commands/session/resume.md +166 -0
- package/src/core/commands/session/status.md +166 -0
- package/src/core/commands/setup/visual-e2e.md +462 -0
- package/src/core/commands/skill/create.md +115 -17
- package/src/core/commands/skill/delete.md +117 -0
- package/src/core/commands/skill/edit.md +104 -0
- package/src/core/commands/skill/list.md +128 -0
- package/src/core/commands/skill/test.md +135 -0
- package/src/core/commands/skill/upgrade.md +542 -0
- package/src/core/commands/sprint.md +17 -1
- package/src/core/commands/status.md +133 -21
- package/src/core/commands/story/list.md +176 -0
- package/src/core/commands/story/view.md +265 -0
- package/src/core/commands/story-validate.md +101 -1
- package/src/core/commands/story.md +204 -51
- package/src/core/commands/template.md +16 -1
- package/src/core/commands/tests.md +226 -64
- package/src/core/commands/update.md +17 -1
- package/src/core/commands/validate-expertise.md +16 -0
- package/src/core/commands/velocity.md +140 -36
- package/src/core/commands/verify.md +14 -0
- package/src/core/commands/whats-new.md +30 -0
- package/src/core/skills/_learnings/README.md +91 -0
- package/src/core/skills/_learnings/_template.yaml +106 -0
- package/src/core/skills/_learnings/code-review.yaml +118 -0
- package/src/core/skills/_learnings/commit.yaml +69 -0
- package/src/core/skills/_learnings/story-writer.yaml +71 -0
- package/src/core/templates/damage-control-patterns.yaml +234 -0
- package/src/core/templates/skill-template.md +53 -11
- package/tools/cli/commands/start.js +180 -0
- package/tools/cli/installers/ide/claude-code.js +127 -0
- package/tools/cli/tui/Dashboard.js +66 -0
- package/tools/cli/tui/StoryList.js +69 -0
- package/tools/cli/tui/index.js +16 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* write-tool-damage-control.js - Enforce path protection for Write tool
|
|
5
|
+
*
|
|
6
|
+
* This PreToolUse hook runs before every Write tool execution.
|
|
7
|
+
* It checks the file path against patterns.yaml to block writes
|
|
8
|
+
* to protected paths.
|
|
9
|
+
*
|
|
10
|
+
* Path protection levels:
|
|
11
|
+
* zeroAccessPaths: Cannot read, write, edit, or delete
|
|
12
|
+
* readOnlyPaths: Can read, cannot write or delete
|
|
13
|
+
* noDeletePaths: Can read and write, cannot delete (Write is allowed)
|
|
14
|
+
*
|
|
15
|
+
* Exit codes:
|
|
16
|
+
* 0 = Allow write to proceed
|
|
17
|
+
* 2 = Block write
|
|
18
|
+
*
|
|
19
|
+
* Usage (as PreToolUse hook):
|
|
20
|
+
* node .claude/hooks/damage-control/write-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: [
|
|
118
|
+
'~/.ssh/',
|
|
119
|
+
'~/.aws/credentials',
|
|
120
|
+
'.env',
|
|
121
|
+
'.env.local',
|
|
122
|
+
'.env.production',
|
|
123
|
+
],
|
|
124
|
+
readOnlyPaths: [
|
|
125
|
+
'/etc/',
|
|
126
|
+
'~/.bashrc',
|
|
127
|
+
'~/.zshrc',
|
|
128
|
+
'package-lock.json',
|
|
129
|
+
'yarn.lock',
|
|
130
|
+
'.git/',
|
|
131
|
+
],
|
|
132
|
+
noDeletePaths: [
|
|
133
|
+
'.agileflow/',
|
|
134
|
+
'.claude/',
|
|
135
|
+
'docs/09-agents/status.json',
|
|
136
|
+
'CLAUDE.md',
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Expand home directory in path
|
|
143
|
+
*/
|
|
144
|
+
function expandHome(filePath) {
|
|
145
|
+
if (filePath.startsWith('~/')) {
|
|
146
|
+
return path.join(process.env.HOME || '', filePath.slice(2));
|
|
147
|
+
}
|
|
148
|
+
return filePath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if a file path matches a pattern
|
|
153
|
+
*/
|
|
154
|
+
function pathMatches(filePath, pattern) {
|
|
155
|
+
const expandedPattern = expandHome(pattern);
|
|
156
|
+
const normalizedFile = path.normalize(filePath);
|
|
157
|
+
const normalizedPattern = path.normalize(expandedPattern);
|
|
158
|
+
|
|
159
|
+
// Exact match
|
|
160
|
+
if (normalizedFile === normalizedPattern) return true;
|
|
161
|
+
|
|
162
|
+
// Directory prefix match
|
|
163
|
+
if (pattern.endsWith('/')) {
|
|
164
|
+
if (normalizedFile.startsWith(normalizedPattern)) return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Glob pattern (**)
|
|
168
|
+
if (pattern.includes('**/')) {
|
|
169
|
+
const globPart = pattern.split('**/')[1];
|
|
170
|
+
if (normalizedFile.includes(globPart)) return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Wildcard at end
|
|
174
|
+
if (pattern.endsWith('*')) {
|
|
175
|
+
const prefix = normalizedPattern.slice(0, -1);
|
|
176
|
+
if (normalizedFile.startsWith(prefix)) return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Basename match
|
|
180
|
+
if (!pattern.includes('/') && !pattern.includes(path.sep)) {
|
|
181
|
+
const basename = path.basename(normalizedFile);
|
|
182
|
+
if (basename === pattern) return true;
|
|
183
|
+
if (pattern.endsWith('*')) {
|
|
184
|
+
const patternBase = pattern.slice(0, -1);
|
|
185
|
+
if (basename.startsWith(patternBase)) return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if file path is protected for writing
|
|
194
|
+
* Returns: { blocked: boolean, reason: string, level: string }
|
|
195
|
+
*/
|
|
196
|
+
function checkPath(filePath, rules) {
|
|
197
|
+
// Check zero access paths (blocked completely)
|
|
198
|
+
for (const pattern of rules.zeroAccessPaths) {
|
|
199
|
+
if (pathMatches(filePath, pattern)) {
|
|
200
|
+
return {
|
|
201
|
+
blocked: true,
|
|
202
|
+
reason: `Path is in zero-access protected list: ${pattern}`,
|
|
203
|
+
level: 'zero-access',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check read-only paths (cannot write)
|
|
209
|
+
for (const pattern of rules.readOnlyPaths) {
|
|
210
|
+
if (pathMatches(filePath, pattern)) {
|
|
211
|
+
return {
|
|
212
|
+
blocked: true,
|
|
213
|
+
reason: `Path is read-only: ${pattern}`,
|
|
214
|
+
level: 'read-only',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// noDeletePaths allows writing, only blocks deletion
|
|
220
|
+
// so we don't block writes here
|
|
221
|
+
|
|
222
|
+
return { blocked: false, reason: null, level: null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Main entry point
|
|
227
|
+
*/
|
|
228
|
+
function main() {
|
|
229
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT;
|
|
230
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
231
|
+
|
|
232
|
+
if (!toolInput) {
|
|
233
|
+
process.exit(EXIT_ALLOW);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let input;
|
|
237
|
+
try {
|
|
238
|
+
input = JSON.parse(toolInput);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
console.error('Error parsing CLAUDE_TOOL_INPUT:', e.message);
|
|
241
|
+
process.exit(EXIT_ALLOW);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const filePath = input.file_path;
|
|
245
|
+
if (!filePath) {
|
|
246
|
+
process.exit(EXIT_ALLOW);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Resolve to absolute path
|
|
250
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
251
|
+
? filePath
|
|
252
|
+
: path.join(projectDir, filePath);
|
|
253
|
+
|
|
254
|
+
// Load rules
|
|
255
|
+
const rules = loadPathRules(projectDir);
|
|
256
|
+
|
|
257
|
+
// Check path
|
|
258
|
+
const result = checkPath(absolutePath, rules);
|
|
259
|
+
|
|
260
|
+
if (result.blocked) {
|
|
261
|
+
console.error(`${c.red}${c.bold}BLOCKED${c.reset}: ${result.reason}`);
|
|
262
|
+
console.error(`${c.yellow}File: ${filePath}${c.reset}`);
|
|
263
|
+
console.error(`${c.cyan}This file is protected by damage control (${result.level}).${c.reset}`);
|
|
264
|
+
process.exit(EXIT_BLOCK);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
process.exit(EXIT_ALLOW);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (require.main === module) {
|
|
271
|
+
main();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { checkPath, loadPathRules, pathMatches };
|
|
@@ -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();
|