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.
- package/CHANGELOG.md +10 -0
- package/README.md +58 -86
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/feedback.js +9 -2
- package/lib/lazy-require.js +59 -0
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +14 -2
- package/scripts/agileflow-welcome.js +450 -459
- package/scripts/claude-tmux.sh +113 -5
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +411 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +90 -0
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/legal-analyzer-a11y.md +110 -0
- package/src/core/agents/legal-analyzer-ai.md +117 -0
- package/src/core/agents/legal-analyzer-consumer.md +108 -0
- package/src/core/agents/legal-analyzer-content.md +113 -0
- package/src/core/agents/legal-analyzer-international.md +115 -0
- package/src/core/agents/legal-analyzer-licensing.md +115 -0
- package/src/core/agents/legal-analyzer-privacy.md +108 -0
- package/src/core/agents/legal-analyzer-security.md +112 -0
- package/src/core/agents/legal-analyzer-terms.md +111 -0
- package/src/core/agents/legal-consensus.md +242 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/team-lead.md +50 -13
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/audit/legal.md +446 -0
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/configure.md +18 -33
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -67
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -75
- package/scripts/tmux-task-watcher.sh +0 -177
package/scripts/claude-tmux.sh
CHANGED
|
@@ -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
|
|
193
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
45
|
-
|
|
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 = {
|
|
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);
|