agileflow 3.0.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +58 -86
  3. package/lib/dashboard-automations.js +130 -0
  4. package/lib/dashboard-git.js +254 -0
  5. package/lib/dashboard-inbox.js +64 -0
  6. package/lib/dashboard-protocol.js +1 -0
  7. package/lib/dashboard-server.js +114 -924
  8. package/lib/dashboard-session.js +136 -0
  9. package/lib/dashboard-status.js +72 -0
  10. package/lib/dashboard-terminal.js +354 -0
  11. package/lib/dashboard-websocket.js +88 -0
  12. package/lib/drivers/codex-driver.ts +4 -4
  13. package/lib/feedback.js +9 -2
  14. package/lib/lazy-require.js +59 -0
  15. package/lib/logger.js +106 -0
  16. package/package.json +4 -2
  17. package/scripts/agileflow-configure.js +14 -2
  18. package/scripts/agileflow-welcome.js +450 -459
  19. package/scripts/claude-tmux.sh +113 -5
  20. package/scripts/context-loader.js +4 -9
  21. package/scripts/lib/command-prereqs.js +280 -0
  22. package/scripts/lib/configure-detect.js +92 -2
  23. package/scripts/lib/configure-features.js +411 -1
  24. package/scripts/lib/context-formatter.js +468 -233
  25. package/scripts/lib/context-loader.js +27 -15
  26. package/scripts/lib/damage-control-utils.js +8 -1
  27. package/scripts/lib/feature-catalog.js +321 -0
  28. package/scripts/lib/portable-tasks-cli.js +274 -0
  29. package/scripts/lib/portable-tasks.js +479 -0
  30. package/scripts/lib/signal-detectors.js +1 -1
  31. package/scripts/lib/team-events.js +86 -1
  32. package/scripts/obtain-context.js +28 -4
  33. package/scripts/smart-detect.js +17 -0
  34. package/scripts/strip-ai-attribution.js +63 -0
  35. package/scripts/team-manager.js +90 -0
  36. package/scripts/welcome-deferred.js +437 -0
  37. package/src/core/agents/legal-analyzer-a11y.md +110 -0
  38. package/src/core/agents/legal-analyzer-ai.md +117 -0
  39. package/src/core/agents/legal-analyzer-consumer.md +108 -0
  40. package/src/core/agents/legal-analyzer-content.md +113 -0
  41. package/src/core/agents/legal-analyzer-international.md +115 -0
  42. package/src/core/agents/legal-analyzer-licensing.md +115 -0
  43. package/src/core/agents/legal-analyzer-privacy.md +108 -0
  44. package/src/core/agents/legal-analyzer-security.md +112 -0
  45. package/src/core/agents/legal-analyzer-terms.md +111 -0
  46. package/src/core/agents/legal-consensus.md +242 -0
  47. package/src/core/agents/perf-analyzer-assets.md +174 -0
  48. package/src/core/agents/perf-analyzer-bundle.md +165 -0
  49. package/src/core/agents/perf-analyzer-caching.md +160 -0
  50. package/src/core/agents/perf-analyzer-compute.md +165 -0
  51. package/src/core/agents/perf-analyzer-memory.md +182 -0
  52. package/src/core/agents/perf-analyzer-network.md +157 -0
  53. package/src/core/agents/perf-analyzer-queries.md +155 -0
  54. package/src/core/agents/perf-analyzer-rendering.md +156 -0
  55. package/src/core/agents/perf-consensus.md +280 -0
  56. package/src/core/agents/security-analyzer-api.md +199 -0
  57. package/src/core/agents/security-analyzer-auth.md +160 -0
  58. package/src/core/agents/security-analyzer-authz.md +168 -0
  59. package/src/core/agents/security-analyzer-deps.md +147 -0
  60. package/src/core/agents/security-analyzer-infra.md +176 -0
  61. package/src/core/agents/security-analyzer-injection.md +148 -0
  62. package/src/core/agents/security-analyzer-input.md +191 -0
  63. package/src/core/agents/security-analyzer-secrets.md +175 -0
  64. package/src/core/agents/security-consensus.md +276 -0
  65. package/src/core/agents/team-lead.md +50 -13
  66. package/src/core/agents/test-analyzer-assertions.md +181 -0
  67. package/src/core/agents/test-analyzer-coverage.md +183 -0
  68. package/src/core/agents/test-analyzer-fragility.md +185 -0
  69. package/src/core/agents/test-analyzer-integration.md +155 -0
  70. package/src/core/agents/test-analyzer-maintenance.md +173 -0
  71. package/src/core/agents/test-analyzer-mocking.md +178 -0
  72. package/src/core/agents/test-analyzer-patterns.md +189 -0
  73. package/src/core/agents/test-analyzer-structure.md +177 -0
  74. package/src/core/agents/test-consensus.md +294 -0
  75. package/src/core/commands/audit/legal.md +446 -0
  76. package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
  77. package/src/core/commands/audit/performance.md +443 -0
  78. package/src/core/commands/audit/security.md +443 -0
  79. package/src/core/commands/audit/test.md +442 -0
  80. package/src/core/commands/babysit.md +505 -463
  81. package/src/core/commands/configure.md +18 -33
  82. package/src/core/commands/research/ask.md +42 -9
  83. package/src/core/commands/research/import.md +14 -8
  84. package/src/core/commands/research/list.md +17 -16
  85. package/src/core/commands/research/synthesize.md +8 -8
  86. package/src/core/commands/research/view.md +28 -4
  87. package/src/core/commands/team/start.md +36 -7
  88. package/src/core/commands/team/stop.md +5 -2
  89. package/src/core/commands/whats-new.md +2 -2
  90. package/src/core/experts/devops/expertise.yaml +13 -2
  91. package/src/core/experts/documentation/expertise.yaml +26 -4
  92. package/src/core/profiles/COMPARISON.md +170 -0
  93. package/src/core/profiles/README.md +178 -0
  94. package/src/core/profiles/claude-code.yaml +111 -0
  95. package/src/core/profiles/codex.yaml +103 -0
  96. package/src/core/profiles/cursor.yaml +134 -0
  97. package/src/core/profiles/examples.js +250 -0
  98. package/src/core/profiles/loader.js +235 -0
  99. package/src/core/profiles/windsurf.yaml +159 -0
  100. package/src/core/teams/logic-audit.json +6 -0
  101. package/src/core/teams/perf-audit.json +71 -0
  102. package/src/core/teams/security-audit.json +71 -0
  103. package/src/core/teams/test-audit.json +71 -0
  104. package/src/core/templates/command-prerequisites.yaml +169 -0
  105. package/src/core/templates/damage-control-patterns.yaml +9 -0
  106. package/tools/cli/installers/ide/_base-ide.js +33 -3
  107. package/tools/cli/installers/ide/claude-code.js +2 -67
  108. package/tools/cli/installers/ide/codex.js +9 -9
  109. package/tools/cli/installers/ide/cursor.js +165 -4
  110. package/tools/cli/installers/ide/windsurf.js +237 -6
  111. package/tools/cli/lib/content-transformer.js +234 -9
  112. package/tools/cli/lib/docs-setup.js +1 -1
  113. package/tools/cli/lib/ide-generator.js +357 -0
  114. package/tools/cli/lib/ide-registry.js +2 -2
  115. package/scripts/tmux-task-name.sh +0 -75
  116. package/scripts/tmux-task-watcher.sh +0 -177
