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
|
@@ -15,6 +15,10 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { execSync, spawnSync } = require('child_process');
|
|
17
17
|
|
|
18
|
+
// Shared utilities
|
|
19
|
+
const { c, box } = require('../lib/colors');
|
|
20
|
+
const { getProjectRoot } = require('../lib/paths');
|
|
21
|
+
|
|
18
22
|
// Session manager path (relative to script location)
|
|
19
23
|
const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
|
|
20
24
|
|
|
@@ -26,68 +30,6 @@ try {
|
|
|
26
30
|
// Update checker not available
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
// ANSI color codes
|
|
30
|
-
const c = {
|
|
31
|
-
reset: '\x1b[0m',
|
|
32
|
-
bold: '\x1b[1m',
|
|
33
|
-
dim: '\x1b[2m',
|
|
34
|
-
|
|
35
|
-
// Standard ANSI colors
|
|
36
|
-
red: '\x1b[31m',
|
|
37
|
-
green: '\x1b[32m',
|
|
38
|
-
yellow: '\x1b[33m',
|
|
39
|
-
blue: '\x1b[34m',
|
|
40
|
-
magenta: '\x1b[35m',
|
|
41
|
-
cyan: '\x1b[36m',
|
|
42
|
-
|
|
43
|
-
brightBlack: '\x1b[90m',
|
|
44
|
-
brightGreen: '\x1b[92m',
|
|
45
|
-
brightYellow: '\x1b[93m',
|
|
46
|
-
brightCyan: '\x1b[96m',
|
|
47
|
-
|
|
48
|
-
// Vibrant 256-color palette (modern, sleek look)
|
|
49
|
-
mintGreen: '\x1b[38;5;158m', // Healthy/success states
|
|
50
|
-
peach: '\x1b[38;5;215m', // Warning states
|
|
51
|
-
coral: '\x1b[38;5;203m', // Critical/error states
|
|
52
|
-
lightGreen: '\x1b[38;5;194m', // Session healthy
|
|
53
|
-
lightYellow: '\x1b[38;5;228m', // Session warning
|
|
54
|
-
lightPink: '\x1b[38;5;210m', // Session critical
|
|
55
|
-
skyBlue: '\x1b[38;5;117m', // Directories/paths
|
|
56
|
-
lavender: '\x1b[38;5;147m', // Model info, story IDs
|
|
57
|
-
softGold: '\x1b[38;5;222m', // Cost/money
|
|
58
|
-
teal: '\x1b[38;5;80m', // Ready/pending states
|
|
59
|
-
slate: '\x1b[38;5;103m', // Secondary info
|
|
60
|
-
rose: '\x1b[38;5;211m', // Blocked/critical accent
|
|
61
|
-
amber: '\x1b[38;5;214m', // WIP/in-progress accent
|
|
62
|
-
powder: '\x1b[38;5;153m', // Labels/headers
|
|
63
|
-
|
|
64
|
-
// Brand color (#e8683a)
|
|
65
|
-
brand: '\x1b[38;2;232;104;58m',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Box drawing characters
|
|
69
|
-
const box = {
|
|
70
|
-
tl: '╭',
|
|
71
|
-
tr: '╮',
|
|
72
|
-
bl: '╰',
|
|
73
|
-
br: '╯',
|
|
74
|
-
h: '─',
|
|
75
|
-
v: '│',
|
|
76
|
-
lT: '├',
|
|
77
|
-
rT: '┤',
|
|
78
|
-
tT: '┬',
|
|
79
|
-
bT: '┴',
|
|
80
|
-
cross: '┼',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
function getProjectRoot() {
|
|
84
|
-
let dir = process.cwd();
|
|
85
|
-
while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
|
|
86
|
-
dir = path.dirname(dir);
|
|
87
|
-
}
|
|
88
|
-
return dir !== '/' ? dir : process.cwd();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
33
|
function getProjectInfo(rootDir) {
|
|
92
34
|
const info = {
|
|
93
35
|
name: 'agileflow',
|
|
@@ -346,6 +288,72 @@ function checkPreCompact(rootDir) {
|
|
|
346
288
|
return result;
|
|
347
289
|
}
|
|
348
290
|
|
|
291
|
+
function checkDamageControl(rootDir) {
|
|
292
|
+
const result = { configured: false, level: 'standard', patternCount: 0, scriptsOk: true };
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Check if PreToolUse hooks are configured in settings
|
|
296
|
+
const settingsPath = path.join(rootDir, '.claude/settings.json');
|
|
297
|
+
if (fs.existsSync(settingsPath)) {
|
|
298
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
299
|
+
if (settings.hooks?.PreToolUse && Array.isArray(settings.hooks.PreToolUse)) {
|
|
300
|
+
// Check for damage-control hooks
|
|
301
|
+
const hasDamageControlHooks = settings.hooks.PreToolUse.some(
|
|
302
|
+
h => h.hooks?.some(hk => hk.command?.includes('damage-control'))
|
|
303
|
+
);
|
|
304
|
+
if (hasDamageControlHooks) {
|
|
305
|
+
result.configured = true;
|
|
306
|
+
|
|
307
|
+
// Count how many hooks are present (should be 3: Bash, Edit, Write)
|
|
308
|
+
const dcHooks = settings.hooks.PreToolUse.filter(h =>
|
|
309
|
+
h.hooks?.some(hk => hk.command?.includes('damage-control'))
|
|
310
|
+
);
|
|
311
|
+
result.hooksCount = dcHooks.length;
|
|
312
|
+
|
|
313
|
+
// Check for enhanced mode (has prompt hook)
|
|
314
|
+
const hasPromptHook = settings.hooks.PreToolUse.some(
|
|
315
|
+
h => h.hooks?.some(hk => hk.type === 'prompt')
|
|
316
|
+
);
|
|
317
|
+
if (hasPromptHook) {
|
|
318
|
+
result.level = 'enhanced';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check if all required scripts exist (in .claude/hooks/damage-control/)
|
|
322
|
+
const hooksDir = path.join(rootDir, '.claude', 'hooks', 'damage-control');
|
|
323
|
+
const requiredScripts = [
|
|
324
|
+
'bash-tool-damage-control.js',
|
|
325
|
+
'edit-tool-damage-control.js',
|
|
326
|
+
'write-tool-damage-control.js',
|
|
327
|
+
];
|
|
328
|
+
for (const script of requiredScripts) {
|
|
329
|
+
if (!fs.existsSync(path.join(hooksDir, script))) {
|
|
330
|
+
result.scriptsOk = false;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Count patterns in patterns.yaml
|
|
339
|
+
const patternsLocations = [
|
|
340
|
+
path.join(rootDir, '.claude', 'hooks', 'damage-control', 'patterns.yaml'),
|
|
341
|
+
path.join(rootDir, '.agileflow', 'scripts', 'damage-control', 'patterns.yaml'),
|
|
342
|
+
];
|
|
343
|
+
for (const patternsPath of patternsLocations) {
|
|
344
|
+
if (fs.existsSync(patternsPath)) {
|
|
345
|
+
const content = fs.readFileSync(patternsPath, 'utf8');
|
|
346
|
+
// Count pattern entries (lines starting with " - pattern:")
|
|
347
|
+
const patternMatches = content.match(/^\s*-\s*pattern:/gm);
|
|
348
|
+
result.patternCount = patternMatches ? patternMatches.length : 0;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {}
|
|
353
|
+
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
349
357
|
// Compare semantic versions: returns -1 if a < b, 0 if equal, 1 if a > b
|
|
350
358
|
function compareVersions(a, b) {
|
|
351
359
|
if (!a || !b) return 0;
|
|
@@ -611,7 +619,8 @@ function formatTable(
|
|
|
611
619
|
precompact,
|
|
612
620
|
parallelSessions,
|
|
613
621
|
updateInfo = {},
|
|
614
|
-
expertise = {}
|
|
622
|
+
expertise = {},
|
|
623
|
+
damageControl = {}
|
|
615
624
|
) {
|
|
616
625
|
const W = 58; // inner width
|
|
617
626
|
const R = W - 24; // right column width (34 chars)
|
|
@@ -783,6 +792,20 @@ function formatTable(
|
|
|
783
792
|
}
|
|
784
793
|
}
|
|
785
794
|
|
|
795
|
+
// Damage control status (PreToolUse hooks for dangerous command protection)
|
|
796
|
+
if (damageControl && damageControl.configured) {
|
|
797
|
+
if (!damageControl.scriptsOk) {
|
|
798
|
+
lines.push(row('Damage control', '⚠️ scripts missing', c.coral, c.coral));
|
|
799
|
+
} else {
|
|
800
|
+
const levelStr = damageControl.level || 'standard';
|
|
801
|
+
const patternStr = damageControl.patternCount > 0 ? `${damageControl.patternCount} patterns` : '';
|
|
802
|
+
const dcStatus = `🛡️ ${levelStr}${patternStr ? ` (${patternStr})` : ''}`;
|
|
803
|
+
lines.push(row('Damage control', dcStatus, c.lavender, c.mintGreen));
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
lines.push(row('Damage control', 'not configured', c.slate, c.slate));
|
|
807
|
+
}
|
|
808
|
+
|
|
786
809
|
lines.push(divider());
|
|
787
810
|
|
|
788
811
|
// Current story (colorful like obtain-context)
|
|
@@ -816,6 +839,7 @@ async function main() {
|
|
|
816
839
|
const precompact = checkPreCompact(rootDir);
|
|
817
840
|
const parallelSessions = checkParallelSessions(rootDir);
|
|
818
841
|
const expertise = validateExpertise(rootDir);
|
|
842
|
+
const damageControl = checkDamageControl(rootDir);
|
|
819
843
|
|
|
820
844
|
// Check for updates (async, cached)
|
|
821
845
|
let updateInfo = {};
|
|
@@ -840,7 +864,7 @@ async function main() {
|
|
|
840
864
|
}
|
|
841
865
|
|
|
842
866
|
console.log(
|
|
843
|
-
formatTable(info, archival, session, precompact, parallelSessions, updateInfo, expertise)
|
|
867
|
+
formatTable(info, archival, session, precompact, parallelSessions, updateInfo, expertise, damageControl)
|
|
844
868
|
);
|
|
845
869
|
|
|
846
870
|
// Show warning and tip if other sessions are active (vibrant colors)
|
|
@@ -21,18 +21,10 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const { execSync } = require('child_process');
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
const c =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
dim: '\x1b[2m',
|
|
29
|
-
red: '\x1b[31m',
|
|
30
|
-
green: '\x1b[32m',
|
|
31
|
-
yellow: '\x1b[33m',
|
|
32
|
-
blue: '\x1b[34m',
|
|
33
|
-
cyan: '\x1b[36m',
|
|
34
|
-
brand: '\x1b[38;2;232;104;58m',
|
|
35
|
-
};
|
|
24
|
+
// Shared utilities
|
|
25
|
+
const { c } = require('../lib/colors');
|
|
26
|
+
const { getProjectRoot } = require('../lib/paths');
|
|
27
|
+
const { safeReadJSON, safeReadFile, safeWriteFile } = require('../lib/errors');
|
|
36
28
|
|
|
37
29
|
// Agents that have expertise files
|
|
38
30
|
const AGENTS_WITH_EXPERTISE = [
|
|
@@ -76,24 +68,11 @@ const DOMAIN_PATTERNS = {
|
|
|
76
68
|
devops: [/deploy/, /kubernetes/, /k8s/, /terraform/, /ansible/],
|
|
77
69
|
};
|
|
78
70
|
|
|
79
|
-
// Find project root
|
|
80
|
-
function getProjectRoot() {
|
|
81
|
-
let dir = process.cwd();
|
|
82
|
-
while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
|
|
83
|
-
dir = path.dirname(dir);
|
|
84
|
-
}
|
|
85
|
-
return dir !== '/' ? dir : process.cwd();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
71
|
// Read session state
|
|
89
72
|
function getSessionState(rootDir) {
|
|
90
73
|
const statePath = path.join(rootDir, 'docs/09-agents/session-state.json');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
94
|
-
}
|
|
95
|
-
} catch (e) {}
|
|
96
|
-
return {};
|
|
74
|
+
const result = safeReadJSON(statePath, { defaultValue: {} });
|
|
75
|
+
return result.ok ? result.data : {};
|
|
97
76
|
}
|
|
98
77
|
|
|
99
78
|
// Get git diff summary
|
|
@@ -232,26 +211,25 @@ function getExpertisePath(rootDir, agent) {
|
|
|
232
211
|
|
|
233
212
|
// Append learning to expertise file
|
|
234
213
|
function appendLearning(expertisePath, learning) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Find the learnings section
|
|
239
|
-
const learningsMatch = content.match(/^learnings:\s*$/m);
|
|
240
|
-
|
|
241
|
-
if (!learningsMatch) {
|
|
242
|
-
// No learnings section, add it at the end
|
|
243
|
-
content += `\n\nlearnings:\n${learning}`;
|
|
244
|
-
} else {
|
|
245
|
-
// Find where to insert (after "learnings:" line)
|
|
246
|
-
const insertPos = learningsMatch.index + learningsMatch[0].length;
|
|
247
|
-
content = content.slice(0, insertPos) + '\n' + learning + content.slice(insertPos);
|
|
248
|
-
}
|
|
214
|
+
const readResult = safeReadFile(expertisePath);
|
|
215
|
+
if (!readResult.ok) return false;
|
|
249
216
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
217
|
+
let content = readResult.data;
|
|
218
|
+
|
|
219
|
+
// Find the learnings section
|
|
220
|
+
const learningsMatch = content.match(/^learnings:\s*$/m);
|
|
221
|
+
|
|
222
|
+
if (!learningsMatch) {
|
|
223
|
+
// No learnings section, add it at the end
|
|
224
|
+
content += `\n\nlearnings:\n${learning}`;
|
|
225
|
+
} else {
|
|
226
|
+
// Find where to insert (after "learnings:" line)
|
|
227
|
+
const insertPos = learningsMatch.index + learningsMatch[0].length;
|
|
228
|
+
content = content.slice(0, insertPos) + '\n' + learning + content.slice(insertPos);
|
|
254
229
|
}
|
|
230
|
+
|
|
231
|
+
const writeResult = safeWriteFile(expertisePath, content);
|
|
232
|
+
return writeResult.ok;
|
|
255
233
|
}
|
|
256
234
|
|
|
257
235
|
// Format learning as YAML
|
package/scripts/check-update.js
CHANGED
|
@@ -24,6 +24,10 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const https = require('https');
|
|
26
26
|
|
|
27
|
+
// Shared utilities
|
|
28
|
+
const { getProjectRoot } = require('../lib/paths');
|
|
29
|
+
const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
|
|
30
|
+
|
|
27
31
|
// Debug mode
|
|
28
32
|
const DEBUG = process.env.DEBUG_UPDATE === '1';
|
|
29
33
|
|
|
@@ -33,35 +37,23 @@ function debugLog(message, data = null) {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
// Find project root (has .agileflow directory)
|
|
37
|
-
function getProjectRoot() {
|
|
38
|
-
let dir = process.cwd();
|
|
39
|
-
while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
|
|
40
|
-
dir = path.dirname(dir);
|
|
41
|
-
}
|
|
42
|
-
return dir !== '/' ? dir : process.cwd();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
// Get installed AgileFlow version
|
|
46
41
|
function getInstalledVersion(rootDir) {
|
|
47
42
|
// First check .agileflow/package.json (installed version)
|
|
48
43
|
const agileflowPkg = path.join(rootDir, '.agileflow', 'package.json');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
44
|
+
const agileflowResult = safeReadJSON(agileflowPkg);
|
|
45
|
+
if (agileflowResult.ok && agileflowResult.data?.version) {
|
|
46
|
+
return agileflowResult.data.version;
|
|
47
|
+
}
|
|
48
|
+
if (!agileflowResult.ok && agileflowResult.error) {
|
|
49
|
+
debugLog('Error reading .agileflow/package.json', agileflowResult.error);
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
// Fallback: check if this is the AgileFlow dev repo
|
|
59
53
|
const cliPkg = path.join(rootDir, 'packages/cli/package.json');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (pkg.name === 'agileflow' && pkg.version) return pkg.version;
|
|
64
|
-
} catch (e) {}
|
|
54
|
+
const cliResult = safeReadJSON(cliPkg);
|
|
55
|
+
if (cliResult.ok && cliResult.data?.name === 'agileflow' && cliResult.data?.version) {
|
|
56
|
+
return cliResult.data.version;
|
|
65
57
|
}
|
|
66
58
|
|
|
67
59
|
return null;
|
|
@@ -78,16 +70,16 @@ function getUpdateConfig(rootDir) {
|
|
|
78
70
|
latestVersion: null,
|
|
79
71
|
};
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
74
|
+
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
75
|
+
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
debugLog('Error reading update config', result.error);
|
|
78
|
+
return defaults;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.data?.updates) {
|
|
82
|
+
return { ...defaults, ...result.data.updates };
|
|
91
83
|
}
|
|
92
84
|
|
|
93
85
|
return defaults;
|
|
@@ -95,21 +87,22 @@ function getUpdateConfig(rootDir) {
|
|
|
95
87
|
|
|
96
88
|
// Save update configuration
|
|
97
89
|
function saveUpdateConfig(rootDir, config) {
|
|
98
|
-
|
|
99
|
-
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
100
|
-
let metadata = {};
|
|
90
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
101
91
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
// Read existing metadata
|
|
93
|
+
const readResult = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
94
|
+
const metadata = readResult.ok ? readResult.data : {};
|
|
95
|
+
|
|
96
|
+
// Update and write
|
|
97
|
+
metadata.updates = config;
|
|
98
|
+
const writeResult = safeWriteJSON(metadataPath, metadata, { createDir: true });
|
|
105
99
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return true;
|
|
109
|
-
} catch (e) {
|
|
110
|
-
debugLog('Error saving update config', e.message);
|
|
100
|
+
if (!writeResult.ok) {
|
|
101
|
+
debugLog('Error saving update config', writeResult.error);
|
|
111
102
|
return false;
|
|
112
103
|
}
|
|
104
|
+
|
|
105
|
+
return true;
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
// Check if cache is still valid
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bash-tool-damage-control.js - Validate bash commands against security patterns
|
|
5
|
+
*
|
|
6
|
+
* This PreToolUse hook runs before every Bash tool execution.
|
|
7
|
+
* It checks the command against patterns.yaml to block or ask for
|
|
8
|
+
* confirmation on dangerous commands.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = Allow command to proceed (or ask with JSON output)
|
|
12
|
+
* 2 = Block command
|
|
13
|
+
*
|
|
14
|
+
* For ask confirmation, outputs JSON:
|
|
15
|
+
* {"result": "ask", "message": "Reason for asking"}
|
|
16
|
+
*
|
|
17
|
+
* Usage (as PreToolUse hook):
|
|
18
|
+
* node .claude/hooks/damage-control/bash-tool-damage-control.js
|
|
19
|
+
*
|
|
20
|
+
* Environment:
|
|
21
|
+
* CLAUDE_TOOL_INPUT - JSON string with tool input (contains "command")
|
|
22
|
+
* CLAUDE_PROJECT_DIR - Project root directory
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
// ANSI colors for output
|
|
29
|
+
const c = {
|
|
30
|
+
reset: '\x1b[0m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
red: '\x1b[31m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
cyan: '\x1b[36m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Exit codes
|
|
38
|
+
const EXIT_ALLOW = 0;
|
|
39
|
+
const EXIT_BLOCK = 2;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load patterns from YAML file
|
|
43
|
+
* Falls back to built-in patterns if YAML parsing fails
|
|
44
|
+
*/
|
|
45
|
+
function loadPatterns(projectDir) {
|
|
46
|
+
const locations = [
|
|
47
|
+
path.join(projectDir, '.claude/hooks/damage-control/patterns.yaml'),
|
|
48
|
+
path.join(projectDir, '.agileflow/hooks/damage-control/patterns.yaml'),
|
|
49
|
+
path.join(projectDir, 'patterns.yaml'),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (const loc of locations) {
|
|
53
|
+
if (fs.existsSync(loc)) {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(loc, 'utf8');
|
|
56
|
+
// Simple YAML parsing for our specific structure
|
|
57
|
+
return parseSimpleYaml(content);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`Warning: Could not parse ${loc}: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Return built-in defaults if no file found
|
|
65
|
+
return getDefaultPatterns();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Simple YAML parser for patterns.yaml structure
|
|
70
|
+
* Only handles the specific structure we use (arrays of objects with pattern/reason/ask)
|
|
71
|
+
*/
|
|
72
|
+
function parseSimpleYaml(content) {
|
|
73
|
+
const patterns = {
|
|
74
|
+
bashToolPatterns: [],
|
|
75
|
+
askPatterns: [],
|
|
76
|
+
agileflowPatterns: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let currentSection = null;
|
|
80
|
+
let currentItem = null;
|
|
81
|
+
|
|
82
|
+
const lines = content.split('\n');
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
// Skip comments and empty lines
|
|
86
|
+
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
87
|
+
|
|
88
|
+
// Check for section headers
|
|
89
|
+
if (line.match(/^bashToolPatterns:/)) {
|
|
90
|
+
currentSection = 'bashToolPatterns';
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (line.match(/^askPatterns:/)) {
|
|
94
|
+
currentSection = 'askPatterns';
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (line.match(/^agileflowPatterns:/)) {
|
|
98
|
+
currentSection = 'agileflowPatterns';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (line.match(/^(zeroAccessPaths|readOnlyPaths|noDeletePaths|config):/)) {
|
|
102
|
+
currentSection = null; // Skip non-pattern sections
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parse pattern items
|
|
107
|
+
if (currentSection && patterns[currentSection]) {
|
|
108
|
+
const patternMatch = line.match(/^\s+-\s*pattern:\s*['"]?(.+?)['"]?\s*$/);
|
|
109
|
+
if (patternMatch) {
|
|
110
|
+
currentItem = { pattern: patternMatch[1] };
|
|
111
|
+
patterns[currentSection].push(currentItem);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const reasonMatch = line.match(/^\s+reason:\s*['"]?(.+?)['"]?\s*$/);
|
|
116
|
+
if (reasonMatch && currentItem) {
|
|
117
|
+
currentItem.reason = reasonMatch[1];
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const askMatch = line.match(/^\s+ask:\s*(true|false)\s*$/);
|
|
122
|
+
if (askMatch && currentItem) {
|
|
123
|
+
currentItem.ask = askMatch[1] === 'true';
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return patterns;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Built-in default patterns (used if patterns.yaml not found)
|
|
134
|
+
*/
|
|
135
|
+
function getDefaultPatterns() {
|
|
136
|
+
return {
|
|
137
|
+
bashToolPatterns: [
|
|
138
|
+
{ pattern: '\\brm\\s+-[rRf]', reason: 'rm with recursive or force flags' },
|
|
139
|
+
{ pattern: 'DROP\\s+(TABLE|DATABASE)', reason: 'DROP commands are destructive' },
|
|
140
|
+
{ pattern: 'DELETE\\s+FROM\\s+\\w+\\s*;', reason: 'DELETE without WHERE clause' },
|
|
141
|
+
{ pattern: 'TRUNCATE\\s+(TABLE\\s+)?\\w+', reason: 'TRUNCATE removes all data' },
|
|
142
|
+
{ pattern: 'git\\s+push\\s+.*--force', reason: 'Force push can overwrite history', ask: true },
|
|
143
|
+
{ pattern: 'git\\s+reset\\s+--hard', reason: 'Hard reset discards changes', ask: true },
|
|
144
|
+
],
|
|
145
|
+
askPatterns: [
|
|
146
|
+
{ pattern: 'DELETE\\s+FROM\\s+\\w+\\s+WHERE', reason: 'Confirm record deletion' },
|
|
147
|
+
{ pattern: 'npm\\s+publish', reason: 'Publishing to npm is permanent' },
|
|
148
|
+
],
|
|
149
|
+
agileflowPatterns: [
|
|
150
|
+
{ pattern: 'rm.*\\.agileflow', reason: 'Deleting .agileflow breaks installation' },
|
|
151
|
+
{ pattern: 'rm.*\\.claude', reason: 'Deleting .claude breaks configuration' },
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check command against patterns
|
|
158
|
+
* Returns: { blocked: boolean, ask: boolean, reason: string }
|
|
159
|
+
*/
|
|
160
|
+
function checkCommand(command, patterns) {
|
|
161
|
+
// Combine all pattern sources
|
|
162
|
+
const allPatterns = [
|
|
163
|
+
...(patterns.bashToolPatterns || []),
|
|
164
|
+
...(patterns.agileflowPatterns || []),
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
// Check block/ask patterns
|
|
168
|
+
for (const p of allPatterns) {
|
|
169
|
+
try {
|
|
170
|
+
const regex = new RegExp(p.pattern, 'i');
|
|
171
|
+
if (regex.test(command)) {
|
|
172
|
+
if (p.ask) {
|
|
173
|
+
return { blocked: false, ask: true, reason: p.reason };
|
|
174
|
+
}
|
|
175
|
+
return { blocked: true, ask: false, reason: p.reason };
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
// Invalid regex, skip
|
|
179
|
+
console.error(`Warning: Invalid regex pattern: ${p.pattern}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check ask-only patterns
|
|
184
|
+
for (const p of (patterns.askPatterns || [])) {
|
|
185
|
+
try {
|
|
186
|
+
const regex = new RegExp(p.pattern, 'i');
|
|
187
|
+
if (regex.test(command)) {
|
|
188
|
+
return { blocked: false, ask: true, reason: p.reason };
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// Invalid regex, skip
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { blocked: false, ask: false, reason: null };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Main entry point
|
|
200
|
+
*/
|
|
201
|
+
function main() {
|
|
202
|
+
// Get tool input from environment
|
|
203
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT;
|
|
204
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
205
|
+
|
|
206
|
+
if (!toolInput) {
|
|
207
|
+
// No input, allow by default
|
|
208
|
+
process.exit(EXIT_ALLOW);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let input;
|
|
212
|
+
try {
|
|
213
|
+
input = JSON.parse(toolInput);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error('Error parsing CLAUDE_TOOL_INPUT:', e.message);
|
|
216
|
+
process.exit(EXIT_ALLOW);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const command = input.command;
|
|
220
|
+
if (!command) {
|
|
221
|
+
process.exit(EXIT_ALLOW);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Load patterns
|
|
225
|
+
const patterns = loadPatterns(projectDir);
|
|
226
|
+
|
|
227
|
+
// Check command
|
|
228
|
+
const result = checkCommand(command, patterns);
|
|
229
|
+
|
|
230
|
+
if (result.blocked) {
|
|
231
|
+
// Block the command
|
|
232
|
+
console.error(`${c.red}${c.bold}BLOCKED${c.reset}: ${result.reason}`);
|
|
233
|
+
console.error(`${c.yellow}Command: ${command}${c.reset}`);
|
|
234
|
+
console.error(`${c.cyan}This command was blocked by damage control.${c.reset}`);
|
|
235
|
+
process.exit(EXIT_BLOCK);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (result.ask) {
|
|
239
|
+
// Ask for confirmation
|
|
240
|
+
const response = {
|
|
241
|
+
result: 'ask',
|
|
242
|
+
message: `${result.reason}\n\nCommand: ${command}\n\nProceed with this command?`,
|
|
243
|
+
};
|
|
244
|
+
console.log(JSON.stringify(response));
|
|
245
|
+
process.exit(EXIT_ALLOW);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Allow the command
|
|
249
|
+
process.exit(EXIT_ALLOW);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Run if called directly
|
|
253
|
+
if (require.main === module) {
|
|
254
|
+
main();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = { checkCommand, loadPatterns, parseSimpleYaml };
|