agileflow 3.3.0 → 3.4.1
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/skill-loader.js +0 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +81 -0
- package/scripts/agileflow-welcome.js +79 -0
- package/scripts/claude-tmux.sh +90 -23
- package/scripts/claude-watchdog.sh +225 -0
- package/scripts/generators/agent-registry.js +14 -1
- package/scripts/generators/inject-babysit.js +22 -9
- package/scripts/generators/inject-help.js +19 -9
- package/scripts/lib/ac-test-matcher.js +452 -0
- package/scripts/lib/audit-cleanup.js +250 -0
- package/scripts/lib/audit-registry.js +304 -0
- package/scripts/lib/configure-features.js +35 -0
- package/scripts/lib/feature-catalog.js +3 -3
- package/scripts/lib/gate-enforcer.js +295 -0
- package/scripts/lib/model-profiles.js +118 -0
- package/scripts/lib/quality-gates.js +163 -0
- package/scripts/lib/signal-detectors.js +44 -1
- package/scripts/lib/skill-catalog.js +557 -0
- package/scripts/lib/skill-recommender.js +311 -0
- package/scripts/lib/status-writer.js +255 -0
- package/scripts/lib/story-claiming.js +128 -45
- package/scripts/lib/task-sync.js +32 -38
- package/scripts/lib/tdd-phase-manager.js +455 -0
- package/scripts/lib/team-events.js +34 -3
- package/scripts/lib/tmux-audit-monitor.js +611 -0
- package/scripts/lib/tmux-group-colors.js +113 -0
- package/scripts/lib/tool-registry.yaml +241 -0
- package/scripts/lib/tool-shed.js +441 -0
- package/scripts/messaging-bridge.js +209 -1
- package/scripts/native-team-observer.js +219 -0
- package/scripts/obtain-context.js +14 -0
- package/scripts/ralph-loop.js +30 -5
- package/scripts/smart-detect.js +21 -0
- package/scripts/spawn-audit-sessions.js +877 -0
- package/scripts/team-manager.js +56 -16
- package/scripts/tmux-close-windows.sh +180 -0
- package/src/core/agents/a11y-analyzer-aria.md +155 -0
- package/src/core/agents/a11y-analyzer-forms.md +162 -0
- package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
- package/src/core/agents/a11y-analyzer-semantic.md +153 -0
- package/src/core/agents/a11y-analyzer-visual.md +158 -0
- package/src/core/agents/a11y-consensus.md +248 -0
- package/src/core/agents/ads-audit-budget.md +181 -0
- package/src/core/agents/ads-audit-compliance.md +169 -0
- package/src/core/agents/ads-audit-creative.md +164 -0
- package/src/core/agents/ads-audit-google.md +226 -0
- package/src/core/agents/ads-audit-meta.md +183 -0
- package/src/core/agents/ads-audit-tracking.md +197 -0
- package/src/core/agents/ads-consensus.md +396 -0
- package/src/core/agents/ads-generate.md +145 -0
- package/src/core/agents/ads-performance-tracker.md +197 -0
- package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
- package/src/core/agents/api-quality-analyzer-docs.md +176 -0
- package/src/core/agents/api-quality-analyzer-errors.md +183 -0
- package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
- package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
- package/src/core/agents/api-quality-consensus.md +214 -0
- package/src/core/agents/arch-analyzer-circular.md +148 -0
- package/src/core/agents/arch-analyzer-complexity.md +171 -0
- package/src/core/agents/arch-analyzer-coupling.md +146 -0
- package/src/core/agents/arch-analyzer-layering.md +151 -0
- package/src/core/agents/arch-analyzer-patterns.md +162 -0
- package/src/core/agents/arch-consensus.md +227 -0
- package/src/core/agents/brainstorm-analyzer-features.md +169 -0
- package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
- package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
- package/src/core/agents/brainstorm-analyzer-market.md +147 -0
- package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
- package/src/core/agents/brainstorm-consensus.md +237 -0
- package/src/core/agents/completeness-consensus.md +5 -5
- package/src/core/agents/perf-consensus.md +2 -2
- package/src/core/agents/security-consensus.md +2 -2
- package/src/core/agents/seo-analyzer-content.md +167 -0
- package/src/core/agents/seo-analyzer-images.md +187 -0
- package/src/core/agents/seo-analyzer-performance.md +206 -0
- package/src/core/agents/seo-analyzer-schema.md +176 -0
- package/src/core/agents/seo-analyzer-sitemap.md +172 -0
- package/src/core/agents/seo-analyzer-technical.md +144 -0
- package/src/core/agents/seo-consensus.md +289 -0
- package/src/core/agents/test-consensus.md +2 -2
- package/src/core/commands/adr.md +1 -0
- package/src/core/commands/ads/audit.md +375 -0
- package/src/core/commands/ads/budget.md +97 -0
- package/src/core/commands/ads/competitor.md +112 -0
- package/src/core/commands/ads/creative.md +85 -0
- package/src/core/commands/ads/generate.md +238 -0
- package/src/core/commands/ads/google.md +112 -0
- package/src/core/commands/ads/health.md +327 -0
- package/src/core/commands/ads/landing.md +119 -0
- package/src/core/commands/ads/linkedin.md +112 -0
- package/src/core/commands/ads/meta.md +91 -0
- package/src/core/commands/ads/microsoft.md +115 -0
- package/src/core/commands/ads/plan.md +321 -0
- package/src/core/commands/ads/test-plan.md +317 -0
- package/src/core/commands/ads/tiktok.md +129 -0
- package/src/core/commands/ads/track.md +288 -0
- package/src/core/commands/ads/youtube.md +124 -0
- package/src/core/commands/ads.md +140 -0
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/audit.md +43 -6
- package/src/core/commands/babysit.md +315 -1266
- package/src/core/commands/baseline.md +1 -0
- package/src/core/commands/blockers.md +1 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +1 -0
- package/src/core/commands/choose.md +1 -0
- package/src/core/commands/ci.md +1 -0
- package/src/core/commands/code/accessibility.md +347 -0
- package/src/core/commands/code/api.md +297 -0
- package/src/core/commands/code/architecture.md +297 -0
- package/src/core/commands/{audit → code}/completeness.md +72 -25
- package/src/core/commands/{audit → code}/legal.md +63 -16
- package/src/core/commands/{audit → code}/logic.md +64 -16
- package/src/core/commands/{audit → code}/performance.md +67 -20
- package/src/core/commands/{audit → code}/security.md +69 -19
- package/src/core/commands/{audit → code}/test.md +67 -20
- package/src/core/commands/configure.md +1 -0
- package/src/core/commands/council.md +1 -0
- package/src/core/commands/deploy.md +1 -0
- package/src/core/commands/diagnose.md +1 -0
- package/src/core/commands/docs.md +1 -0
- package/src/core/commands/epic/edit.md +213 -0
- package/src/core/commands/epic.md +1 -0
- package/src/core/commands/export.md +238 -0
- package/src/core/commands/help.md +16 -1
- package/src/core/commands/{discovery → ideate}/brief.md +12 -12
- package/src/core/commands/{discovery/new.md → ideate/discover.md} +20 -16
- package/src/core/commands/ideate/features.md +496 -0
- package/src/core/commands/ideate/new.md +158 -124
- package/src/core/commands/impact.md +1 -0
- package/src/core/commands/learn/explain.md +118 -0
- package/src/core/commands/learn/glossary.md +135 -0
- package/src/core/commands/learn/patterns.md +138 -0
- package/src/core/commands/learn/tour.md +126 -0
- package/src/core/commands/migrate/codemods.md +151 -0
- package/src/core/commands/migrate/plan.md +131 -0
- package/src/core/commands/migrate/scan.md +114 -0
- package/src/core/commands/migrate/validate.md +119 -0
- package/src/core/commands/multi-expert.md +1 -0
- package/src/core/commands/pr.md +1 -0
- package/src/core/commands/review.md +1 -0
- package/src/core/commands/seo/audit.md +373 -0
- package/src/core/commands/seo/competitor.md +174 -0
- package/src/core/commands/seo/content.md +107 -0
- package/src/core/commands/seo/geo.md +229 -0
- package/src/core/commands/seo/hreflang.md +140 -0
- package/src/core/commands/seo/images.md +96 -0
- package/src/core/commands/seo/page.md +198 -0
- package/src/core/commands/seo/plan.md +163 -0
- package/src/core/commands/seo/programmatic.md +131 -0
- package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
- package/src/core/commands/seo/references/eeat-framework.md +110 -0
- package/src/core/commands/seo/references/quality-gates.md +91 -0
- package/src/core/commands/seo/references/schema-types.md +102 -0
- package/src/core/commands/seo/schema.md +183 -0
- package/src/core/commands/seo/sitemap.md +97 -0
- package/src/core/commands/seo/technical.md +100 -0
- package/src/core/commands/seo.md +107 -0
- package/src/core/commands/skill/list.md +68 -212
- package/src/core/commands/skill/recommend.md +216 -0
- package/src/core/commands/sprint.md +1 -0
- package/src/core/commands/status/undo.md +191 -0
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story/edit.md +204 -0
- package/src/core/commands/story/view.md +29 -7
- package/src/core/commands/story-validate.md +1 -0
- package/src/core/commands/story.md +1 -0
- package/src/core/commands/tdd-next.md +238 -0
- package/src/core/commands/tdd.md +211 -0
- package/src/core/commands/team/start.md +10 -6
- package/src/core/commands/tests.md +1 -0
- package/src/core/commands/verify.md +27 -1
- package/src/core/commands/workflow.md +2 -0
- package/src/core/experts/_core-expertise.yaml +105 -0
- package/src/core/experts/analytics/expertise.yaml +5 -99
- package/src/core/experts/codebase-query/expertise.yaml +3 -72
- package/src/core/experts/compliance/expertise.yaml +6 -72
- package/src/core/experts/database/expertise.yaml +9 -52
- package/src/core/experts/documentation/expertise.yaml +7 -140
- package/src/core/experts/integrations/expertise.yaml +7 -127
- package/src/core/experts/mentor/expertise.yaml +8 -35
- package/src/core/experts/monitoring/expertise.yaml +7 -49
- package/src/core/experts/performance/expertise.yaml +1 -26
- package/src/core/experts/security/expertise.yaml +9 -34
- package/src/core/experts/ui/expertise.yaml +6 -36
- package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
- package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
- package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
- package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
- package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
- package/src/core/teams/backend.json +41 -0
- package/src/core/teams/frontend.json +41 -0
- package/src/core/teams/qa.json +41 -0
- package/src/core/teams/solo.json +35 -0
- package/src/core/templates/agileflow-metadata.json +20 -1
- package/tools/cli/commands/setup.js +85 -3
- package/tools/cli/commands/update.js +42 -0
- package/tools/cli/installers/ide/_base-ide.js +42 -5
- package/tools/cli/installers/ide/claude-code.js +71 -3
- package/tools/cli/lib/content-injector.js +160 -12
- package/tools/cli/lib/docs-setup.js +1 -1
- package/src/core/commands/skill/create.md +0 -698
- package/src/core/commands/skill/delete.md +0 -316
- package/src/core/commands/skill/edit.md +0 -359
- package/src/core/commands/skill/test.md +0 -394
- package/src/core/commands/skill/upgrade.md +0 -552
- package/src/core/templates/skill-template.md +0 -117
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ac-test-matcher.js - Automated Acceptance Criteria to Test Mapping
|
|
3
|
+
*
|
|
4
|
+
* Reads story AC from status.json, scans test files for keyword overlap,
|
|
5
|
+
* and returns matched/unmatched AC with confidence levels.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { matchACToTests } = require('./lib/ac-test-matcher');
|
|
9
|
+
*
|
|
10
|
+
* const result = matchACToTests('US-0042', projectRoot);
|
|
11
|
+
* // result = {
|
|
12
|
+
* // storyId: 'US-0042',
|
|
13
|
+
* // total: 5,
|
|
14
|
+
* // matched: [{ index: 0, ac: '...', confidence: 'high', testFiles: [...] }],
|
|
15
|
+
* // unmatched: [{ index: 2, ac: '...' }],
|
|
16
|
+
* // coverage: 0.6
|
|
17
|
+
* // }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const { getProjectRoot, getStatusPath } = require('../../lib/paths');
|
|
26
|
+
const { safeReadJSON } = require('../../lib/errors');
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Keyword Extraction
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract meaningful keywords from an AC string.
|
|
34
|
+
* Filters out stop words and short tokens.
|
|
35
|
+
*/
|
|
36
|
+
const STOP_WORDS = new Set([
|
|
37
|
+
'the',
|
|
38
|
+
'a',
|
|
39
|
+
'an',
|
|
40
|
+
'is',
|
|
41
|
+
'are',
|
|
42
|
+
'was',
|
|
43
|
+
'were',
|
|
44
|
+
'be',
|
|
45
|
+
'been',
|
|
46
|
+
'being',
|
|
47
|
+
'have',
|
|
48
|
+
'has',
|
|
49
|
+
'had',
|
|
50
|
+
'do',
|
|
51
|
+
'does',
|
|
52
|
+
'did',
|
|
53
|
+
'will',
|
|
54
|
+
'would',
|
|
55
|
+
'could',
|
|
56
|
+
'should',
|
|
57
|
+
'may',
|
|
58
|
+
'might',
|
|
59
|
+
'must',
|
|
60
|
+
'shall',
|
|
61
|
+
'can',
|
|
62
|
+
'need',
|
|
63
|
+
'dare',
|
|
64
|
+
'to',
|
|
65
|
+
'of',
|
|
66
|
+
'in',
|
|
67
|
+
'for',
|
|
68
|
+
'on',
|
|
69
|
+
'with',
|
|
70
|
+
'at',
|
|
71
|
+
'by',
|
|
72
|
+
'from',
|
|
73
|
+
'as',
|
|
74
|
+
'into',
|
|
75
|
+
'through',
|
|
76
|
+
'during',
|
|
77
|
+
'before',
|
|
78
|
+
'after',
|
|
79
|
+
'above',
|
|
80
|
+
'below',
|
|
81
|
+
'and',
|
|
82
|
+
'but',
|
|
83
|
+
'or',
|
|
84
|
+
'nor',
|
|
85
|
+
'not',
|
|
86
|
+
'so',
|
|
87
|
+
'yet',
|
|
88
|
+
'both',
|
|
89
|
+
'either',
|
|
90
|
+
'neither',
|
|
91
|
+
'each',
|
|
92
|
+
'every',
|
|
93
|
+
'all',
|
|
94
|
+
'any',
|
|
95
|
+
'few',
|
|
96
|
+
'more',
|
|
97
|
+
'most',
|
|
98
|
+
'other',
|
|
99
|
+
'some',
|
|
100
|
+
'such',
|
|
101
|
+
'no',
|
|
102
|
+
'only',
|
|
103
|
+
'own',
|
|
104
|
+
'same',
|
|
105
|
+
'than',
|
|
106
|
+
'too',
|
|
107
|
+
'very',
|
|
108
|
+
'just',
|
|
109
|
+
'that',
|
|
110
|
+
'this',
|
|
111
|
+
'these',
|
|
112
|
+
'those',
|
|
113
|
+
'it',
|
|
114
|
+
'its',
|
|
115
|
+
'when',
|
|
116
|
+
'where',
|
|
117
|
+
'how',
|
|
118
|
+
'what',
|
|
119
|
+
'which',
|
|
120
|
+
'who',
|
|
121
|
+
'whom',
|
|
122
|
+
'then',
|
|
123
|
+
'there',
|
|
124
|
+
'here',
|
|
125
|
+
'if',
|
|
126
|
+
'else',
|
|
127
|
+
'while',
|
|
128
|
+
'about',
|
|
129
|
+
'up',
|
|
130
|
+
'out',
|
|
131
|
+
'also',
|
|
132
|
+
'given',
|
|
133
|
+
'user',
|
|
134
|
+
'system',
|
|
135
|
+
'able',
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
function extractKeywords(text) {
|
|
139
|
+
if (!text || typeof text !== 'string') return [];
|
|
140
|
+
// Split on non-alphanumeric (keep hyphenated words)
|
|
141
|
+
const tokens = text
|
|
142
|
+
.toLowerCase()
|
|
143
|
+
.replace(/[^a-z0-9-_]/g, ' ')
|
|
144
|
+
.split(/\s+/)
|
|
145
|
+
.filter(t => t.length >= 3 && !STOP_WORDS.has(t));
|
|
146
|
+
|
|
147
|
+
return [...new Set(tokens)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Test File Discovery
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/** Common test file patterns */
|
|
155
|
+
const TEST_PATTERNS = [
|
|
156
|
+
/\.test\.[jt]sx?$/,
|
|
157
|
+
/\.spec\.[jt]sx?$/,
|
|
158
|
+
/_test\.[jt]sx?$/,
|
|
159
|
+
/_test\.go$/,
|
|
160
|
+
/test_.*\.py$/,
|
|
161
|
+
/.*_test\.py$/,
|
|
162
|
+
/\.test\.ts$/,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Recursively find test files under a directory.
|
|
167
|
+
* Skips node_modules, dist, .git, coverage, etc.
|
|
168
|
+
*/
|
|
169
|
+
function findTestFiles(dir, maxDepth = 5) {
|
|
170
|
+
const results = [];
|
|
171
|
+
const skipDirs = new Set([
|
|
172
|
+
'node_modules',
|
|
173
|
+
'.git',
|
|
174
|
+
'dist',
|
|
175
|
+
'build',
|
|
176
|
+
'coverage',
|
|
177
|
+
'.next',
|
|
178
|
+
'.nuxt',
|
|
179
|
+
'.agileflow',
|
|
180
|
+
'.claude',
|
|
181
|
+
'vendor',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
function walk(currentDir, depth) {
|
|
185
|
+
if (depth > maxDepth) return;
|
|
186
|
+
let entries;
|
|
187
|
+
try {
|
|
188
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
189
|
+
} catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isDirectory()) {
|
|
194
|
+
if (!skipDirs.has(entry.name)) {
|
|
195
|
+
walk(path.join(currentDir, entry.name), depth + 1);
|
|
196
|
+
}
|
|
197
|
+
} else if (entry.isFile()) {
|
|
198
|
+
if (TEST_PATTERNS.some(pat => pat.test(entry.name))) {
|
|
199
|
+
results.push(path.join(currentDir, entry.name));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
walk(dir, 0);
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Test Content Scanning
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read a test file and extract its content for keyword matching.
|
|
215
|
+
* Returns lowercased content (describe/it blocks, comments, etc.)
|
|
216
|
+
*/
|
|
217
|
+
function readTestContent(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
220
|
+
return content.toLowerCase();
|
|
221
|
+
} catch {
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// AC-to-Test Matching
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Calculate keyword overlap between AC keywords and test file content.
|
|
232
|
+
* Returns a confidence score.
|
|
233
|
+
*/
|
|
234
|
+
function calculateConfidence(acKeywords, testContent) {
|
|
235
|
+
if (acKeywords.length === 0) return 0;
|
|
236
|
+
let matchCount = 0;
|
|
237
|
+
for (const kw of acKeywords) {
|
|
238
|
+
if (testContent.includes(kw)) {
|
|
239
|
+
matchCount++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const ratio = matchCount / acKeywords.length;
|
|
243
|
+
return ratio;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Determine confidence level from ratio.
|
|
248
|
+
*/
|
|
249
|
+
function confidenceLevel(ratio) {
|
|
250
|
+
if (ratio >= 0.6) return 'high';
|
|
251
|
+
if (ratio >= 0.3) return 'medium';
|
|
252
|
+
if (ratio > 0) return 'low';
|
|
253
|
+
return 'none';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Match acceptance criteria to test files.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} storyId - Story ID (e.g., 'US-0042')
|
|
260
|
+
* @param {string} [rootDir] - Project root (auto-detected if omitted)
|
|
261
|
+
* @returns {{ storyId, total, matched, unmatched, coverage, error? }}
|
|
262
|
+
*/
|
|
263
|
+
function matchACToTests(storyId, rootDir) {
|
|
264
|
+
rootDir = rootDir || getProjectRoot();
|
|
265
|
+
|
|
266
|
+
if (!rootDir) {
|
|
267
|
+
return {
|
|
268
|
+
storyId,
|
|
269
|
+
total: 0,
|
|
270
|
+
matched: [],
|
|
271
|
+
unmatched: [],
|
|
272
|
+
coverage: 0,
|
|
273
|
+
error: 'Project root not found',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Load story from status.json
|
|
278
|
+
const statusPath = getStatusPath(rootDir);
|
|
279
|
+
const result = safeReadJSON(statusPath, { defaultValue: { stories: {} } });
|
|
280
|
+
const status = result.ok ? result.data : null;
|
|
281
|
+
if (!status || !status.stories) {
|
|
282
|
+
return {
|
|
283
|
+
storyId,
|
|
284
|
+
total: 0,
|
|
285
|
+
matched: [],
|
|
286
|
+
unmatched: [],
|
|
287
|
+
coverage: 0,
|
|
288
|
+
error: 'status.json not found or invalid',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const story = status.stories[storyId];
|
|
293
|
+
if (!story) {
|
|
294
|
+
return {
|
|
295
|
+
storyId,
|
|
296
|
+
total: 0,
|
|
297
|
+
matched: [],
|
|
298
|
+
unmatched: [],
|
|
299
|
+
coverage: 0,
|
|
300
|
+
error: `Story ${storyId} not found`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const acList = story.acceptance_criteria || story.ac || [];
|
|
305
|
+
if (!Array.isArray(acList) || acList.length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
storyId,
|
|
308
|
+
total: 0,
|
|
309
|
+
matched: [],
|
|
310
|
+
unmatched: [],
|
|
311
|
+
coverage: 0,
|
|
312
|
+
error: 'No acceptance criteria defined',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find test files
|
|
317
|
+
const testFiles = findTestFiles(rootDir);
|
|
318
|
+
if (testFiles.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
storyId,
|
|
321
|
+
total: acList.length,
|
|
322
|
+
matched: [],
|
|
323
|
+
unmatched: acList.map((ac, i) => ({
|
|
324
|
+
index: i,
|
|
325
|
+
ac: typeof ac === 'string' ? ac : ac.text || String(ac),
|
|
326
|
+
})),
|
|
327
|
+
coverage: 0,
|
|
328
|
+
error: 'No test files found',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Read all test content (cached in memory for this run)
|
|
333
|
+
const testContentMap = {};
|
|
334
|
+
for (const tf of testFiles) {
|
|
335
|
+
testContentMap[tf] = readTestContent(tf);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Match each AC against test files
|
|
339
|
+
const matched = [];
|
|
340
|
+
const unmatched = [];
|
|
341
|
+
|
|
342
|
+
for (let i = 0; i < acList.length; i++) {
|
|
343
|
+
const acText = typeof acList[i] === 'string' ? acList[i] : acList[i].text || String(acList[i]);
|
|
344
|
+
const keywords = extractKeywords(acText);
|
|
345
|
+
|
|
346
|
+
if (keywords.length === 0) {
|
|
347
|
+
unmatched.push({ index: i, ac: acText });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Find best matching test files
|
|
352
|
+
const fileMatches = [];
|
|
353
|
+
for (const [filePath, content] of Object.entries(testContentMap)) {
|
|
354
|
+
const ratio = calculateConfidence(keywords, content);
|
|
355
|
+
if (ratio > 0) {
|
|
356
|
+
fileMatches.push({
|
|
357
|
+
file: path.relative(rootDir, filePath),
|
|
358
|
+
ratio,
|
|
359
|
+
confidence: confidenceLevel(ratio),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Sort by match ratio descending
|
|
365
|
+
fileMatches.sort((a, b) => b.ratio - a.ratio);
|
|
366
|
+
|
|
367
|
+
// Take top 3 matches
|
|
368
|
+
const topMatches = fileMatches.slice(0, 3);
|
|
369
|
+
const bestConfidence = topMatches.length > 0 ? topMatches[0].confidence : 'none';
|
|
370
|
+
|
|
371
|
+
if (bestConfidence === 'high' || bestConfidence === 'medium') {
|
|
372
|
+
matched.push({
|
|
373
|
+
index: i,
|
|
374
|
+
ac: acText,
|
|
375
|
+
confidence: bestConfidence,
|
|
376
|
+
keywords,
|
|
377
|
+
testFiles: topMatches.map(m => ({ file: m.file, confidence: m.confidence })),
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
unmatched.push({
|
|
381
|
+
index: i,
|
|
382
|
+
ac: acText,
|
|
383
|
+
keywords,
|
|
384
|
+
testFiles:
|
|
385
|
+
topMatches.length > 0
|
|
386
|
+
? topMatches.map(m => ({ file: m.file, confidence: m.confidence }))
|
|
387
|
+
: undefined,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
storyId,
|
|
394
|
+
total: acList.length,
|
|
395
|
+
matched,
|
|
396
|
+
unmatched,
|
|
397
|
+
coverage: acList.length > 0 ? matched.length / acList.length : 0,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Write ac_status to status.json for a story based on match results.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} storyId - Story ID
|
|
405
|
+
* @param {Object} matchResult - Result from matchACToTests
|
|
406
|
+
* @param {Object} [manualOverrides] - Manual AC verification { index: 'verified' }
|
|
407
|
+
* @param {string} [rootDir] - Project root
|
|
408
|
+
*/
|
|
409
|
+
function writeACStatus(storyId, matchResult, manualOverrides = {}, rootDir) {
|
|
410
|
+
rootDir = rootDir || getProjectRoot();
|
|
411
|
+
const statusPath = getStatusPath(rootDir);
|
|
412
|
+
const result = safeReadJSON(statusPath);
|
|
413
|
+
if (!result.ok || !result.data || !result.data.stories || !result.data.stories[storyId]) return;
|
|
414
|
+
const status = result.data;
|
|
415
|
+
|
|
416
|
+
const acStatus = {};
|
|
417
|
+
|
|
418
|
+
// Auto-verified from high-confidence matches
|
|
419
|
+
for (const m of matchResult.matched) {
|
|
420
|
+
if (m.confidence === 'high') {
|
|
421
|
+
acStatus[m.index] = 'auto-verified';
|
|
422
|
+
} else {
|
|
423
|
+
acStatus[m.index] = 'likely-covered';
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Unmatched remain unverified
|
|
428
|
+
for (const u of matchResult.unmatched) {
|
|
429
|
+
acStatus[u.index] = 'unverified';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Apply manual overrides
|
|
433
|
+
for (const [idx, val] of Object.entries(manualOverrides)) {
|
|
434
|
+
acStatus[idx] = val;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
status.stories[storyId].ac_status = acStatus;
|
|
438
|
+
status.stories[storyId].ac_coverage = matchResult.coverage;
|
|
439
|
+
|
|
440
|
+
const { safeWriteJSON: writeJSON } = require('../../lib/errors');
|
|
441
|
+
writeJSON(statusPath, status);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
matchACToTests,
|
|
446
|
+
writeACStatus,
|
|
447
|
+
// Exported for testing
|
|
448
|
+
extractKeywords,
|
|
449
|
+
findTestFiles,
|
|
450
|
+
calculateConfidence,
|
|
451
|
+
confidenceLevel,
|
|
452
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit-cleanup.js - Orphan cleanup for ULTRADEEP audit sessions
|
|
3
|
+
*
|
|
4
|
+
* Cleans up abandoned tmux sessions and incomplete sentinel directories
|
|
5
|
+
* from ULTRADEEP audit runs. Designed to be called from Stop hooks or
|
|
6
|
+
* manually for maintenance.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { cleanupOrphanSessions } = require('./audit-cleanup');
|
|
10
|
+
* cleanupOrphanSessions(rootDir);
|
|
11
|
+
*
|
|
12
|
+
* CLI:
|
|
13
|
+
* node scripts/lib/audit-cleanup.js [--max-age=60] [--dry-run]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execFileSync } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const MAX_AGE_MINUTES = 60;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find all ultradeep trace directories.
|
|
24
|
+
* @param {string} rootDir - Project root
|
|
25
|
+
* @returns {Array<{ traceId: string, dir: string, status: object|null }>}
|
|
26
|
+
*/
|
|
27
|
+
function findTraceDirectories(rootDir) {
|
|
28
|
+
const ultradeepDir = path.join(rootDir, 'docs', '09-agents', 'ultradeep');
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(ultradeepDir)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const traces = [];
|
|
35
|
+
try {
|
|
36
|
+
const entries = fs.readdirSync(ultradeepDir, { withFileTypes: true });
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.isDirectory()) continue;
|
|
39
|
+
|
|
40
|
+
const traceDir = path.join(ultradeepDir, entry.name);
|
|
41
|
+
const statusFile = path.join(traceDir, '_status.json');
|
|
42
|
+
let status = null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(statusFile)) {
|
|
46
|
+
status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
} catch (_) {
|
|
49
|
+
// Corrupt status file
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
traces.push({
|
|
53
|
+
traceId: entry.name,
|
|
54
|
+
dir: traceDir,
|
|
55
|
+
status,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {
|
|
59
|
+
// Directory read failure
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return traces;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a trace is stale (older than maxAge).
|
|
67
|
+
* @param {object} trace - Trace info from findTraceDirectories
|
|
68
|
+
* @param {number} maxAgeMinutes - Maximum age in minutes
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
function isStaleTrace(trace, maxAgeMinutes) {
|
|
72
|
+
if (!trace.status || !trace.status.started_at) {
|
|
73
|
+
// No status = assume stale
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const startedAt = new Date(trace.status.started_at).getTime();
|
|
78
|
+
if (isNaN(startedAt)) return true; // Invalid date = treat as stale
|
|
79
|
+
const age = Date.now() - startedAt;
|
|
80
|
+
return age > maxAgeMinutes * 60 * 1000;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a trace is incomplete (not all analyzers have findings).
|
|
85
|
+
* @param {object} trace - Trace info from findTraceDirectories
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function isIncompleteTrace(trace) {
|
|
89
|
+
if (!trace.status || !trace.status.analyzers) return true;
|
|
90
|
+
|
|
91
|
+
const expected = trace.status.analyzers;
|
|
92
|
+
const completed = trace.status.completed || [];
|
|
93
|
+
|
|
94
|
+
return completed.length < expected.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Find orphaned tmux sessions matching audit pattern.
|
|
99
|
+
* @returns {string[]} Array of session names
|
|
100
|
+
*/
|
|
101
|
+
function findOrphanedTmuxSessions() {
|
|
102
|
+
try {
|
|
103
|
+
const output = execFileSync('tmux', ['list-sessions', '-F', '#{session_name}'], {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
}).trim();
|
|
107
|
+
|
|
108
|
+
if (!output) return [];
|
|
109
|
+
|
|
110
|
+
return output.split('\n').filter(name => name.startsWith('audit-'));
|
|
111
|
+
} catch (_) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Kill a tmux session by name.
|
|
118
|
+
* @param {string} sessionName - Session name to kill
|
|
119
|
+
* @returns {boolean} True if killed successfully
|
|
120
|
+
*/
|
|
121
|
+
function killTmuxSession(sessionName) {
|
|
122
|
+
try {
|
|
123
|
+
execFileSync('tmux', ['kill-session', '-t', sessionName], {
|
|
124
|
+
stdio: 'pipe',
|
|
125
|
+
});
|
|
126
|
+
return true;
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove a sentinel directory.
|
|
134
|
+
* @param {string} dir - Directory to remove
|
|
135
|
+
* @returns {boolean} True if removed successfully
|
|
136
|
+
*/
|
|
137
|
+
function removeSentinelDir(dir) {
|
|
138
|
+
try {
|
|
139
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
return true;
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clean up orphaned ULTRADEEP audit sessions and stale sentinel dirs.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} rootDir - Project root directory
|
|
150
|
+
* @param {object} [options] - Options
|
|
151
|
+
* @param {number} [options.maxAgeMinutes] - Max age for stale traces (default: 60)
|
|
152
|
+
* @param {boolean} [options.dryRun] - If true, report but don't delete
|
|
153
|
+
* @returns {{ sessionsKilled: string[], tracesRemoved: string[], errors: string[] }}
|
|
154
|
+
*/
|
|
155
|
+
function cleanupOrphanSessions(rootDir, options) {
|
|
156
|
+
const maxAge = (options && options.maxAgeMinutes) || MAX_AGE_MINUTES;
|
|
157
|
+
const dryRun = (options && options.dryRun) || false;
|
|
158
|
+
|
|
159
|
+
const result = {
|
|
160
|
+
sessionsKilled: [],
|
|
161
|
+
tracesRemoved: [],
|
|
162
|
+
errors: [],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 1. Find and kill orphaned tmux sessions
|
|
166
|
+
const orphanedSessions = findOrphanedTmuxSessions();
|
|
167
|
+
for (const session of orphanedSessions) {
|
|
168
|
+
// Extract trace ID from session name: audit-{type}-{traceId}
|
|
169
|
+
const parts = session.split('-');
|
|
170
|
+
if (parts.length < 3) continue; // Malformed session name, skip
|
|
171
|
+
const traceId = parts.slice(2).join('-');
|
|
172
|
+
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
result.sessionsKilled.push(`${session} (dry-run)`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (killTmuxSession(session)) {
|
|
179
|
+
result.sessionsKilled.push(session);
|
|
180
|
+
} else {
|
|
181
|
+
result.errors.push(`Failed to kill session: ${session}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Clean up stale sentinel directories
|
|
186
|
+
const traces = findTraceDirectories(rootDir);
|
|
187
|
+
for (const trace of traces) {
|
|
188
|
+
if (isStaleTrace(trace, maxAge) && isIncompleteTrace(trace)) {
|
|
189
|
+
if (dryRun) {
|
|
190
|
+
result.tracesRemoved.push(`${trace.traceId} (dry-run)`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (removeSentinelDir(trace.dir)) {
|
|
195
|
+
result.tracesRemoved.push(trace.traceId);
|
|
196
|
+
} else {
|
|
197
|
+
result.errors.push(`Failed to remove trace dir: ${trace.traceId}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// CLI
|
|
206
|
+
if (require.main === module) {
|
|
207
|
+
const args = process.argv.slice(2);
|
|
208
|
+
let maxAge = MAX_AGE_MINUTES;
|
|
209
|
+
let dryRun = false;
|
|
210
|
+
|
|
211
|
+
for (const arg of args) {
|
|
212
|
+
if (arg.startsWith('--max-age=')) {
|
|
213
|
+
const parsed = parseInt(arg.split('=')[1], 10);
|
|
214
|
+
maxAge = isNaN(parsed) ? MAX_AGE_MINUTES : parsed;
|
|
215
|
+
}
|
|
216
|
+
if (arg === '--dry-run') dryRun = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rootDir = process.cwd();
|
|
220
|
+
const result = cleanupOrphanSessions(rootDir, { maxAgeMinutes: maxAge, dryRun });
|
|
221
|
+
|
|
222
|
+
if (result.sessionsKilled.length > 0) {
|
|
223
|
+
console.log(`Killed ${result.sessionsKilled.length} orphaned session(s):`);
|
|
224
|
+
result.sessionsKilled.forEach(s => console.log(` - ${s}`));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (result.tracesRemoved.length > 0) {
|
|
228
|
+
console.log(`Removed ${result.tracesRemoved.length} stale trace(s):`);
|
|
229
|
+
result.tracesRemoved.forEach(t => console.log(` - ${t}`));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (result.errors.length > 0) {
|
|
233
|
+
console.error(`${result.errors.length} error(s):`);
|
|
234
|
+
result.errors.forEach(e => console.error(` - ${e}`));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (result.sessionsKilled.length === 0 && result.tracesRemoved.length === 0) {
|
|
238
|
+
console.log('No orphaned sessions or stale traces found.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
findTraceDirectories,
|
|
244
|
+
isStaleTrace,
|
|
245
|
+
isIncompleteTrace,
|
|
246
|
+
findOrphanedTmuxSessions,
|
|
247
|
+
killTmuxSession,
|
|
248
|
+
removeSentinelDir,
|
|
249
|
+
cleanupOrphanSessions,
|
|
250
|
+
};
|