@@ -155,6 +155,81 @@ if command -v tmux &> /dev/null; then
155
155
  unset _TMUX_BASE _TMUX_SOCK_DIR
156
156
  fi
157
157
 
158
+ # ══════════════════════════════════════════════════════════════════════════════
159
+ # TAB FORMAT BUILDER — dynamic compaction based on window count & terminal width
160
+ # Uses tmux 3.2+ #{e|...} numeric operators for cascading tier selection
161
+ # ══════════════════════════════════════════════════════════════════════════════
162
+ build_tab_format() {
163
+ # Chrome-like tab compaction: 14 tiers with threshold = width for minimal waste.
164
+ # Per-window budget (width/windows) picks the largest tier that fits.
165
+ # #{pN:#{=N:var}} = exactly N visible chars (truncate long + pad short names).
166
+
167
+ # ── Active tab: orange bg index + dark bg name ──────────────────────────
168
+ # " I " prefix = 5 visible chars (wide); " I " = 3 chars (narrow)
169
+ # Width = prefix + 1(space) + pN(name) + 1(space)
170
+ local a0='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p33:#{=33:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
171
+ local a1='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p20:#{=20:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
172
+ local a2='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p13:#{=13:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
173
+ local a3='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p11:#{=11:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
174
+ local a4='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p8:#{=8:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
175
+ local a5='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{p6:#{=6:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
176
+ local a6='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e0e0e0 bg=#2d2f3a] #{p5:#{=5:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
177
+ local a7='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e0e0e0 bg=#2d2f3a] #{p4:#{=4:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
178
+ local a8='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e0e0e0 bg=#2d2f3a] #{p3:#{=3:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
179
+ local a9='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e0e0e0 bg=#2d2f3a] #{p2:#{=2:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
180
+ local a10='#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e0e0e0 bg=#2d2f3a] #{p1:#{=1:window_name}} #[bg=#1a1b26 fg=#2d2f3a]'
181
+ local a11='#[fg=#1a1b26 bg=#e8683a bold] #I #[bg=#1a1b26 fg=#e8683a]'
182
+ local a12='#[fg=#1a1b26 bg=#e8683a bold] #I #[bg=#1a1b26 fg=#e8683a]'
183
+ local a13='#[fg=#e8683a bold]#I#[fg=default]'
184
+
185
+ # ── Inactive tab: gray text ─────────────────────────────────────────────
186
+ # " I:" prefix = 4 visible chars (wide); " I:" = 3 chars (narrow)
187
+ # Width = prefix + pN(name) + 1(space)
188
+ local i0='#[fg=#8a8a8a] #I:#{p35:#{=35:window_name}} '
189
+ local i1='#[fg=#8a8a8a] #I:#{p22:#{=22:window_name}} '
190
+ local i2='#[fg=#8a8a8a] #I:#{p15:#{=15:window_name}} '
191
+ local i3='#[fg=#8a8a8a] #I:#{p12:#{=12:window_name}} '
192
+ local i4='#[fg=#8a8a8a] #I:#{p9:#{=9:window_name}} '
193
+ local i5='#[fg=#8a8a8a] #I:#{p7:#{=7:window_name}} '
194
+ local i6='#[fg=#8a8a8a] #I:#{p6:#{=6:window_name}} '
195
+ local i7='#[fg=#8a8a8a] #I:#{p5:#{=5:window_name}} '
196
+ local i8='#[fg=#8a8a8a] #I:#{p4:#{=4:window_name}} '
197
+ local i9='#[fg=#8a8a8a] #I:#{p3:#{=3:window_name}} '
198
+ local i10='#[fg=#8a8a8a] #I:#{p2:#{=2:window_name}} '
199
+ local i11='#[fg=#8a8a8a] #I:#{p1:#{=1:window_name}} '
200
+ local i12='#[fg=#8a8a8a] #I '
201
+ local i13='#[fg=#565a6e]#I'
202
+
203
+ # ── Tier selection: budget = width / windows ─────────────────────────────
204
+ local budget='#{e|/|:#{client_width},#{session_windows}}'
205
+ local cp="#{?#{e|>=|:${budget},"
206
+ local cm='},'
207
+ local cs='}'
208
+
209
+ # 14 tiers: threshold = format width → minimal wasted space.
210
+ # 81-col fill: 2-11 wins >=95%, 12-16 wins >=86%.
211
+ #
212
+ # Tier >=Thr Width 81-col example Fill%
213
+ # T0 40 40 2 wins (40ea) 98%
214
+ # T1 27 27 3 wins (27ea) 100%
215
+ # T2 20 20 4 wins (20ea) 98%
216
+ # T3 16 16 5 wins (16ea) 98%
217
+ # T4 13 13 6 wins (13ea) 96%
218
+ # T5 11 11 7 wins (11ea) 95%
219
+ # T6 10 10 8 wins (10ea) 98%
220
+ # T7 9 9 9 wins (9ea) 100%
221
+ # T8 8 8 10 wins (8ea) 98%
222
+ # T9 7 7 11 wins (7ea) 95%
223
+ # T10 6 6 12-13 wins 88-96%
224
+ # T11 5 5 14-16 wins 86-98%
225
+ # T12 3 3 17-27 wins
226
+ # T13 fallback 1 28+ wins
227
+ local active="${cp}40${cm}${a0},${cp}27${cm}${a1},${cp}20${cm}${a2},${cp}16${cm}${a3},${cp}13${cm}${a4},${cp}11${cm}${a5},${cp}10${cm}${a6},${cp}9${cm}${a7},${cp}8${cm}${a8},${cp}7${cm}${a9},${cp}6${cm}${a10},${cp}5${cm}${a11},${cp}3${cm}${a12},${a13}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}"
228
+ local inactive="${cp}40${cm}${i0},${cp}27${cm}${i1},${cp}20${cm}${i2},${cp}16${cm}${i3},${cp}13${cm}${i4},${cp}11${cm}${i5},${cp}10${cm}${i6},${cp}9${cm}${i7},${cp}8${cm}${i8},${cp}7${cm}${i9},${cp}6${cm}${i10},${cp}5${cm}${i11},${cp}3${cm}${i12},${i13}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}${cs}"
229
+
230
+ echo "#[bg=#1a1b26]#{W:#{?window_active,${active},${inactive}}}"
231
+ }
232
+
158
233
  # ══════════════════════════════════════════════════════════════════════════════
