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.
Files changed (210) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/skill-loader.js +0 -1
  4. package/package.json +1 -1
  5. package/scripts/agileflow-statusline.sh +81 -0
  6. package/scripts/agileflow-welcome.js +79 -0
  7. package/scripts/claude-tmux.sh +90 -23
  8. package/scripts/claude-watchdog.sh +225 -0
  9. package/scripts/generators/agent-registry.js +14 -1
  10. package/scripts/generators/inject-babysit.js +22 -9
  11. package/scripts/generators/inject-help.js +19 -9
  12. package/scripts/lib/ac-test-matcher.js +452 -0
  13. package/scripts/lib/audit-cleanup.js +250 -0
  14. package/scripts/lib/audit-registry.js +304 -0
  15. package/scripts/lib/configure-features.js +35 -0
  16. package/scripts/lib/feature-catalog.js +3 -3
  17. package/scripts/lib/gate-enforcer.js +295 -0
  18. package/scripts/lib/model-profiles.js +118 -0
  19. package/scripts/lib/quality-gates.js +163 -0
  20. package/scripts/lib/signal-detectors.js +44 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/status-writer.js +255 -0
  24. package/scripts/lib/story-claiming.js +128 -45
  25. package/scripts/lib/task-sync.js +32 -38
  26. package/scripts/lib/tdd-phase-manager.js +455 -0
  27. package/scripts/lib/team-events.js +34 -3
  28. package/scripts/lib/tmux-audit-monitor.js +611 -0
  29. package/scripts/lib/tmux-group-colors.js +113 -0
  30. package/scripts/lib/tool-registry.yaml +241 -0
  31. package/scripts/lib/tool-shed.js +441 -0
  32. package/scripts/messaging-bridge.js +209 -1
  33. package/scripts/native-team-observer.js +219 -0
  34. package/scripts/obtain-context.js +14 -0
  35. package/scripts/ralph-loop.js +30 -5
  36. package/scripts/smart-detect.js +21 -0
  37. package/scripts/spawn-audit-sessions.js +877 -0
  38. package/scripts/team-manager.js +56 -16
  39. package/scripts/tmux-close-windows.sh +180 -0
  40. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  41. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  42. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  43. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  44. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  45. package/src/core/agents/a11y-consensus.md +248 -0
  46. package/src/core/agents/ads-audit-budget.md +181 -0
  47. package/src/core/agents/ads-audit-compliance.md +169 -0
  48. package/src/core/agents/ads-audit-creative.md +164 -0
  49. package/src/core/agents/ads-audit-google.md +226 -0
  50. package/src/core/agents/ads-audit-meta.md +183 -0
  51. package/src/core/agents/ads-audit-tracking.md +197 -0
  52. package/src/core/agents/ads-consensus.md +396 -0
  53. package/src/core/agents/ads-generate.md +145 -0
  54. package/src/core/agents/ads-performance-tracker.md +197 -0
  55. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  56. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  57. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  58. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  59. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  60. package/src/core/agents/api-quality-consensus.md +214 -0
  61. package/src/core/agents/arch-analyzer-circular.md +148 -0
  62. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  63. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  64. package/src/core/agents/arch-analyzer-layering.md +151 -0
  65. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  66. package/src/core/agents/arch-consensus.md +227 -0
  67. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  68. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  69. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  70. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  71. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  72. package/src/core/agents/brainstorm-consensus.md +237 -0
  73. package/src/core/agents/completeness-consensus.md +5 -5
  74. package/src/core/agents/perf-consensus.md +2 -2
  75. package/src/core/agents/security-consensus.md +2 -2
  76. package/src/core/agents/seo-analyzer-content.md +167 -0
  77. package/src/core/agents/seo-analyzer-images.md +187 -0
  78. package/src/core/agents/seo-analyzer-performance.md +206 -0
  79. package/src/core/agents/seo-analyzer-schema.md +176 -0
  80. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  81. package/src/core/agents/seo-analyzer-technical.md +144 -0
  82. package/src/core/agents/seo-consensus.md +289 -0
  83. package/src/core/agents/test-consensus.md +2 -2
  84. package/src/core/commands/adr.md +1 -0
  85. package/src/core/commands/ads/audit.md +375 -0
  86. package/src/core/commands/ads/budget.md +97 -0
  87. package/src/core/commands/ads/competitor.md +112 -0
  88. package/src/core/commands/ads/creative.md +85 -0
  89. package/src/core/commands/ads/generate.md +238 -0
  90. package/src/core/commands/ads/google.md +112 -0
  91. package/src/core/commands/ads/health.md +327 -0
  92. package/src/core/commands/ads/landing.md +119 -0
  93. package/src/core/commands/ads/linkedin.md +112 -0
  94. package/src/core/commands/ads/meta.md +91 -0
  95. package/src/core/commands/ads/microsoft.md +115 -0
  96. package/src/core/commands/ads/plan.md +321 -0
  97. package/src/core/commands/ads/test-plan.md +317 -0
  98. package/src/core/commands/ads/tiktok.md +129 -0
  99. package/src/core/commands/ads/track.md +288 -0
  100. package/src/core/commands/ads/youtube.md +124 -0
  101. package/src/core/commands/ads.md +140 -0
  102. package/src/core/commands/assign.md +1 -0
  103. package/src/core/commands/audit.md +43 -6
  104. package/src/core/commands/babysit.md +315 -1266
  105. package/src/core/commands/baseline.md +1 -0
  106. package/src/core/commands/blockers.md +1 -0
  107. package/src/core/commands/board.md +1 -0
  108. package/src/core/commands/changelog.md +1 -0
  109. package/src/core/commands/choose.md +1 -0
  110. package/src/core/commands/ci.md +1 -0
  111. package/src/core/commands/code/accessibility.md +347 -0
  112. package/src/core/commands/code/api.md +297 -0
  113. package/src/core/commands/code/architecture.md +297 -0
  114. package/src/core/commands/{audit → code}/completeness.md +72 -25
  115. package/src/core/commands/{audit → code}/legal.md +63 -16
  116. package/src/core/commands/{audit → code}/logic.md +64 -16
  117. package/src/core/commands/{audit → code}/performance.md +67 -20
  118. package/src/core/commands/{audit → code}/security.md +69 -19
  119. package/src/core/commands/{audit → code}/test.md +67 -20
  120. package/src/core/commands/configure.md +1 -0
  121. package/src/core/commands/council.md +1 -0
  122. package/src/core/commands/deploy.md +1 -0
  123. package/src/core/commands/diagnose.md +1 -0
  124. package/src/core/commands/docs.md +1 -0
  125. package/src/core/commands/epic/edit.md +213 -0
  126. package/src/core/commands/epic.md +1 -0
  127. package/src/core/commands/export.md +238 -0
  128. package/src/core/commands/help.md +16 -1
  129. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  130. package/src/core/commands/{discovery/new.md → ideate/discover.md} +20 -16
  131. package/src/core/commands/ideate/features.md +496 -0
  132. package/src/core/commands/ideate/new.md +158 -124
  133. package/src/core/commands/impact.md +1 -0
  134. package/src/core/commands/learn/explain.md +118 -0
  135. package/src/core/commands/learn/glossary.md +135 -0
  136. package/src/core/commands/learn/patterns.md +138 -0
  137. package/src/core/commands/learn/tour.md +126 -0
  138. package/src/core/commands/migrate/codemods.md +151 -0
  139. package/src/core/commands/migrate/plan.md +131 -0
  140. package/src/core/commands/migrate/scan.md +114 -0
  141. package/src/core/commands/migrate/validate.md +119 -0
  142. package/src/core/commands/multi-expert.md +1 -0
  143. package/src/core/commands/pr.md +1 -0
  144. package/src/core/commands/review.md +1 -0
  145. package/src/core/commands/seo/audit.md +373 -0
  146. package/src/core/commands/seo/competitor.md +174 -0
  147. package/src/core/commands/seo/content.md +107 -0
  148. package/src/core/commands/seo/geo.md +229 -0
  149. package/src/core/commands/seo/hreflang.md +140 -0
  150. package/src/core/commands/seo/images.md +96 -0
  151. package/src/core/commands/seo/page.md +198 -0
  152. package/src/core/commands/seo/plan.md +163 -0
  153. package/src/core/commands/seo/programmatic.md +131 -0
  154. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  155. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  156. package/src/core/commands/seo/references/quality-gates.md +91 -0
  157. package/src/core/commands/seo/references/schema-types.md +102 -0
  158. package/src/core/commands/seo/schema.md +183 -0
  159. package/src/core/commands/seo/sitemap.md +97 -0
  160. package/src/core/commands/seo/technical.md +100 -0
  161. package/src/core/commands/seo.md +107 -0
  162. package/src/core/commands/skill/list.md +68 -212
  163. package/src/core/commands/skill/recommend.md +216 -0
  164. package/src/core/commands/sprint.md +1 -0
  165. package/src/core/commands/status/undo.md +191 -0
  166. package/src/core/commands/status.md +1 -0
  167. package/src/core/commands/story/edit.md +204 -0
  168. package/src/core/commands/story/view.md +29 -7
  169. package/src/core/commands/story-validate.md +1 -0
  170. package/src/core/commands/story.md +1 -0
  171. package/src/core/commands/tdd-next.md +238 -0
  172. package/src/core/commands/tdd.md +211 -0
  173. package/src/core/commands/team/start.md +10 -6
  174. package/src/core/commands/tests.md +1 -0
  175. package/src/core/commands/verify.md +27 -1
  176. package/src/core/commands/workflow.md +2 -0
  177. package/src/core/experts/_core-expertise.yaml +105 -0
  178. package/src/core/experts/analytics/expertise.yaml +5 -99
  179. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  180. package/src/core/experts/compliance/expertise.yaml +6 -72
  181. package/src/core/experts/database/expertise.yaml +9 -52
  182. package/src/core/experts/documentation/expertise.yaml +7 -140
  183. package/src/core/experts/integrations/expertise.yaml +7 -127
  184. package/src/core/experts/mentor/expertise.yaml +8 -35
  185. package/src/core/experts/monitoring/expertise.yaml +7 -49
  186. package/src/core/experts/performance/expertise.yaml +1 -26
  187. package/src/core/experts/security/expertise.yaml +9 -34
  188. package/src/core/experts/ui/expertise.yaml +6 -36
  189. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  190. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  191. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  192. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  193. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  194. package/src/core/teams/backend.json +41 -0
  195. package/src/core/teams/frontend.json +41 -0
  196. package/src/core/teams/qa.json +41 -0
  197. package/src/core/teams/solo.json +35 -0
  198. package/src/core/templates/agileflow-metadata.json +20 -1
  199. package/tools/cli/commands/setup.js +85 -3
  200. package/tools/cli/commands/update.js +42 -0
  201. package/tools/cli/installers/ide/_base-ide.js +42 -5
  202. package/tools/cli/installers/ide/claude-code.js +71 -3
  203. package/tools/cli/lib/content-injector.js +160 -12
  204. package/tools/cli/lib/docs-setup.js +1 -1
  205. package/src/core/commands/skill/create.md +0 -698
  206. package/src/core/commands/skill/delete.md +0 -316
  207. package/src/core/commands/skill/edit.md +0 -359
  208. package/src/core/commands/skill/test.md +0 -394
  209. package/src/core/commands/skill/upgrade.md +0 -552
  210. 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
+ };