agileflow 2.90.7 → 2.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +818 -0
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate-names.js +3 -3
- package/lib/validate.js +116 -52
- package/package.json +4 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +118 -1048
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +36 -11
- package/scripts/query-codebase.js +538 -0
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +180 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +101 -0
- package/scripts/tui/blessed/panels/sessions.js +150 -0
- package/scripts/tui/blessed/panels/trace.js +97 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +47 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -30
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +239 -0
- package/scripts/validators/json-schema-validator.js +186 -0
- package/scripts/validators/markdown-validator.js +152 -0
- package/scripts/validators/migration-validator.js +129 -0
- package/scripts/validators/security-validator.js +380 -0
- package/scripts/validators/story-format-validator.js +197 -0
- package/scripts/validators/test-result-validator.js +114 -0
- package/scripts/validators/workflow-validator.js +247 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +261 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +205 -1
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +76 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +200 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Component Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates React/Vue/Svelte component files for common issues.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Success
|
|
9
|
+
* 2 = Error (Claude will attempt to fix)
|
|
10
|
+
* 1 = Warning (logged but not blocking)
|
|
11
|
+
*
|
|
12
|
+
* Usage in agent hooks:
|
|
13
|
+
* hooks:
|
|
14
|
+
* PostToolUse:
|
|
15
|
+
* - matcher: "Write"
|
|
16
|
+
* hooks:
|
|
17
|
+
* - type: command
|
|
18
|
+
* command: "node .agileflow/hooks/validators/component-validator.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
const context = JSON.parse(input);
|
|
29
|
+
const filePath = context.tool_input?.file_path;
|
|
30
|
+
|
|
31
|
+
// Only validate component files
|
|
32
|
+
if (!filePath || !isComponentFile(filePath)) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip if file doesn't exist
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
console.log(`File not found: ${filePath} (skipping validation)`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const issues = validateComponent(filePath);
|
|
43
|
+
|
|
44
|
+
if (issues.length > 0) {
|
|
45
|
+
console.error(`Fix these component issues in ${filePath}:`);
|
|
46
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
47
|
+
process.exit(2); // Claude will fix
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`Component validation passed: ${filePath}`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`Validator error: ${e.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function isComponentFile(filePath) {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
const componentExtensions = ['.jsx', '.tsx', '.vue', '.svelte'];
|
|
61
|
+
|
|
62
|
+
// Also check for .js/.ts files in component directories
|
|
63
|
+
if (['.js', '.ts'].includes(ext)) {
|
|
64
|
+
const normalizedPath = filePath.toLowerCase();
|
|
65
|
+
return (
|
|
66
|
+
normalizedPath.includes('/components/') ||
|
|
67
|
+
normalizedPath.includes('/pages/') ||
|
|
68
|
+
normalizedPath.includes('/views/')
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return componentExtensions.includes(ext);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateComponent(filePath) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
80
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
81
|
+
const fileName = path.basename(filePath, ext);
|
|
82
|
+
|
|
83
|
+
// Check for empty component
|
|
84
|
+
if (!content.trim()) {
|
|
85
|
+
issues.push('Component file is empty');
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// React/JSX/TSX validation
|
|
90
|
+
if (['.jsx', '.tsx'].includes(ext) || content.includes('React')) {
|
|
91
|
+
issues.push(...validateReactComponent(content, fileName));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Vue validation
|
|
95
|
+
if (ext === '.vue') {
|
|
96
|
+
issues.push(...validateVueComponent(content));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Svelte validation
|
|
100
|
+
if (ext === '.svelte') {
|
|
101
|
+
issues.push(...validateSvelteComponent(content));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// General accessibility checks
|
|
105
|
+
issues.push(...validateAccessibility(content));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
issues.push(`Read error: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateReactComponent(content, fileName) {
|
|
114
|
+
const issues = [];
|
|
115
|
+
|
|
116
|
+
// Check for component export
|
|
117
|
+
if (
|
|
118
|
+
!content.includes('export default') &&
|
|
119
|
+
!content.includes('export function') &&
|
|
120
|
+
!content.includes('export const')
|
|
121
|
+
) {
|
|
122
|
+
issues.push(
|
|
123
|
+
'Component should have an export (export default, export function, or export const)'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for React import in JSX files
|
|
128
|
+
if (
|
|
129
|
+
content.includes('React.') &&
|
|
130
|
+
!content.includes("from 'react'") &&
|
|
131
|
+
!content.includes('from "react"')
|
|
132
|
+
) {
|
|
133
|
+
issues.push('Using React. prefix but React is not imported');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for proper function/component naming (should match filename for default exports)
|
|
137
|
+
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
138
|
+
if (!pascalCaseRegex.test(fileName) && !fileName.startsWith('use')) {
|
|
139
|
+
// Only warn, don't block - could be a utility file
|
|
140
|
+
console.log(`Note: Component filename "${fileName}" should be PascalCase`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for inline styles (prefer CSS modules or styled-components)
|
|
144
|
+
const inlineStyleCount = (content.match(/style=\{\{/g) || []).length;
|
|
145
|
+
if (inlineStyleCount > 5) {
|
|
146
|
+
issues.push(
|
|
147
|
+
`Too many inline styles (${inlineStyleCount}) - consider using CSS modules or styled-components`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for console.log in production code
|
|
152
|
+
if (
|
|
153
|
+
content.includes('console.log') &&
|
|
154
|
+
!content.includes('// debug') &&
|
|
155
|
+
!content.includes('// DEBUG')
|
|
156
|
+
) {
|
|
157
|
+
console.log("Note: console.log found - ensure it's removed before production");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for missing key prop in map
|
|
161
|
+
if (content.includes('.map(') && content.includes('<') && !content.includes('key=')) {
|
|
162
|
+
issues.push('Array .map() rendering elements should include key prop');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return issues;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateVueComponent(content) {
|
|
169
|
+
const issues = [];
|
|
170
|
+
|
|
171
|
+
// Check for required template section
|
|
172
|
+
if (!content.includes('<template>') && !content.includes('<template ')) {
|
|
173
|
+
issues.push('Vue component must have a <template> section');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check for script section
|
|
177
|
+
if (!content.includes('<script') && !content.includes('<script>')) {
|
|
178
|
+
// Not strictly required but recommended
|
|
179
|
+
console.log('Note: Vue component has no <script> section');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for scoped styles (recommended)
|
|
183
|
+
if (
|
|
184
|
+
content.includes('<style>') &&
|
|
185
|
+
!content.includes('<style scoped') &&
|
|
186
|
+
!content.includes('scoped>')
|
|
187
|
+
) {
|
|
188
|
+
console.log('Note: Consider using scoped styles to prevent CSS leaks');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return issues;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateSvelteComponent(content) {
|
|
195
|
+
const issues = [];
|
|
196
|
+
|
|
197
|
+
// Check for script section
|
|
198
|
+
if (!content.includes('<script') && !content.includes('<script>')) {
|
|
199
|
+
console.log('Note: Svelte component has no <script> section');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return issues;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validateAccessibility(content) {
|
|
206
|
+
const issues = [];
|
|
207
|
+
|
|
208
|
+
// Check for images without alt
|
|
209
|
+
const imgWithoutAlt = content.match(/<img[^>]*(?!alt=)[^>]*>/gi) || [];
|
|
210
|
+
const imgWithEmptyAlt = content.match(/<img[^>]*alt=["']["'][^>]*>/gi) || [];
|
|
211
|
+
|
|
212
|
+
if (imgWithoutAlt.length > 0) {
|
|
213
|
+
issues.push(`Found ${imgWithoutAlt.length} <img> tag(s) without alt attribute`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for button without type
|
|
217
|
+
if (content.includes('<button') && !content.includes('type=')) {
|
|
218
|
+
console.log('Note: <button> elements should have explicit type attribute');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check for click handlers on non-interactive elements
|
|
222
|
+
const clickOnDiv = content.match(/onClick[^>]*>[^<]*<\/div>/gi) || [];
|
|
223
|
+
if (clickOnDiv.length > 0) {
|
|
224
|
+
issues.push(
|
|
225
|
+
'onClick on <div> detected - use <button> for interactive elements (accessibility)'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for form inputs without labels
|
|
230
|
+
if (
|
|
231
|
+
content.includes('<input') &&
|
|
232
|
+
!content.includes('<label') &&
|
|
233
|
+
!content.includes('aria-label')
|
|
234
|
+
) {
|
|
235
|
+
console.log('Note: <input> elements should have associated <label> or aria-label');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return issues;
|
|
239
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* JSON Schema Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates JSON files for proper structure after Write operations.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Success
|
|
9
|
+
* 2 = Error (Claude will attempt to fix)
|
|
10
|
+
* 1 = Warning (logged but not blocking)
|
|
11
|
+
*
|
|
12
|
+
* Usage in agent hooks:
|
|
13
|
+
* hooks:
|
|
14
|
+
* PostToolUse:
|
|
15
|
+
* - matcher: "Write"
|
|
16
|
+
* hooks:
|
|
17
|
+
* - type: command
|
|
18
|
+
* command: "node .agileflow/hooks/validators/json-schema-validator.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Import status constants from single source of truth
|
|
25
|
+
const { VALID_STATUSES } = require('../lib/story-state-machine');
|
|
26
|
+
|
|
27
|
+
// Extended statuses for backward compatibility (maps to canonical values)
|
|
28
|
+
// These legacy values are accepted but should be migrated to canonical values
|
|
29
|
+
const LEGACY_STATUSES = ['pending', 'done', 'in-progress', 'in-review'];
|
|
30
|
+
const ALL_ACCEPTED_STATUSES = [...VALID_STATUSES, ...LEGACY_STATUSES];
|
|
31
|
+
|
|
32
|
+
let input = '';
|
|
33
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
34
|
+
process.stdin.on('end', () => {
|
|
35
|
+
try {
|
|
36
|
+
const context = JSON.parse(input);
|
|
37
|
+
const filePath = context.tool_input?.file_path;
|
|
38
|
+
|
|
39
|
+
// Only validate JSON files
|
|
40
|
+
if (!filePath || !filePath.endsWith('.json')) {
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Skip if file doesn't exist (might be a create failure)
|
|
45
|
+
if (!fs.existsSync(filePath)) {
|
|
46
|
+
console.log(`File not found: ${filePath} (skipping validation)`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const issues = validateJson(filePath);
|
|
51
|
+
|
|
52
|
+
if (issues.length > 0) {
|
|
53
|
+
console.error(`Resolve these JSON issues in ${filePath}:`);
|
|
54
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
55
|
+
process.exit(2); // Claude will fix
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`JSON validation passed: ${filePath}`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`Validator error: ${e.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function validateJson(filePath) {
|
|
67
|
+
const issues = [];
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
|
|
72
|
+
// Check for empty file
|
|
73
|
+
if (!content.trim()) {
|
|
74
|
+
issues.push('File is empty');
|
|
75
|
+
return issues;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try to parse JSON
|
|
79
|
+
const data = JSON.parse(content);
|
|
80
|
+
|
|
81
|
+
// Check for common JSON issues
|
|
82
|
+
if (typeof data !== 'object' || data === null) {
|
|
83
|
+
issues.push('Root must be an object or array');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Specific checks for known files
|
|
87
|
+
const fileName = path.basename(filePath);
|
|
88
|
+
|
|
89
|
+
if (fileName === 'status.json') {
|
|
90
|
+
issues.push(...validateStatusJson(data));
|
|
91
|
+
} else if (fileName === 'package.json') {
|
|
92
|
+
issues.push(...validatePackageJson(data));
|
|
93
|
+
} else if (fileName === 'tsconfig.json') {
|
|
94
|
+
issues.push(...validateTsConfig(data));
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (e instanceof SyntaxError) {
|
|
98
|
+
issues.push(`Invalid JSON syntax: ${e.message}`);
|
|
99
|
+
} else {
|
|
100
|
+
issues.push(`Read error: ${e.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return issues;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateStatusJson(data) {
|
|
108
|
+
const issues = [];
|
|
109
|
+
|
|
110
|
+
// Check for required structure - status.json has epics object with embedded stories
|
|
111
|
+
if (!data.epics && !data.current_story && !data.updated) {
|
|
112
|
+
issues.push('status.json should have epics, current_story, or updated field');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate epics object if present
|
|
116
|
+
if (data.epics) {
|
|
117
|
+
if (typeof data.epics !== 'object' || Array.isArray(data.epics)) {
|
|
118
|
+
issues.push('epics must be an object (not array)');
|
|
119
|
+
} else {
|
|
120
|
+
Object.entries(data.epics).forEach(([epicId, epic]) => {
|
|
121
|
+
if (!epic.title) {
|
|
122
|
+
issues.push(`Epic ${epicId} missing 'title' field`);
|
|
123
|
+
}
|
|
124
|
+
if (epic.stories && !Array.isArray(epic.stories)) {
|
|
125
|
+
issues.push(`Epic ${epicId} stories must be an array`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate stories - can be object (map by ID) or array
|
|
132
|
+
if (data.stories) {
|
|
133
|
+
if (Array.isArray(data.stories)) {
|
|
134
|
+
// Array format
|
|
135
|
+
data.stories.forEach((story, index) => {
|
|
136
|
+
if (!story.id) {
|
|
137
|
+
issues.push(`Story at index ${index} missing required 'id' field`);
|
|
138
|
+
}
|
|
139
|
+
if (!story.title && !story.name) {
|
|
140
|
+
issues.push(`Story ${story.id || index} missing 'title' or 'name' field`);
|
|
141
|
+
}
|
|
142
|
+
if (story.status && !ALL_ACCEPTED_STATUSES.includes(story.status)) {
|
|
143
|
+
issues.push(`Story ${story.id || index} has invalid status: ${story.status}`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} else if (typeof data.stories === 'object') {
|
|
147
|
+
// Object/map format (keyed by story ID)
|
|
148
|
+
Object.entries(data.stories).forEach(([storyId, story]) => {
|
|
149
|
+
if (!story.title && !story.name) {
|
|
150
|
+
issues.push(`Story ${storyId} missing 'title' or 'name' field`);
|
|
151
|
+
}
|
|
152
|
+
if (story.status && !ALL_ACCEPTED_STATUSES.includes(story.status)) {
|
|
153
|
+
issues.push(`Story ${storyId} has invalid status: ${story.status}`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return issues;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validatePackageJson(data) {
|
|
163
|
+
const issues = [];
|
|
164
|
+
|
|
165
|
+
if (!data.name) {
|
|
166
|
+
issues.push('package.json requires "name" field');
|
|
167
|
+
}
|
|
168
|
+
if (!data.version) {
|
|
169
|
+
issues.push('package.json requires "version" field');
|
|
170
|
+
}
|
|
171
|
+
if (data.version && !/^\d+\.\d+\.\d+/.test(data.version)) {
|
|
172
|
+
issues.push(`Invalid version format: ${data.version} (expected semver)`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return issues;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateTsConfig(data) {
|
|
179
|
+
const issues = [];
|
|
180
|
+
|
|
181
|
+
if (!data.compilerOptions) {
|
|
182
|
+
issues.push('tsconfig.json should have compilerOptions');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return issues;
|
|
186
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Markdown Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates markdown files for proper structure after Write operations.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Success
|
|
9
|
+
* 2 = Error (Claude will attempt to fix)
|
|
10
|
+
* 1 = Warning (logged but not blocking)
|
|
11
|
+
*
|
|
12
|
+
* Usage in agent hooks:
|
|
13
|
+
* hooks:
|
|
14
|
+
* PostToolUse:
|
|
15
|
+
* - matcher: "Write"
|
|
16
|
+
* hooks:
|
|
17
|
+
* - type: command
|
|
18
|
+
* command: "node .agileflow/hooks/validators/markdown-validator.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
const context = JSON.parse(input);
|
|
29
|
+
const filePath = context.tool_input?.file_path;
|
|
30
|
+
|
|
31
|
+
// Only validate markdown files
|
|
32
|
+
if (!filePath || !filePath.endsWith('.md')) {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip if file doesn't exist
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
console.log(`File not found: ${filePath} (skipping validation)`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const issues = validateMarkdown(filePath);
|
|
43
|
+
|
|
44
|
+
if (issues.length > 0) {
|
|
45
|
+
console.error(`Resolve these markdown issues in ${filePath}:`);
|
|
46
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
47
|
+
process.exit(2); // Claude will fix
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`Markdown validation passed: ${filePath}`);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`Validator error: ${e.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function validateMarkdown(filePath) {
|
|
59
|
+
const issues = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
63
|
+
const lines = content.split('\n');
|
|
64
|
+
const fileName = path.basename(filePath);
|
|
65
|
+
|
|
66
|
+
// Check for empty file
|
|
67
|
+
if (!content.trim()) {
|
|
68
|
+
issues.push('File is empty');
|
|
69
|
+
return issues;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for title (first line should be # heading)
|
|
73
|
+
const firstContentLine = lines.find(l => l.trim());
|
|
74
|
+
if (!firstContentLine?.startsWith('#')) {
|
|
75
|
+
issues.push('Document should start with a heading (#)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for broken links (basic check)
|
|
79
|
+
const brokenLinkPattern = /\[([^\]]*)\]\(\s*\)/g;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = brokenLinkPattern.exec(content)) !== null) {
|
|
82
|
+
issues.push(`Empty link found: [${match[1]}]()`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for unclosed code blocks
|
|
86
|
+
const codeBlockCount = (content.match(/```/g) || []).length;
|
|
87
|
+
if (codeBlockCount % 2 !== 0) {
|
|
88
|
+
issues.push('Unclosed code block (odd number of ``` markers)');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for heading hierarchy issues
|
|
92
|
+
let lastHeadingLevel = 0;
|
|
93
|
+
lines.forEach((line, index) => {
|
|
94
|
+
const headingMatch = line.match(/^(#{1,6})\s/);
|
|
95
|
+
if (headingMatch) {
|
|
96
|
+
const level = headingMatch[1].length;
|
|
97
|
+
// Skip from level 0 to level 1 is OK
|
|
98
|
+
if (lastHeadingLevel > 0 && level > lastHeadingLevel + 1) {
|
|
99
|
+
issues.push(`Line ${index + 1}: Heading level jump (h${lastHeadingLevel} to h${level})`);
|
|
100
|
+
}
|
|
101
|
+
lastHeadingLevel = level;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Check for specific file types
|
|
106
|
+
if (fileName.startsWith('adr-') || fileName.startsWith('ADR-')) {
|
|
107
|
+
issues.push(...validateADR(content));
|
|
108
|
+
} else if (filePath.includes('/10-research/')) {
|
|
109
|
+
issues.push(...validateResearchNote(content));
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
issues.push(`Read error: ${e.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return issues;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateADR(content) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
|
|
121
|
+
const requiredSections = ['## Context', '## Decision', '## Consequences'];
|
|
122
|
+
requiredSections.forEach(section => {
|
|
123
|
+
if (!content.includes(section)) {
|
|
124
|
+
issues.push(`ADR missing required section: ${section}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!content.includes('**Status**:')) {
|
|
129
|
+
issues.push('ADR missing Status field');
|
|
130
|
+
}
|
|
131
|
+
if (!content.includes('**Date**:')) {
|
|
132
|
+
issues.push('ADR missing Date field');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return issues;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateResearchNote(content) {
|
|
139
|
+
const issues = [];
|
|
140
|
+
|
|
141
|
+
if (!content.includes('**Import Date**:')) {
|
|
142
|
+
issues.push('Research note missing Import Date');
|
|
143
|
+
}
|
|
144
|
+
if (!content.includes('## Summary')) {
|
|
145
|
+
issues.push('Research note missing Summary section');
|
|
146
|
+
}
|
|
147
|
+
if (!content.includes('## Key Findings')) {
|
|
148
|
+
issues.push('Research note missing Key Findings section');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return issues;
|
|
152
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migration Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates database migration commands for proper patterns.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Success
|
|
9
|
+
* 2 = Error (Claude will attempt to fix)
|
|
10
|
+
* 1 = Warning (logged but not blocking)
|
|
11
|
+
*
|
|
12
|
+
* Usage in agent hooks:
|
|
13
|
+
* hooks:
|
|
14
|
+
* PostToolUse:
|
|
15
|
+
* - matcher: "Bash"
|
|
16
|
+
* hooks:
|
|
17
|
+
* - type: command
|
|
18
|
+
* command: "node .agileflow/hooks/validators/migration-validator.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let input = '';
|
|
22
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
23
|
+
process.stdin.on('end', () => {
|
|
24
|
+
try {
|
|
25
|
+
const context = JSON.parse(input);
|
|
26
|
+
const command = context.tool_input?.command;
|
|
27
|
+
const result = context.result || '';
|
|
28
|
+
|
|
29
|
+
// Only validate migration-related commands
|
|
30
|
+
if (!command || !isMigrationCommand(command)) {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const issues = validateMigration(command, result);
|
|
35
|
+
|
|
36
|
+
if (issues.length > 0) {
|
|
37
|
+
console.error(`Migration validation issues:`);
|
|
38
|
+
issues.forEach(i => console.error(` - ${i}`));
|
|
39
|
+
process.exit(2); // Claude will fix
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Migration validation passed: ${command.substring(0, 50)}...`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error(`Validator error: ${e.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function isMigrationCommand(command) {
|
|
51
|
+
const migrationPatterns = [
|
|
52
|
+
/prisma\s+migrate/i,
|
|
53
|
+
/drizzle-kit/i,
|
|
54
|
+
/typeorm\s+migration/i,
|
|
55
|
+
/knex\s+migrate/i,
|
|
56
|
+
/sequelize\s+db:migrate/i,
|
|
57
|
+
/alembic/i,
|
|
58
|
+
/flyway/i,
|
|
59
|
+
/liquibase/i,
|
|
60
|
+
/rails\s+db:migrate/i,
|
|
61
|
+
/django.*migrate/i,
|
|
62
|
+
/psql.*-f.*\.sql/i,
|
|
63
|
+
/mysql.*<.*\.sql/i,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return migrationPatterns.some(pattern => pattern.test(command));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateMigration(command, result) {
|
|
70
|
+
const issues = [];
|
|
71
|
+
|
|
72
|
+
// Check for destructive operations without safeguards
|
|
73
|
+
const destructivePatterns = [
|
|
74
|
+
{
|
|
75
|
+
pattern: /DROP\s+TABLE/i,
|
|
76
|
+
message: 'DROP TABLE detected - ensure backup exists and this is intentional',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /DROP\s+DATABASE/i,
|
|
80
|
+
message: 'DROP DATABASE detected - this is extremely destructive!',
|
|
81
|
+
},
|
|
82
|
+
{ pattern: /TRUNCATE/i, message: 'TRUNCATE detected - this removes all data permanently' },
|
|
83
|
+
{
|
|
84
|
+
pattern: /DELETE\s+FROM.*WHERE\s*$/i,
|
|
85
|
+
message: 'DELETE without WHERE clause detected - will delete all rows',
|
|
86
|
+
},
|
|
87
|
+
{ pattern: /--force|--skip-safe/i, message: 'Force flag used - bypassing safety checks' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const { pattern, message } of destructivePatterns) {
|
|
91
|
+
if (pattern.test(command) || pattern.test(result)) {
|
|
92
|
+
issues.push(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check for production environment safeguards
|
|
97
|
+
if (/production|prod/i.test(command) && !/--dry-run/i.test(command)) {
|
|
98
|
+
issues.push('Production migration without --dry-run flag - consider dry run first');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for failed migrations in output
|
|
102
|
+
const failurePatterns = [
|
|
103
|
+
{ pattern: /error.*migration/i, message: 'Migration error detected in output' },
|
|
104
|
+
{ pattern: /rollback.*failed/i, message: 'Rollback failure detected' },
|
|
105
|
+
{ pattern: /constraint.*violation/i, message: 'Database constraint violation' },
|
|
106
|
+
{
|
|
107
|
+
pattern: /duplicate.*key/i,
|
|
108
|
+
message: 'Duplicate key error - migration may have partially applied',
|
|
109
|
+
},
|
|
110
|
+
{ pattern: /already exists/i, message: 'Object already exists - migration may need cleanup' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const { pattern, message } of failurePatterns) {
|
|
114
|
+
if (pattern.test(result)) {
|
|
115
|
+
issues.push(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for missing rollback strategy
|
|
120
|
+
if (/migrate.*dev|migrate.*run/i.test(command)) {
|
|
121
|
+
// Prisma and similar ORMs
|
|
122
|
+
if (!/down|rollback|reset/i.test(result) && result.includes('migration')) {
|
|
123
|
+
// Just a warning, not blocking
|
|
124
|
+
console.log('Note: Ensure rollback migration exists for this change');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return issues;
|
|
129
|
+
}
|