agileflow 3.4.0 → 3.4.2

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 (115) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/scripts/agileflow-welcome.js +79 -0
  5. package/scripts/claude-tmux.sh +12 -36
  6. package/scripts/lib/ac-test-matcher.js +452 -0
  7. package/scripts/lib/audit-registry.js +94 -2
  8. package/scripts/lib/configure-features.js +35 -0
  9. package/scripts/lib/model-profiles.js +25 -5
  10. package/scripts/lib/quality-gates.js +163 -0
  11. package/scripts/lib/signal-detectors.js +43 -0
  12. package/scripts/lib/status-writer.js +255 -0
  13. package/scripts/lib/story-claiming.js +128 -45
  14. package/scripts/lib/task-sync.js +32 -38
  15. package/scripts/lib/tmux-audit-monitor.js +611 -0
  16. package/scripts/lib/tmux-group-colors.js +2 -2
  17. package/scripts/lib/tool-registry.yaml +241 -0
  18. package/scripts/lib/tool-shed.js +441 -0
  19. package/scripts/native-team-observer.js +219 -0
  20. package/scripts/obtain-context.js +14 -0
  21. package/scripts/ralph-loop.js +30 -5
  22. package/scripts/smart-detect.js +21 -0
  23. package/scripts/spawn-audit-sessions.js +373 -45
  24. package/scripts/team-manager.js +19 -0
  25. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  26. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  27. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  28. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  29. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  30. package/src/core/agents/a11y-consensus.md +248 -0
  31. package/src/core/agents/ads-consensus.md +74 -0
  32. package/src/core/agents/ads-generate.md +145 -0
  33. package/src/core/agents/ads-performance-tracker.md +197 -0
  34. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  35. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  36. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  37. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  38. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  39. package/src/core/agents/api-quality-consensus.md +214 -0
  40. package/src/core/agents/arch-analyzer-circular.md +148 -0
  41. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  42. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  43. package/src/core/agents/arch-analyzer-layering.md +151 -0
  44. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  45. package/src/core/agents/arch-consensus.md +227 -0
  46. package/src/core/commands/adr.md +1 -0
  47. package/src/core/commands/ads/audit.md +67 -5
  48. package/src/core/commands/ads/generate.md +238 -0
  49. package/src/core/commands/ads/health.md +327 -0
  50. package/src/core/commands/ads/test-plan.md +317 -0
  51. package/src/core/commands/ads/track.md +288 -0
  52. package/src/core/commands/ads.md +28 -16
  53. package/src/core/commands/assign.md +1 -0
  54. package/src/core/commands/audit.md +43 -6
  55. package/src/core/commands/babysit.md +90 -6
  56. package/src/core/commands/baseline.md +1 -0
  57. package/src/core/commands/blockers.md +1 -0
  58. package/src/core/commands/board.md +1 -0
  59. package/src/core/commands/changelog.md +1 -0
  60. package/src/core/commands/choose.md +1 -0
  61. package/src/core/commands/ci.md +1 -0
  62. package/src/core/commands/code/accessibility.md +347 -0
  63. package/src/core/commands/code/api.md +297 -0
  64. package/src/core/commands/code/architecture.md +297 -0
  65. package/src/core/commands/code/completeness.md +43 -6
  66. package/src/core/commands/code/legal.md +43 -6
  67. package/src/core/commands/code/logic.md +43 -6
  68. package/src/core/commands/code/performance.md +43 -6
  69. package/src/core/commands/code/security.md +43 -6
  70. package/src/core/commands/code/test.md +43 -6
  71. package/src/core/commands/configure.md +1 -0
  72. package/src/core/commands/council.md +1 -0
  73. package/src/core/commands/deploy.md +1 -0
  74. package/src/core/commands/diagnose.md +1 -0
  75. package/src/core/commands/docs.md +1 -0
  76. package/src/core/commands/epic/edit.md +213 -0
  77. package/src/core/commands/epic.md +1 -0
  78. package/src/core/commands/export.md +238 -0
  79. package/src/core/commands/help.md +16 -1
  80. package/src/core/commands/ideate/discover.md +7 -3
  81. package/src/core/commands/ideate/features.md +65 -4
  82. package/src/core/commands/ideate/new.md +158 -124
  83. package/src/core/commands/impact.md +1 -0
  84. package/src/core/commands/learn/explain.md +118 -0
  85. package/src/core/commands/learn/glossary.md +135 -0
  86. package/src/core/commands/learn/patterns.md +138 -0
  87. package/src/core/commands/learn/tour.md +126 -0
  88. package/src/core/commands/migrate/codemods.md +151 -0
  89. package/src/core/commands/migrate/plan.md +131 -0
  90. package/src/core/commands/migrate/scan.md +114 -0
  91. package/src/core/commands/migrate/validate.md +119 -0
  92. package/src/core/commands/multi-expert.md +1 -0
  93. package/src/core/commands/pr.md +1 -0
  94. package/src/core/commands/review.md +1 -0
  95. package/src/core/commands/seo/audit.md +61 -6
  96. package/src/core/commands/sprint.md +1 -0
  97. package/src/core/commands/status/undo.md +191 -0
  98. package/src/core/commands/status.md +1 -0
  99. package/src/core/commands/story/edit.md +204 -0
  100. package/src/core/commands/story/view.md +29 -7
  101. package/src/core/commands/story-validate.md +1 -0
  102. package/src/core/commands/story.md +1 -0
  103. package/src/core/commands/tdd.md +1 -0
  104. package/src/core/commands/team/start.md +10 -6
  105. package/src/core/commands/tests.md +1 -0
  106. package/src/core/commands/verify.md +27 -1
  107. package/src/core/commands/workflow.md +2 -0
  108. package/src/core/teams/backend.json +41 -0
  109. package/src/core/teams/frontend.json +41 -0
  110. package/src/core/teams/qa.json +41 -0
  111. package/src/core/teams/solo.json +35 -0
  112. package/src/core/templates/agileflow-metadata.json +5 -0
  113. package/tools/cli/commands/setup.js +85 -3
  114. package/tools/cli/commands/update.js +42 -0
  115. package/tools/cli/installers/ide/claude-code.js +68 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.4.2] - 2026-03-07