159
234
  # TMUX CONFIGURATION FUNCTION — applies theme, keybinds, and status bar
160
235
  # Defined early so --refresh can use it before any session logic
@@ -189,8 +264,11 @@ configure_tmux_session() {
189
264
  # Uses #() for live branch updates (runs on status-interval, every 30s)
190
265
  tmux set-option -t "$target_session" status-format[0] "#[bg=#1a1b26] #[fg=#e8683a bold]#{s/claude-//:session_name} #[fg=#3b4261]· #[fg=#7aa2f7]󰘬 #(git -C #{pane_current_path} branch --show-current 2>/dev/null || echo '-')#[align=right]#[fg=#565a6e]Alt+h help "
191
266
 
192
- # Line 1 (bottom): Window tabs with smart truncation and brand color
193
- tmux set-option -t "$target_session" status-format[1] "#[bg=#1a1b26]#{W:#{?window_active,#[fg=#1a1b26 bg=#e8683a bold] #I #[fg=#e8683a bg=#2d2f3a]#[fg=#e0e0e0] #{=15:window_name} #[bg=#1a1b26 fg=#2d2f3a],#[fg=#8a8a8a] #I:#{=|8|...:window_name} }}"
267
+ # Line 1 (bottom): Window tabs with dynamic compaction
268
+ # Tabs auto-shrink based on window count and terminal width
269
+ local tab_format
270
+ tab_format=$(build_tab_format)
271
+ tmux set-option -t "$target_session" status-format[1] "$tab_format"
194
272
 
195
273
  # Pane border styling - blue inactive, orange active
196
274
  tmux set-option -t "$target_session" pane-border-style "fg=#3d59a1"
@@ -360,7 +438,7 @@ fi
360
438
  # Silently remove sessions where all panes have exited (dead/empty shells).
361
439
  # This prevents accumulation of orphan sessions over time.
362
440
  SESSION_BASE="claude-${DIR_NAME}"
363
- for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)"); do
441
+ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)"); do
364
442
  # Count alive panes (pane_dead=0 means alive)
