agileflow 2.77.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.
- package/README.md +3 -3
- package/package.json +6 -1
- package/scripts/agileflow-configure.js +174 -2
- package/scripts/agileflow-statusline.sh +171 -78
- package/scripts/agileflow-welcome.js +79 -2
- 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/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/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 +361 -36
- 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/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/commit.yaml +69 -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/tui/Dashboard.js +66 -0
- package/tools/cli/tui/StoryList.js +69 -0
- package/tools/cli/tui/index.js +16 -0
|
@@ -346,6 +346,67 @@ function checkPreCompact(rootDir) {
|
|
|
346
346
|
return result;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
function checkDamageControl(rootDir) {
|
|
350
|
+
const result = { configured: false, level: null, patternCount: 0, scriptsOk: true };
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Check if PreToolUse hooks are configured in settings
|
|
354
|
+
const settingsPath = path.join(rootDir, '.claude/settings.json');
|
|
355
|
+
if (fs.existsSync(settingsPath)) {
|
|
356
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
357
|
+
if (settings.hooks?.PreToolUse && Array.isArray(settings.hooks.PreToolUse)) {
|
|
358
|
+
// Check for damage-control hooks
|
|
359
|
+
const hasDamageControlHooks = settings.hooks.PreToolUse.some(
|
|
360
|
+
h => h.hooks?.some(hk => hk.command?.includes('damage-control'))
|
|
361
|
+
);
|
|
362
|
+
if (hasDamageControlHooks) {
|
|
363
|
+
result.configured = true;
|
|
364
|
+
|
|
365
|
+
// Count how many hooks are present (should be 3: Bash, Edit, Write)
|
|
366
|
+
const dcHooks = settings.hooks.PreToolUse.filter(h =>
|
|
367
|
+
h.hooks?.some(hk => hk.command?.includes('damage-control'))
|
|
368
|
+
);
|
|
369
|
+
result.hooksCount = dcHooks.length;
|
|
370
|
+
|
|
371
|
+
// Check if all required scripts exist
|
|
372
|
+
const scriptsDir = path.join(rootDir, '.agileflow', 'scripts');
|
|
373
|
+
const requiredScripts = [
|
|
374
|
+
'damage-control-bash.js',
|
|
375
|
+
'damage-control-edit.js',
|
|
376
|
+
'damage-control-write.js',
|
|
377
|
+
];
|
|
378
|
+
for (const script of requiredScripts) {
|
|
379
|
+
if (!fs.existsSync(path.join(scriptsDir, script))) {
|
|
380
|
+
result.scriptsOk = false;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get protection level and pattern count from metadata
|
|
389
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
390
|
+
if (fs.existsSync(metadataPath)) {
|
|
391
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
392
|
+
if (metadata.features?.damagecontrol) {
|
|
393
|
+
result.level = metadata.features.damagecontrol.protectionLevel || 'standard';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Count patterns in config file
|
|
398
|
+
const patternsPath = path.join(rootDir, '.agileflow', 'config', 'damage-control-patterns.yaml');
|
|
399
|
+
if (fs.existsSync(patternsPath)) {
|
|
400
|
+
const content = fs.readFileSync(patternsPath, 'utf8');
|
|
401
|
+
// Count pattern entries (lines starting with " - pattern:")
|
|
402
|
+
const patternMatches = content.match(/^\s*-\s*pattern:/gm);
|
|
403
|
+
result.patternCount = patternMatches ? patternMatches.length : 0;
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
349
410
|
// Compare semantic versions: returns -1 if a < b, 0 if equal, 1 if a > b
|
|
350
411
|
function compareVersions(a, b) {
|
|
351
412
|
if (!a || !b) return 0;
|
|
@@ -611,7 +672,8 @@ function formatTable(
|
|
|
611
672
|
precompact,
|
|
612
673
|
parallelSessions,
|
|
613
674
|
updateInfo = {},
|
|
614
|
-
expertise = {}
|
|
675
|
+
expertise = {},
|
|
676
|
+
damageControl = {}
|
|
615
677
|
) {
|
|
616
678
|
const W = 58; // inner width
|
|
617
679
|
const R = W - 24; // right column width (34 chars)
|
|
@@ -783,6 +845,20 @@ function formatTable(
|
|
|
783
845
|
}
|
|
784
846
|
}
|
|
785
847
|
|
|
848
|
+
// Damage control status (PreToolUse hooks for dangerous command protection)
|
|
849
|
+
if (damageControl && damageControl.configured) {
|
|
850
|
+
if (!damageControl.scriptsOk) {
|
|
851
|
+
lines.push(row('Damage control', '⚠️ scripts missing', c.coral, c.coral));
|
|
852
|
+
} else {
|
|
853
|
+
const levelStr = damageControl.level || 'standard';
|
|
854
|
+
const patternStr = damageControl.patternCount > 0 ? `${damageControl.patternCount} patterns` : '';
|
|
855
|
+
const dcStatus = `🛡️ ${levelStr}${patternStr ? ` (${patternStr})` : ''}`;
|
|
856
|
+
lines.push(row('Damage control', dcStatus, c.lavender, c.mintGreen));
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
lines.push(row('Damage control', 'not configured', c.slate, c.slate));
|
|
860
|
+
}
|
|
861
|
+
|
|
786
862
|
lines.push(divider());
|
|
787
863
|
|
|
788
864
|
// Current story (colorful like obtain-context)
|
|
@@ -816,6 +892,7 @@ async function main() {
|
|
|
816
892
|
const precompact = checkPreCompact(rootDir);
|
|
817
893
|
const parallelSessions = checkParallelSessions(rootDir);
|
|
818
894
|
const expertise = validateExpertise(rootDir);
|
|
895
|
+
const damageControl = checkDamageControl(rootDir);
|
|
819
896
|
|
|
820
897
|
// Check for updates (async, cached)
|
|
821
898
|
let updateInfo = {};
|
|
@@ -840,7 +917,7 @@ async function main() {
|
|
|
840
917
|
}
|
|
841
918
|
|
|
842
919
|
console.log(
|
|
843
|
-
formatTable(info, archival, session, precompact, parallelSessions, updateInfo, expertise)
|
|
920
|
+
formatTable(info, archival, session, precompact, parallelSessions, updateInfo, expertise, damageControl)
|
|
844
921
|
);
|
|
845
922
|
|
|
846
923
|
// Show warning and tip if other sessions are active (vibrant colors)
|
|
@@ -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();
|