11
+
12
+ ### Added
13
+ - DEPTH=ultradeep|extreme for SEO and Ads audit commands
14
+
15
+ ## [3.4.1] - 2026-03-06
16
+
17
+ ### Added
18
+ - DEPTH=extreme audit mode, CI feedback loops, and DX quick wins
19
+
10
20
  ## [3.4.0] - 2026-02-28
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  </p>
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
- [![Commands](https://img.shields.io/badge/commands-124-blue)](https://docs.agileflow.projectquestorg.com/docs/commands)
7
- [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-111-orange)](https://docs.agileflow.projectquestorg.com/docs/agents)
6
+ [![Commands](https://img.shields.io/badge/commands-143-blue)](https://docs.agileflow.projectquestorg.com/docs/commands)
7
+ [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-131-orange)](https://docs.agileflow.projectquestorg.com/docs/agents)
8
8
  [![Skills](https://img.shields.io/badge/skills-dynamic-purple)](https://docs.agileflow.projectquestorg.com/docs/features/skills)
9
9
 
10
10
  **AI-driven agile development for Claude Code, Cursor, Windsurf, OpenAI Codex, and more.** Combining Scrum, Kanban, ADRs, and docs-as-code principles into one framework-agnostic system.
@@ -54,8 +54,8 @@ Traditional project management tools create friction between planning and execut
54
54
 
55
55
  | Component | Count | Description |
56
56
  |-----------|-------|-------------|
57
- | [Commands](https://docs.agileflow.projectquestorg.com/docs/commands) | 124 | Slash commands for agile workflows |
58
- | [Agents/Experts](https://docs.agileflow.projectquestorg.com/docs/agents) | 111 | Specialized agents with self-improving knowledge bases |
57
+ | [Commands](https://docs.agileflow.projectquestorg.com/docs/commands) | 143 | Slash commands for agile workflows |
58
+ | [Agents/Experts](https://docs.agileflow.projectquestorg.com/docs/agents) | 131 | Specialized agents with self-improving knowledge bases |
59
59
  | [Skills](https://docs.agileflow.projectquestorg.com/docs/features/skills) | Dynamic | Browse and install from skills.sh marketplace via `/agileflow:skill:recommend` |
60
60
 
61
61
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -1320,6 +1320,49 @@ function getExpertiseCountFast(rootDir) {
1320
1320
  return result;
1321
1321
  }
1322
1322
 
1323
+ /**
1324
+ * Check if installed commands are in sync with source.
1325
+ * Counts .md files in three directories:
1326
+ * - .agileflow/commands/ (core install)
1327
+ * - .claude/commands/agileflow/ excluding agents/ (IDE install)
1328
+ * - packages/cli/src/core/commands/ (dogfooding source, if exists)
1329
+ * Returns { sourceCount, coreCount, ideCount, stale, staleCoreInstall } or null on error.
1330
+ */
1331
+ function checkCommandSync(rootDir) {
1332
+ try {
1333
+ const countMdFiles = dir => {
1334
+ if (!fs.existsSync(dir)) return 0;
1335
+ return fs.readdirSync(dir, { recursive: true }).filter(f => String(f).endsWith('.md')).length;
1336
+ };
1337
+
1338
+ const agileflowDir = getAgileflowDir(rootDir);
1339
+ const coreCount = countMdFiles(path.join(agileflowDir, 'commands'));
1340
+
1341
+ // IDE install: .claude/commands/agileflow/ excluding agents/ subdir
1342
+ const ideDir = path.join(rootDir, '.claude', 'commands', 'agileflow');
1343
+ let ideCount = 0;
1344
+ if (fs.existsSync(ideDir)) {
1345
+ ideCount = fs.readdirSync(ideDir, { recursive: true }).filter(f => {
1346
+ const s = String(f);
1347
+ return s.endsWith('.md') && !s.startsWith('agents/') && !s.startsWith('agents\\');
1348
+ }).length;
1349
+ }
1350
+
1351
+ // Dogfooding source (only exists in the AgileFlow repo itself)
1352
+ const sourceDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'commands');
1353
+ const sourceCount = countMdFiles(sourceDir);
1354
+
1355
+ // Determine staleness
1356
+ const referenceCount = sourceCount > 0 ? sourceCount : coreCount;
1357
+ const stale = referenceCount > 0 && ideCount < referenceCount;
1358
+ const staleCoreInstall = sourceCount > 0 && coreCount < sourceCount;
1359
+
1360
+ return { sourceCount, coreCount, ideCount, stale, staleCoreInstall };
1361
+ } catch (e) {
1362
+ return null;
1363
+ }
1364
+ }
1365
+
1323
1366
  // Full validation function (kept for /agileflow:validate-expertise command)
1324
1367
  function validateExpertise(rootDir) {
1325
1368
  const result = { total: 0, passed: 0, warnings: 0, failed: 0, issues: [] };
@@ -1894,6 +1937,9 @@ function main() {
1894
1937
  }
1895
1938
  _mark('expertise');
1896
1939
 
1940
+ const commandSync = checkCommandSync(rootDir);
1941
+ _mark('cmdSync');
1942
+
1897
1943
  const damageControl = checkDamageControl(rootDir, cache);
1898
1944
  _mark('damageCtl');
1899
1945
 
@@ -2057,6 +2103,21 @@ function main() {
2057
2103
  );
2058
2104
  }
2059
2105
 
2106
+ // Show command sync staleness notification
2107
+ if (commandSync && commandSync.stale) {
2108
+ const ref = commandSync.sourceCount > 0 ? commandSync.sourceCount : commandSync.coreCount;
2109
+ console.log('');
2110
+ console.log(
2111
+ `${c.amber}⚠️ ${ref - commandSync.ideCount} command(s) out of sync${c.reset} ${c.dim}(IDE has ${commandSync.ideCount}/${ref})${c.reset}`
2112
+ );
2113
+ if (commandSync.staleCoreInstall) {
2114
+ console.log(
2115
+ ` ${c.dim}Core install also stale: ${commandSync.coreCount}/${commandSync.sourceCount}${c.reset}`
2116
+ );
2117
+ }
2118
+ console.log(` ${c.slate}Auto-syncing in background...${c.reset}`);
2119
+ }
2120
+
2060
2121
  // Show tmux installation notice if tmux auto-spawn is enabled but tmux not installed
2061
2122
  if (tmuxAutoSpawnEnabled && !tmuxCheck.available) {
2062
2123
  console.log('');
@@ -2141,6 +2202,24 @@ function main() {
2141
2202
 
2142
2203
  _mark('deferred');
2143
2204
 
2205
+ // Spawn background command sync if stale
2206
+ if (commandSync && commandSync.stale) {
2207
+ try {
2208
+ const isDogfooding = commandSync.sourceCount > 0;
2209
+ if (isDogfooding) {
2210
+ const cliPath = path.join(rootDir, 'packages', 'cli', 'tools', 'cli', 'agileflow-cli.js');
2211
+ if (fs.existsSync(cliPath)) {
2212
+ spawnBackground('node', [cliPath, 'update', '--force', '-d', rootDir], { cwd: rootDir });
2213
+ }
2214
+ } else {
2215
+ spawnBackground('npx', ['agileflow', 'update', '--force'], { cwd: rootDir });
2216
+ }
2217
+ } catch (e) {
2218
+ // Command sync spawn failed, non-critical
2219
+ }
2220
+ }
2221
+ _mark('cmdSyncSpawn');
2222
+
2144
2223
  // Record hook metrics
2145
2224
  if (timer && hookMetrics) {
2146
2225
  hookMetrics.recordHookMetrics(timer, 'success', null, { rootDir });
@@ -41,6 +41,7 @@ SHOW_HELP=false
41
41
  ATTACH_ONLY=false
42
42
  FORCE_NEW=false
43
43
  REFRESH_CONFIG=false
44
+ CONFIGURE_SESSION=""
44
45
  USE_RESUME=false
45
46
  RESUME_SESSION_ID=""
46
47
 
@@ -74,6 +75,10 @@ for arg in "$@"; do
74
75
  REFRESH_CONFIG=true
75
76
  shift
76
77
  ;;
78
+ --configure-session=*)
79
+ CONFIGURE_SESSION="${arg#*=}"
80
+ shift
81
+ ;;
77
82
  --help|-h)
78
83
  SHOW_HELP=true
79
84
  shift
@@ -128,7 +133,6 @@ FREEZE RECOVERY:
128
133
  Alt+R Respawn pane (fresh shell)
129
134
 
130
135
  OTHER:
131
- Alt+b Scroll mode (browse history)
132
136
  Alt+h Show keybind help panel
133
137
  EOF
134
138
  exit 0
@@ -347,40 +351,6 @@ configure_tmux_session() {
347
351
  # Alt+z to zoom/unzoom pane (fullscreen toggle)
348
352
  tmux bind-key -n M-z resize-pane -Z
349
353
 
350
- # Alt+b to enter copy mode (for scrolling / browsing history)
351
- # NOTE: Do NOT use Alt+[ here — \e[ is the CSI prefix for arrow keys and
352
- # function keys. Binding M-[ in the root table causes accidental copy-mode
353
- # entry whenever an escape sequence is split by network latency, making the
354
- # terminal appear to "lose focus" until Escape is pressed.
355
- tmux bind-key -n M-b copy-mode
356
-
357
- # Disable ALL command-prompt bindings in copy mode — they open a text input
358
- # in the status bar which intercepts keystrokes and confuses the workflow.
359
- # Navigation (arrows, scroll, mouse, PgUp/PgDn) and exit (q/Escape) still work.
360
- #
361
- # Emacs copy-mode: goto-line, search, jump-to-char, repeat-count
362
- tmux unbind-key -T copy-mode g
363
- tmux unbind-key -T copy-mode C-r
364
- tmux unbind-key -T copy-mode C-s
365
- tmux unbind-key -T copy-mode f
366
- tmux unbind-key -T copy-mode F
367
- tmux unbind-key -T copy-mode t
368
- tmux unbind-key -T copy-mode T
369
- for i in 1 2 3 4 5 6 7 8 9; do
370
- tmux unbind-key -T copy-mode "M-$i"
371
- done
372
- # Vi copy-mode: goto-line, search, jump-to-char, repeat-count
373
- tmux unbind-key -T copy-mode-vi :
374
- tmux unbind-key -T copy-mode-vi /
375
- tmux unbind-key -T copy-mode-vi ?
376
- tmux unbind-key -T copy-mode-vi f
377
- tmux unbind-key -T copy-mode-vi F
378
- tmux unbind-key -T copy-mode-vi t
379
- tmux unbind-key -T copy-mode-vi T
380
- for i in 1 2 3 4 5 6 7 8 9; do
381
- tmux unbind-key -T copy-mode-vi "$i"
382
- done
383
-
384
354
  # ─── Session Creation Keybindings ──────────────────────────────────────────
385
355
  # Alt+s to create a new Claude window (starts fresh, future re-runs in same pane resume)
386
356
  # Window gets sequential name (claude-2, claude-3, ...) so windows are distinguishable
@@ -438,13 +408,19 @@ configure_tmux_session() {
438
408
  printf ' Alt+R Respawn pane (fresh)\\n';\
439
409
  printf '\\n';\
440
410
  printf ' \\033[1;38;5;208mOTHER\\033[0m\\n';\
441
- printf ' Alt+b Scroll mode\\n';\
442
411
  printf ' Alt+k Unfreeze (Ctrl+C x2)\\n';\
443
412
  printf ' Alt+h This help\\n';\
444
413
  printf '\\n';\
445
414
  read -n 1 -s -r -p ' Press any key to close'"
446
415
  }
447
416
 
417
+ # Handle --configure-session flag — apply theme to a specific session and exit
418
+ # Used by spawn-audit-sessions.js to give audit sessions the same status bar
419
+ if [ -n "$CONFIGURE_SESSION" ]; then
420
+ configure_tmux_session "$CONFIGURE_SESSION"
421
+ exit 0
422
+ fi
423
+
448
424
  # Handle --refresh flag — re-apply config to all existing claude-* sessions
449
425
  if [ "$REFRESH_CONFIG" = true ]; then
450
426
  REFRESHED=0
@@ -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
+ };