365
443
  ALIVE=$(tmux list-panes -t "$sid" -F '#{pane_dead}' 2>/dev/null | grep -c '^0$' || true)
366
444
  if [ "$ALIVE" = "0" ]; then
@@ -368,6 +446,36 @@ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESS
368
446
  fi
369
447
  done
370
448
 
449
+ # ── Consolidate duplicate sessions ───────────────────────────────────────
450
+ # Kill numbered duplicates (e.g. claude-Acuide-2, -3) that were created by
451
+ # a previous bug. If the base session exists, duplicates are unnecessary.
452
+ # If only numbered sessions remain, promote the lowest to the base name.
453
+ if [ "$FORCE_NEW" = false ]; then
454
+ HAS_BASE=false
455
+ NUMBERED=()
456
+ if tmux has-session -t "$SESSION_BASE" 2>/dev/null; then
457
+ HAS_BASE=true
458
+ fi
459
+ while IFS= read -r sid; do
460
+ [ -n "$sid" ] && NUMBERED+=("$sid")
461
+ done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}-[0-9]+$" | sort -t- -k3 -n)
462
+
463
+ if [ "$HAS_BASE" = true ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
464
+ # Base exists — kill all numbered duplicates
465
+ for sid in "${NUMBERED[@]}"; do
466
+ tmux kill-session -t "$sid" 2>/dev/null || true
467
+ done
468
+ elif [ "$HAS_BASE" = false ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
469
+ # No base — promote lowest numbered session to base name
470
+ PROMOTE="${NUMBERED[0]}"
471
+ tmux rename-session -t "$PROMOTE" "$SESSION_BASE" 2>/dev/null || true
472
+ # Kill remaining duplicates
473
+ for sid in "${NUMBERED[@]:1}"; do
474
+ tmux kill-session -t "$sid" 2>/dev/null || true
475
+ done
476
+ fi
477
+ fi
478
+
371
479
  # ── Auto-reattach to detached session ──────────────────────────────────────
372
480
  # When user does Alt+Q (detach) and then runs `af` again, reattach to the
373
481
  # existing session instead of creating a new one. This preserves tmux windows,
@@ -377,7 +485,7 @@ if [ "$FORCE_NEW" = false ]; then
377
485
  DETACHED=()
378
486
  while IFS= read -r sid; do
379
487
  [ -n "$sid" ] && DETACHED+=("$sid")
380
- done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)")
488
+ done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
381
489
 
382
490
  if [ "${#DETACHED[@]}" -eq 1 ]; then
383
491
  # Single detached session — just reattach
@@ -416,7 +524,7 @@ if [ "$FORCE_NEW" = false ]; then
416
524
  EXISTING=()
417
525
  while IFS= read -r sid; do
418
526
  [ -n "$sid" ] && EXISTING+=("$sid")
419
- done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)")
527
+ done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
420
528
 
421
529
  if [ "${#EXISTING[@]}" -gt 0 ]; then
422
530
  # Prefer the base session, otherwise pick the first one
@@ -39,15 +39,10 @@ try {
39
39
  // Feature flags not available
40
40
  }
41
41
 
42
- // Colors for output
43
- const c = {
44
- dim: '\x1b[2m',
45
- cyan: '\x1b[36m',
46
- yellow: '\x1b[33m',
47
- green: '\x1b[32m',
48
- red: '\x1b[31m',
49
- reset: '\x1b[0m',
50
- };
42
+ // Colors for output (use shared color utilities)
43
+ const { c } = require('../lib/colors');
44
+ const { createLogger } = require('../lib/logger');
45
+ const log = createLogger('context-loader');
51
46
 
52
47
  /**
53
48
  * Find project root by looking for .agileflow or .git directory
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * command-prereqs.js
4
+ *
5
+ * Declarative prerequisite checker for AgileFlow commands.
6
+ * Validates that environment signals are met before command execution
7
+ * and provides actionable warnings with fix instructions.
8
+ *
9
+ * Uses:
10
+ * - resolveSignalPath() from feature-catalog.js for signal checking
11
+ * - mtime-based caching pattern from damage-control-utils.js
12
+ * - safeLoad() from yaml-utils.js for YAML parsing
13
+ *
14
+ * All functions fail-open: errors return empty/safe defaults.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // Import resolveSignalPath from feature-catalog
23
+ const { resolveSignalPath } = require('./feature-catalog');
24
+
25
+ // Lazy-load yaml-utils (may not be available in all environments)
26
+ let safeLoad;
27
+ try {
28
+ safeLoad = require('../../lib/yaml-utils').safeLoad;
29
+ } catch {
30
+ // Fallback: no YAML parsing available
31
+ safeLoad = null;
32
+ }
33
+
34
+ // Inline colors (standalone - no dependency on colors.js for hook compat)
35
+ const c = {
36
+ coral: '\x1b[38;5;203m',
37
+ amber: '\x1b[38;5;215m',
38
+ mintGreen: '\x1b[38;5;158m',
39
+ skyBlue: '\x1b[38;5;117m',
40
+ dim: '\x1b[2m',
41
+ bold: '\x1b[1m',
42
+ reset: '\x1b[0m',
43
+ };
44
+
45
+ // Severity display config
46
+ const SEVERITY = {
47
+ critical: { icon: '\u2718', color: c.coral, label: 'CRITICAL' },
48
+ high: { icon: '!', color: c.amber, label: 'HIGH' },
49
+ medium: { icon: '\u25CB', color: c.dim, label: 'MEDIUM' },
50
+ };
51
+
52
+ // =============================================================================
53
+ // Config Cache (mtime-based invalidation, same pattern as damage-control-utils)
54
+ // =============================================================================
55
+
56
+ const _prereqCache = {
57
+ /** @type {string|null} */ filePath: null,
58
+ /** @type {number} */ mtime: 0,
59
+ /** @type {object|null} */ config: null,
60
+ };
61
+
62
+ /**
63
+ * Clear the prereq config cache (for testing or forced reload).
64
+ */
65
+ function clearPrereqCache() {
66
+ _prereqCache.filePath = null;
67
+ _prereqCache.mtime = 0;
68
+ _prereqCache.config = null;
69
+ }
70
+
71
+ // Config search paths (installed location, then source template)
72
+ const CONFIG_PATHS = [
73
+ '.agileflow/templates/command-prerequisites.yaml',
74
+ '.agileflow/templates/command-prerequisites.yml',
75
+ '.agileflow/config/command-prerequisites.yaml',
76
+ ];
77
+
78
+ /**
79
+ * Load prerequisite configuration from YAML with mtime-based caching.
80
+ * Returns safe defaults on any error (fail-open).
81
+ *
82
+ * @param {string} [projectRoot] - Project root directory (defaults to cwd)
83
+ * @returns {{ commands: Object, settings: Object }} Parsed config
84
+ */
85
+ function loadPrereqConfig(projectRoot) {
86
+ const root = projectRoot || process.cwd();
87
+ const defaultConfig = { commands: {}, settings: { fail_open: true, max_warnings: 5 } };
88
+
89
+ if (!safeLoad) {
90
+ return defaultConfig;
91
+ }
92
+
93
+ for (const configPath of CONFIG_PATHS) {
94
+ const fullPath = path.join(root, configPath);
95
+ if (!fs.existsSync(fullPath)) continue;
96
+
97
+ try {
98
+ const stat = fs.statSync(fullPath);
99
+ const mtime = stat.mtimeMs;
100
+
101
+ // Return cached if file hasn't changed
102
+ if (
103
+ _prereqCache.filePath === fullPath &&
104
+ _prereqCache.mtime === mtime &&
105
+ _prereqCache.config
106
+ ) {
107
+ return _prereqCache.config;
108
+ }
109
+
110
+ const content = fs.readFileSync(fullPath, 'utf8');
111
+ const parsed = safeLoad(content);
112
+
113
+ if (!parsed || typeof parsed !== 'object') {
114
+ continue;
115
+ }
116
+
117
+ const config = {
118
+ commands: parsed.commands || {},
119
+ settings: { ...defaultConfig.settings, ...(parsed.settings || {}) },
120
+ };
121
+
122
+ // Store in cache
123
+ _prereqCache.filePath = fullPath;
124
+ _prereqCache.mtime = mtime;
125
+ _prereqCache.config = config;
126
+
127
+ return config;
128
+ } catch {
129
+ // Continue to next path
130
+ }
131
+ }
132
+
133
+ return defaultConfig;
134
+ }
135
+
136
+ // =============================================================================
137
+ // Prerequisite Checking (pure function)
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Check prerequisites for a specific command against current signals.
142
+ * Pure function - no I/O, no side effects.
143
+ *
144
+ * @param {string} commandName - Command to check (e.g. 'deploy', 'babysit')
145
+ * @param {Object} signals - Extracted signals from smart-detect
146
+ * @param {{ commands: Object, settings: Object }} config - Loaded prereq config
147
+ * @returns {{
148
+ * command: string,
149
+ * hasPrereqs: boolean,
150
+ * allMet: boolean,
151
+ * results: Array<{ signal: string, description: string, fix: string, severity: string, met: boolean }>,
152
+ * unmet: Array<{ signal: string, description: string, fix: string, severity: string }>,
153
+ * criticalUnmet: number,
154
+ * highUnmet: number
155
+ * }}
156
+ */
157
+ function checkCommandPrereqs(commandName, signals, config) {
158
+ const result = {
159
+ command: commandName,
160
+ hasPrereqs: false,
161
+ allMet: true,
162
+ results: [],
163
+ unmet: [],
164
+ criticalUnmet: 0,
165
+ highUnmet: 0,
166
+ };
167
+
168
+ if (!commandName || !config || !config.commands) {
169
+ return result;
170
+ }
171
+
172
+ const commandConfig = config.commands[commandName];
173
+ if (!commandConfig || !Array.isArray(commandConfig.prerequisites)) {
174
+ return result;
175
+ }
176
+
177
+ result.hasPrereqs = true;
178
+ const prereqs = commandConfig.prerequisites;
179
+
180
+ for (const prereq of prereqs) {
181
+ if (!prereq.signal) continue;
182
+
183
+ const value = resolveSignalPath(signals || {}, prereq.signal);
184
+ // Evaluate signal: empty arrays and empty strings are "unmet"
185
+ // (e.g. git.filesChanged=[] means no changes, which is unmet)
186
+ const met = Array.isArray(value) ? value.length > 0 : !!value;
187
+
188
+ const entry = {
189
+ signal: prereq.signal,
190
+ description: prereq.description || prereq.signal,
191
+ fix: prereq.fix || '',
192
+ severity: prereq.severity || 'medium',
193
+ met,
194
+ };
195
+
196
+ result.results.push(entry);
197
+
198
+ if (!met) {
199
+ result.allMet = false;
200
+ result.unmet.push(entry);
201
+
202
+ if (entry.severity === 'critical') {
203
+ result.criticalUnmet++;
204
+ } else if (entry.severity === 'high') {
205
+ result.highUnmet++;
206
+ }
207
+ }
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ // =============================================================================
214
+ // Warning Formatting
215
+ // =============================================================================
216
+
217
+ /**
218
+ * Format unmet prerequisite warnings with ANSI colors.
219
+ * Returns empty string if all prerequisites are met.
220
+ *
221
+ * @param {{ allMet: boolean, command: string, unmet: Array, criticalUnmet: number }} checkResult
222
+ * @param {{ max_warnings?: number }} [settings] - Display settings
223
+ * @returns {string} Formatted warning string (with trailing newline) or empty string
224
+ */
225
+ function formatPrereqWarnings(checkResult, settings) {
226
+ if (!checkResult || checkResult.allMet || checkResult.unmet.length === 0) {
227
+ return '';
228
+ }
229
+
230
+ const maxWarnings = (settings && settings.max_warnings) || 5;
231
+ const { command, unmet, criticalUnmet } = checkResult;
232
+
233
+ const lines = [];
234
+ const headerColor = criticalUnmet > 0 ? c.coral : c.amber;
235
+ const headerIcon = criticalUnmet > 0 ? '\u26A0\uFE0F' : '\u2139\uFE0F';
236
+
237
+ lines.push('');
238
+ lines.push(
239
+ `${headerColor}${c.bold}\u2501\u2501\u2501 ${headerIcon} Command Prerequisites: /${command} \u2501\u2501\u2501${c.reset}`
240
+ );
241
+
242
+ if (criticalUnmet > 0) {
243
+ lines.push(
244
+ `${c.coral}${criticalUnmet} critical prerequisite(s) not met - command may fail${c.reset}`
245
+ );
246
+ } else {
247
+ lines.push(
248
+ `${c.amber}${unmet.length} prerequisite(s) not met - results may be suboptimal${c.reset}`
249
+ );
250
+ }
251
+
252
+ lines.push('');
253
+
254
+ const displayed = unmet.slice(0, maxWarnings);
255
+ for (const prereq of displayed) {
256
+ const sev = SEVERITY[prereq.severity] || SEVERITY.medium;
257
+ lines.push(` ${sev.color}${sev.icon} [${sev.label}]${c.reset} ${prereq.description}`);
258
+ if (prereq.fix) {
259
+ lines.push(` ${c.dim}\u2192 Fix: ${prereq.fix}${c.reset}`);
260
+ }
261
+ }
262
+
263
+ if (unmet.length > maxWarnings) {
264
+ lines.push(` ${c.dim}... and ${unmet.length - maxWarnings} more${c.reset}`);
265
+ }
266
+
267
+ lines.push('');
268
+
269
+ return lines.join('\n');
270
+ }
271
+
272
+ module.exports = {
273
+ loadPrereqConfig,
274
+ checkCommandPrereqs,
275
+ formatPrereqWarnings,
276
+ clearPrereqCache,
277
+ // Exported for testing
278
+ CONFIG_PATHS,
279
+ SEVERITY,
280
+ };
@@ -83,6 +83,13 @@ function detectConfig(version) {
83
83
  level: null,
84
84
  patternCount: 0,
85
85
  },
86
+ noaiattribution: {
87
+ enabled: false,
88
+ valid: true,
89
+ issues: [],
90
+ version: null,
91
+ outdated: false,
92
+ },
86
93
  askuserquestion: {
87
94
  enabled: false,
88
95
  valid: true,
@@ -98,6 +105,22 @@ function detectConfig(version) {
98
105
  version: null,
99
106
  outdated: false,
100
107
  },
108
+ browserqa: {
109
+ enabled: false,
110
+ valid: true,
111
+ issues: [],
112
+ version: null,
113
+ outdated: false,
114
+ playwright_detected: false,
115
+ },
116
+ contextverbosity: {
117
+ enabled: false,
118
+ valid: true,
119
+ issues: [],
120
+ version: null,
121
+ outdated: false,
122
+ mode: 'full',
123
+ },
101
124
  },
102
125
  metadata: { exists: false, version: null },
103
126
  currentVersion: version,
@@ -225,7 +248,7 @@ function detectStopHooks(hook, status) {
225
248
  }
226
249
 
227
250
  /**
228
- * Detect PreToolUse hooks (damage control)
251
+ * Detect PreToolUse hooks (damage control, no AI attribution)
229
252
  */
230
253
  function detectPreToolUseHooks(hooks, status) {
231
254
  if (!Array.isArray(hooks) || hooks.length === 0) return;
@@ -248,6 +271,17 @@ function detectPreToolUseHooks(hooks, status) {
248
271
  status.features.damagecontrol.issues.push(`Only ${hookCount}/3 hooks configured`);
249
272
  }
250
273
  }
274
+
275
+ // Detect no AI attribution hook
276
+ const hasNoAiHook = hooks.some(
277
+ h =>
278
+ h.matcher === 'Bash' &&
279
+ Array.isArray(h.hooks) &&
280
+ h.hooks.some(hk => hk.command?.includes('strip-ai-attribution'))
281
+ );
282
+ if (hasNoAiHook) {
283
+ status.features.noaiattribution.enabled = true;
284
+ }
251
285
  }
252
286
 
253
287
  /**
@@ -303,9 +337,31 @@ function detectMetadata(status, version) {
303
337
  status.features.tmuxautospawn.enabled = true; // Default enabled
304
338
  }
305
339
 
340
+ // No AI attribution metadata
341
+ if (meta.features?.noaiattribution?.enabled) {
342
+ status.features.noaiattribution.enabled = true;
343
+ }
344
+
345
+ // Browser QA metadata
346
+ if (meta.features?.browserqa?.enabled) {
347
+ status.features.browserqa.enabled = true;
348
+ status.features.browserqa.playwright_detected =
349
+ meta.features.browserqa.playwright_detected || false;
350
+ }
351
+
352
+ // Context verbosity metadata
353
+ if (meta.features?.contextVerbosity?.enabled) {
354
+ status.features.contextverbosity.enabled = true;
355
+ status.features.contextverbosity.mode = meta.features.contextVerbosity.mode || 'full';
356
+ }
357
+
306
358
  // Read feature versions and check if outdated (content-based)
307
359
  if (meta.features) {
308
- const featureKeyMap = { askUserQuestion: 'askuserquestion', tmuxAutoSpawn: 'tmuxautospawn' };
360
+ const featureKeyMap = {
361
+ askUserQuestion: 'askuserquestion',
362
+ tmuxAutoSpawn: 'tmuxautospawn',
363
+ contextVerbosity: 'contextverbosity',
364
+ };
309
365
  const packageScriptDir = findPackageScriptDir();
310
366
 
311
367
  Object.entries(meta.features).forEach(([feature, data]) => {
@@ -434,6 +490,14 @@ function printStatus(status) {
434
490
  log(` Damage Control: disabled`, c.dim);
435
491
  }
436
492
 
493
+ // No AI Attribution
494
+ const naa = status.features.noaiattribution;
495
+ if (naa.enabled) {
496
+ log(` No AI Attribution: enabled`, c.green);
497
+ } else {
498
+ log(` No AI Attribution: disabled`, c.dim);
499
+ }
500
+
437
501
  // AskUserQuestion
438
502
  const auq = status.features.askuserquestion;
439
503
  if (auq.enabled) {
@@ -452,6 +516,32 @@ function printStatus(status) {
452
516
  log(` Tmux Auto-Spawn: disabled`, c.dim);
453
517
  }
454
518
 
519
+ // Browser QA
520
+ const bqa = status.features.browserqa;
521
+ if (bqa) {
522
+ if (bqa.enabled) {
523
+ let bqaText = 'enabled';
524
+ if (bqa.playwright_detected) bqaText += ' (Playwright detected)';
525
+ else bqaText += ' (Playwright not found)';
526
+ log(
527
+ ` ${bqa.playwright_detected ? '' : ''} Browser QA: ${bqaText}`,
528
+ bqa.playwright_detected ? c.green : c.yellow
529
+ );
530
+ } else {
531
+ log(` Browser QA: disabled`, c.dim);
532
+ }
533
+ }
534
+
535
+ // Context Verbosity
536
+ const cv = status.features.contextverbosity;
537
+ if (cv) {
538
+ if (cv.enabled) {
539
+ log(` Context Verbosity: ${cv.mode}`, c.green);
540
+ } else {
541
+ log(` Context Verbosity: full (default)`, c.dim);
542
+ }
543
+ }
544
+
455
545
  // Metadata version
456
546
  if (status.metadata.exists) {
457
547
  log(`\nMetadata: v${status.metadata.version}`, c.dim);