agileflow 3.0.1 → 3.1.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 (69) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +8 -8
  3. package/lib/api-server.js +3 -2
  4. package/lib/feedback.js +9 -2
  5. package/lib/flag-detection.js +4 -2
  6. package/lib/git-operations.js +4 -2
  7. package/lib/lazy-require.js +59 -0
  8. package/lib/process-executor.js +24 -9
  9. package/lib/skill-loader.js +11 -3
  10. package/package.json +1 -1
  11. package/scripts/agileflow-configure.js +12 -0
  12. package/scripts/agileflow-welcome.js +146 -90
  13. package/scripts/claude-tmux.sh +42 -6
  14. package/scripts/damage-control-multi-agent.js +14 -10
  15. package/scripts/lib/bus-utils.js +3 -1
  16. package/scripts/lib/configure-detect.js +12 -9
  17. package/scripts/lib/configure-features.js +128 -7
  18. package/scripts/lib/configure-repair.js +6 -5
  19. package/scripts/lib/context-formatter.js +13 -3
  20. package/scripts/lib/damage-control-utils.js +5 -1
  21. package/scripts/lib/lifecycle-detector.js +5 -3
  22. package/scripts/lib/process-cleanup.js +8 -4
  23. package/scripts/lib/scale-detector.js +47 -8
  24. package/scripts/lib/signal-detectors.js +117 -59
  25. package/scripts/lib/task-registry.js +5 -1
  26. package/scripts/lib/team-events.js +4 -4
  27. package/scripts/messaging-bridge.js +7 -1
  28. package/scripts/ralph-loop.js +10 -8
  29. package/scripts/smart-detect.js +32 -11
  30. package/scripts/team-manager.js +86 -1
  31. package/scripts/tmux-task-name.sh +105 -0
  32. package/scripts/tmux-task-watcher.sh +344 -0
  33. package/src/core/agents/legal-analyzer-a11y.md +110 -0
  34. package/src/core/agents/legal-analyzer-ai.md +117 -0
  35. package/src/core/agents/legal-analyzer-consumer.md +108 -0
  36. package/src/core/agents/legal-analyzer-content.md +113 -0
  37. package/src/core/agents/legal-analyzer-international.md +115 -0
  38. package/src/core/agents/legal-analyzer-licensing.md +115 -0
  39. package/src/core/agents/legal-analyzer-privacy.md +108 -0
  40. package/src/core/agents/legal-analyzer-security.md +112 -0
  41. package/src/core/agents/legal-analyzer-terms.md +111 -0
  42. package/src/core/agents/legal-consensus.md +242 -0
  43. package/src/core/agents/team-lead.md +50 -13
  44. package/src/core/commands/babysit.md +75 -42
  45. package/src/core/commands/blockers.md +7 -7
  46. package/src/core/commands/configure.md +15 -61
  47. package/src/core/commands/discovery/brief.md +363 -0
  48. package/src/core/commands/discovery/new.md +395 -0
  49. package/src/core/commands/ideate/new.md +5 -5
  50. package/src/core/commands/legal/audit.md +446 -0
  51. package/src/core/commands/logic/audit.md +5 -5
  52. package/src/core/commands/review.md +7 -1
  53. package/src/core/commands/rpi.md +61 -26
  54. package/src/core/commands/sprint.md +7 -6
  55. package/src/core/commands/team/start.md +36 -7
  56. package/src/core/commands/team/stop.md +5 -2
  57. package/src/core/templates/product-brief.md +136 -0
  58. package/tools/cli/installers/ide/claude-code.js +69 -2
  59. package/src/core/agents/configuration/archival.md +0 -350
  60. package/src/core/agents/configuration/attribution.md +0 -343
  61. package/src/core/agents/configuration/ci.md +0 -1103
  62. package/src/core/agents/configuration/damage-control.md +0 -375
  63. package/src/core/agents/configuration/git-config.md +0 -537
  64. package/src/core/agents/configuration/hooks.md +0 -623
  65. package/src/core/agents/configuration/precompact.md +0 -302
  66. package/src/core/agents/configuration/status-line.md +0 -557
  67. package/src/core/agents/configuration/verify.md +0 -618
  68. package/src/core/agents/configuration-damage-control.md +0 -259
  69. package/src/core/agents/configuration-visual-e2e.md +0 -339
@@ -128,16 +128,43 @@ function listTemplates(rootDir) {
128
128
  return { ok: true, templates };
129
129
  }
130
130
 
131
+ /**
132
+ * Validate template name to prevent path traversal.
133
+ * Only allows alphanumeric characters, hyphens, and underscores.
134
+ */
135
+ function validateTemplateName(name) {
136
+ if (typeof name !== 'string' || name.length === 0 || name.length > 255) {
137
+ return { valid: false, error: 'Template name must be 1-255 characters' };
138
+ }
139
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
140
+ return { valid: false, error: 'Template name must contain only alphanumeric characters, hyphens, and underscores' };
141
+ }
142
+ return { valid: true };
143
+ }
144
+
131
145
  /**
132
146
  * Get a specific template by name
133
147
  */
134
148
  function getTemplate(rootDir, name) {
149
+ const validation = validateTemplateName(name);
150
+ if (!validation.valid) {
151
+ return { ok: false, error: validation.error };
152
+ }
153
+
135
154
  const teamsDir = getTeamsDir(rootDir);
136
155
  if (!teamsDir) {
137
156
  return { ok: false, error: 'No teams directory found' };
138
157
  }
139
158
 
140
159
  const filePath = path.join(teamsDir, `${name}.json`);
160
+
161
+ // Defense-in-depth: verify resolved path stays within teams directory
162
+ const resolvedPath = path.resolve(filePath);
163
+ const resolvedTeamsDir = path.resolve(teamsDir);
164
+ if (!resolvedPath.startsWith(resolvedTeamsDir + path.sep)) {
165
+ return { ok: false, error: 'Invalid template path' };
166
+ }
167
+
141
168
  if (!fs.existsSync(filePath)) {
142
169
  return { ok: false, error: `Template "${name}" not found` };
143
170
  }
@@ -172,6 +199,62 @@ function buildNativeTeamPayload(template, templateName) {
172
199
  };
173
200
  }
174
201
 
202
+ /**
203
+ * Build a rich prompt for a teammate from template data.
204
+ * Used when spawning teammates via the Task tool to give them
205
+ * full context about their role, quality gates, and project state.
206
+ *
207
+ * @param {object} teammate - Teammate entry from template (agent, role, domain, description, instructions)
208
+ * @param {object} template - Full team template (for quality_gates context)
209
+ * @returns {string} Formatted prompt string
210
+ */
211
+ function buildTeammatePrompt(teammate, template) {
212
+ const parts = [];
213
+
214
+ // Role and domain header
215
+ parts.push(`## Role: ${teammate.role || 'teammate'} (${teammate.domain || 'general'})`);
216
+ parts.push('');
217
+
218
+ // Instructions - prefer explicit instructions, fall back to description, then auto-generate
219
+ if (teammate.instructions) {
220
+ parts.push(teammate.instructions);
221
+ } else if (teammate.description) {
222
+ parts.push(teammate.description);
223
+ } else {
224
+ parts.push(`You are the ${teammate.role || 'teammate'} agent responsible for the ${teammate.domain || 'general'} domain.`);
225
+ }
226
+ parts.push('');
227
+
228
+ // Quality gate awareness
229
+ if (template && template.quality_gates) {
230
+ const gates = template.quality_gates;
231
+ const requirements = [];
232
+
233
+ if (gates.teammate_idle) {
234
+ if (gates.teammate_idle.tests) requirements.push('tests must pass');
235
+ if (gates.teammate_idle.lint) requirements.push('linting must pass');
236
+ if (gates.teammate_idle.types) requirements.push('type checking must pass');
237
+ }
238
+ if (gates.task_completed && gates.task_completed.require_validator_approval) {
239
+ requirements.push('validator approval required');
240
+ }
241
+
242
+ if (requirements.length > 0) {
243
+ parts.push('## Quality Gates');
244
+ parts.push(`Before marking work complete: ${requirements.join(', ')}.`);
245
+ parts.push('');
246
+ }
247
+ }
248
+
249
+ // Project context pointers
250
+ parts.push('## Context');
251
+ parts.push('- Read CLAUDE.md for project conventions');
252
+ parts.push('- Check `docs/09-agents/status.json` for current work items and team state');
253
+ parts.push('');
254
+
255
+ return parts.join('\n');
256
+ }
257
+
175
258
  /**
176
259
  * Start a team from a template.
177
260
  * When native Agent Teams is enabled, builds a TeamCreate-compatible payload.
@@ -204,7 +287,7 @@ function startTeam(rootDir, templateName) {
204
287
  const sessionStatePath = paths.getSessionStatePath(rootDir);
205
288
  const fileLock = getFileLock();
206
289
 
207
- const updateState = (state) => {
290
+ const updateState = state => {
208
291
  state.active_team = {
209
292
  template: templateName,
210
293
  mode,
@@ -458,6 +541,8 @@ module.exports = {
458
541
  stopTeam,
459
542
  getTeamsDir,
460
543
  buildNativeTeamPayload,
544
+ buildTeammatePrompt,
545
+ validateTemplateName,
461
546
  };
462
547
 
463
548
  // Run CLI if invoked directly
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # tmux-task-name.sh - Rename current tmux window based on task/work description
3
+ #
4
+ # Called by Claude Code when starting work on a task (via TaskCreate/TaskUpdate).
5
+ # Reads the task subject from ~/.claude/tasks/ or accepts it as an argument.
6
+ #
7
+ # Usage:
8
+ # tmux-task-name.sh "Fix auth middleware" # Rename to task subject
9
+ # tmux-task-name.sh --scan # Auto-detect from task files
10
+ # tmux-task-name.sh --scan --session <UUID> # Scan only one session's tasks
11
+ # tmux-task-name.sh --reset # Reset to default "claude-N"
12
+ #
13
+ # The script is best-effort: silently exits if not inside tmux.
14
+
15
+ set -euo pipefail
16
+
17
+ # Exit silently if not in tmux
18
+ [ -n "${TMUX:-}" ] || exit 0
19
+
20
+ MAX_LEN=30
21
+
22
+ truncate_name() {
23
+ local name="$1"
24
+ if [ ${#name} -gt $MAX_LEN ]; then
25
+ echo "${name:0:$((MAX_LEN - 1))}…"
26
+ else
27
+ echo "$name"
28
+ fi
29
+ }
30
+
31
+ # Mode: reset to default sequential name
32
+ if [ "${1:-}" = "--reset" ]; then
33
+ N=$(( $(tmux list-windows -F '#{window_name}' 2>/dev/null | grep -c '^claude') ))
34
+ [ "$N" -eq 0 ] && N=1
35
+ tmux rename-window "claude-$N" 2>/dev/null || true
36
+ exit 0
37
+ fi
38
+
39
+ # Mode: scan ~/.claude/tasks/ for most recently modified in-progress task
40
+ if [ "${1:-}" = "--scan" ]; then
41
+ TASKS_BASE="${HOME}/.claude/tasks"
42
+ [ -d "$TASKS_BASE" ] || exit 0
43
+
44
+ # Determine session scope: --session param, pane option, or global scan
45
+ SESSION_ID=""
46
+ if [ "${2:-}" = "--session" ] && [ -n "${3:-}" ]; then
47
+ # Validate session ID is alphanumeric + hyphens only (prevent path traversal)
48
+ if [[ "$3" =~ ^[a-zA-Z0-9_-]+$ ]]; then
49
+ SESSION_ID="$3"
50
+ fi
51
+ elif [ -n "${TMUX:-}" ]; then
52
+ SESSION_ID=$(tmux show-options -pqv @claude_session_id 2>/dev/null || true)
53
+ fi
54
+
55
+ BEST_SUBJECT=""
56
+ BEST_MTIME=0
57
+
58
+ if [ -n "$SESSION_ID" ]; then
59
+ # Scoped scan: only look at this session's tasks
60
+ SCAN_DIR="$TASKS_BASE/$SESSION_ID"
61
+ if [ -d "$SCAN_DIR" ]; then
62
+ for f in "$SCAN_DIR"/*.json; do
63
+ [ -f "$f" ] || continue
64
+ status=$(python3 -c "import json,sys; d=json.load(open('$f')); print(d.get('status',''))" 2>/dev/null || echo "")
65
+ if [ "$status" = "in_progress" ]; then
66
+ mtime=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null || echo "0")
67
+ if [ "$mtime" -gt "$BEST_MTIME" ]; then
68
+ BEST_MTIME=$mtime
69
+ BEST_SUBJECT=$(python3 -c "import json; d=json.load(open('$f')); print(d.get('subject',''))" 2>/dev/null || echo "")
70
+ fi
71
+ fi
72
+ done
73
+ fi
74
+ else
75
+ # Global scan: all sessions (fallback for no session context)
76
+ for dir in "$TASKS_BASE"/*/; do
77
+ [ -d "$dir" ] || continue
78
+ for f in "$dir"*.json; do
79
+ [ -f "$f" ] || continue
80
+ status=$(python3 -c "import json,sys; d=json.load(open('$f')); print(d.get('status',''))" 2>/dev/null || echo "")
81
+ if [ "$status" = "in_progress" ]; then
82
+ mtime=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null || echo "0")
83
+ if [ "$mtime" -gt "$BEST_MTIME" ]; then
84
+ BEST_MTIME=$mtime
85
+ BEST_SUBJECT=$(python3 -c "import json; d=json.load(open('$f')); print(d.get('subject',''))" 2>/dev/null || echo "")
86
+ fi
87
+ fi
88
+ done
89
+ done
90
+ fi
91
+
92
+ if [ -n "$BEST_SUBJECT" ]; then
93
+ tmux rename-window "$(truncate_name "$BEST_SUBJECT")" 2>/dev/null || true
94
+ fi
95
+ exit 0
96
+ fi
97
+
98
+ # Mode: direct - rename to provided argument
99
+ if [ -n "${1:-}" ]; then
100
+ tmux rename-window "$(truncate_name "$1")" 2>/dev/null || true
101
+ exit 0
102
+ fi
103
+
104
+ echo "Usage: tmux-task-name.sh <task-subject> | --scan | --reset" >&2
105
+ exit 1
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bash
2
+ # tmux-task-watcher.sh - Auto-rename tmux window based on Claude Code tasks
3
+ #
4
+ # Launched automatically by SessionStart hook. Self-backgrounds immediately.
5
+ # Polls ~/.claude/tasks/<session-id>/ for in-progress tasks every few seconds.
6
+ # Renames the tmux window to match the active task subject.
7
+ #
8
+ # Usage:
9
+ # tmux-task-watcher.sh # Start watcher (backgrounds itself)
10
+ # tmux-task-watcher.sh stop # Stop watcher for current pane
11
+ #
12
+ # Requirements:
13
+ # - Must be inside tmux ($TMUX set)
14
+ # - Node.js available (for JSON parsing)
15
+ # - Claude Code session active ($CLAUDECODE=1)
16
+
17
+ set -euo pipefail
18
+
19
+ # Only run inside tmux
20
+ [ -n "${TMUX:-}" ] || exit 0
21
+
22
+ MODE="${1:-start}"
23
+ MAX_LEN=30
24
+
25
+ # Get current pane ID for per-pane tracking
26
+ PANE_ID=$(tmux display-message -p '#{pane_id}' 2>/dev/null || true)
27
+ [ -n "$PANE_ID" ] || exit 0
28
+
29
+ # PID file keyed by pane to allow multiple watchers (one per window)
30
+ SAFE_PANE_ID="${PANE_ID//[^a-zA-Z0-9]/_}"
31
+ PID_FILE="/tmp/tmux-task-watcher-${SAFE_PANE_ID}.pid"
32
+
33
+ # Claim file: marks which pane owns which session to prevent cross-contamination
34
+ CLAIM_DIR="/tmp/tmux-session-claims"
35
+
36
+ cleanup_claims() {
37
+ [ -d "$CLAIM_DIR" ] || return 0
38
+ for cf in "$CLAIM_DIR"/claim-*; do
39
+ [ -f "$cf" ] || continue
40
+ local claimer
41
+ claimer=$(cat "$cf" 2>/dev/null || true)
42
+ if [ "$claimer" = "$PANE_ID" ]; then
43
+ rm -f "$cf"
44
+ fi
45
+ done
46
+ }
47
+
48
+ # --- Stop mode ---
49
+ if [ "$MODE" = "stop" ]; then
50
+ if [ -f "$PID_FILE" ]; then
51
+ kill "$(cat "$PID_FILE")" 2>/dev/null || true
52
+ rm "$PID_FILE" 2>/dev/null || true
53
+ fi
54
+ cleanup_claims
55
+ exit 0
56
+ fi
57
+
58
+ # --- Start mode ---
59
+ if [ "$MODE" = "start" ]; then
60
+ # Kill any existing watcher for this pane first
61
+ if [ -f "$PID_FILE" ]; then
62
+ kill "$(cat "$PID_FILE")" 2>/dev/null || true
63
+ rm "$PID_FILE" 2>/dev/null || true
64
+ fi
65
+
66
+ # Self-background: parent exits immediately so hook completes fast
67
+ _WATCHER_BG=1 nohup "$0" "_run" >/dev/null 2>&1 &
68
+ echo $! > "$PID_FILE"
69
+ exit 0
70
+ fi
71
+
72
+ # --- _run mode: skip to background worker below ---
73
+
74
+ # ============================================================
75
+ # Background watcher logic (runs in forked process)
76
+ # ============================================================
77
+
78
+ # Disable strict mode for background worker - we handle errors ourselves
79
+ set +eu
80
+
81
+ # Derive Claude Code project dir from PWD
82
+ CLAUDE_PROJECT_DIR="${HOME}/.claude/projects/$(pwd | sed 's|/|-|g')"
83
+
84
+ # --- Session claim helpers ---
85
+ mkdir -p "$CLAIM_DIR" 2>/dev/null || true
86
+
87
+ claim_session() {
88
+ local sid="$1"
89
+ echo "$PANE_ID" > "$CLAIM_DIR/claim-${sid}" 2>/dev/null || true
90
+ }
91
+
92
+ is_claimed_by_other() {
93
+ local sid="$1"
94
+ local cf="$CLAIM_DIR/claim-${sid}"
95
+ [ -f "$cf" ] || return 1 # not claimed
96
+ local claimer
97
+ claimer=$(cat "$cf" 2>/dev/null || true)
98
+ # Empty or unreadable claim file - treat as unclaimed
99
+ [ -n "$claimer" ] || return 1
100
+ [ "$claimer" != "$PANE_ID" ] || return 1 # claimed by us = not "other"
101
+ # Check if claimer pane is still alive
102
+ if tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qF "$claimer"; then
103
+ return 0 # claimed by another live pane
104
+ fi
105
+ # Claimer is dead - remove stale claim
106
+ rm -f "$cf"
107
+ return 1
108
+ }
109
+
110
+ # Find Claude PID running inside this tmux pane
111
+ find_claude_pid() {
112
+ local pane_pid
113
+ pane_pid=$(tmux display-message -p -t "$PANE_ID" '#{pane_pid}' 2>/dev/null || true)
114
+ [ -n "$pane_pid" ] || return 1
115
+
116
+ # Walk descendants of the pane shell looking for claude/claude-code process
117
+ # Process tree: pane_shell -> (possibly bash/claude-smart.sh) -> claude
118
+ local candidates
119
+ candidates=$(ps -eo pid,ppid,comm 2>/dev/null | awk -v root="$pane_pid" '
120
+ BEGIN { pids[root]=1 }
121
+ { child=$1; parent=$2; comm=$3; children[parent]=children[parent] " " child; comms[child]=comm }
122
+ END {
123
+ # BFS through process tree
124
+ queue[1]=root; qi=1; qn=1
125
+ while (qi <= qn) {
126
+ p = queue[qi++]
127
+ n = split(children[p], kids, " ")
128
+ for (i=1; i<=n; i++) {
129
+ if (kids[i] != "") {
130
+ qn++; queue[qn] = kids[i]
131
+ pids[kids[i]] = 1
132
+ }
133
+ }
134
+ }
135
+ for (p in pids) {
136
+ c = comms[p]
137
+ if (c == "claude" || c == "claude-code" || c ~ /^node.*claude/) {
138
+ print p
139
+ }
140
+ }
141
+ }
142
+ ' || true)
143
+
144
+ # Return the first match
145
+ echo "$candidates" | head -n1 | tr -d '[:space:]'
146
+ }
147
+
148
+ # Get process start time (epoch seconds) from /proc
149
+ get_pid_start_time() {
150
+ local pid="$1"
151
+ # /proc/<pid>/stat field 22 is starttime in clock ticks since boot
152
+ # Simpler: use stat on /proc/<pid> - ctime ≈ process creation
153
+ if [ -d "/proc/$pid" ]; then
154
+ stat -c '%Z' "/proc/$pid" 2>/dev/null && return
155
+ fi
156
+ # Fallback: ps-based start time
157
+ ps -o lstart= -p "$pid" 2>/dev/null | xargs -I{} date -d '{}' +%s 2>/dev/null || echo "0"
158
+ }
159
+
160
+ # --- 3-tier session finder ---
161
+ # Tier 1: Check tmux pane option (set by previous run or claude-smart.sh)
162
+ # Tier 2: Correlate Claude PID start time with JSONL birth time
163
+ # Tier 3: Newest unclaimed JSONL as fallback
164
+ find_session_id() {
165
+ [ -d "$CLAUDE_PROJECT_DIR" ] || return 1
166
+
167
+ # -- Tier 1: Cached pane option --
168
+ local cached_id
169
+ cached_id=$(tmux show-options -pqv @claude_session_id 2>/dev/null || true)
170
+ if [ -n "$cached_id" ]; then
171
+ local cached_dir="${HOME}/.claude/tasks/${cached_id}"
172
+ if [ -d "$cached_dir" ] || [ -f "$CLAUDE_PROJECT_DIR/${cached_id}.jsonl" ]; then
173
+ # Verify not claimed by another live pane
174
+ if ! is_claimed_by_other "$cached_id"; then
175
+ echo "$cached_id"
176
+ return 0
177
+ fi
178
+ fi
179
+ fi
180
+ # Also check @claude_uuid (set by claude-smart.sh)
181
+ cached_id=$(tmux show-options -pqv @claude_uuid 2>/dev/null || true)
182
+ if [ -n "$cached_id" ] && [ -f "$CLAUDE_PROJECT_DIR/${cached_id}.jsonl" ]; then
183
+ if ! is_claimed_by_other "$cached_id"; then
184
+ echo "$cached_id"
185
+ return 0
186
+ fi
187
+ fi
188
+
189
+ # -- Tier 2: PID-to-JSONL time correlation --
190
+ local claude_pid
191
+ claude_pid=$(find_claude_pid || true)
192
+ if [ -n "$claude_pid" ]; then
193
+ local pid_start
194
+ pid_start=$(get_pid_start_time "$claude_pid" || echo "0")
195
+ if [ "$pid_start" -gt 0 ] 2>/dev/null; then
196
+ local best_file="" best_delta=999999
197
+ for f in "$CLAUDE_PROJECT_DIR"/*.jsonl; do
198
+ [ -f "$f" ] || continue
199
+ local birth
200
+ birth=$(stat -c '%W' "$f" 2>/dev/null || echo "0")
201
+ [ "$birth" -gt 0 ] 2>/dev/null || continue
202
+ # JSONL should be born AFTER the PID started (within 120s window)
203
+ local delta=$(( birth - pid_start ))
204
+ if [ "$delta" -ge 0 ] && [ "$delta" -le 120 ] && [ "$delta" -lt "$best_delta" ]; then
205
+ best_delta=$delta
206
+ best_file=$f
207
+ fi
208
+ done
209
+ if [ -n "$best_file" ]; then
210
+ basename "$best_file" .jsonl
211
+ return 0
212
+ fi
213
+ fi
214
+ fi
215
+
216
+ # -- Tier 3: Newest unclaimed JSONL --
217
+ local best_file="" best_birth=0
218
+ for f in "$CLAUDE_PROJECT_DIR"/*.jsonl; do
219
+ [ -f "$f" ] || continue
220
+ local sid
221
+ sid=$(basename "$f" .jsonl)
222
+ # Skip if claimed by another live pane
223
+ if is_claimed_by_other "$sid"; then
224
+ continue
225
+ fi
226
+ local birth
227
+ birth=$(stat -c '%W' "$f" 2>/dev/null || echo "0")
228
+ if [ "$birth" -gt "$best_birth" ] 2>/dev/null; then
229
+ best_birth=$birth
230
+ best_file=$f
231
+ fi
232
+ done
233
+ [ -n "$best_file" ] || return 1
234
+ basename "$best_file" .jsonl
235
+ }
236
+
237
+ # Wait briefly for session file to be created/updated
238
+ sleep 2
239
+
240
+ SESSION_ID=$(find_session_id || true)
241
+ if [ -z "$SESSION_ID" ]; then
242
+ # Retry after a longer wait
243
+ sleep 5
244
+ SESSION_ID=$(find_session_id || true)
245
+ fi
246
+
247
+ # Store session ID and claim it for this pane
248
+ if [ -n "$SESSION_ID" ]; then
249
+ tmux set-option -p @claude_session_id "$SESSION_ID" 2>/dev/null || true
250
+ claim_session "$SESSION_ID"
251
+ fi
252
+
253
+ # Only set TASKS_DIR if we have a valid session (avoid scanning parent dir)
254
+ TASKS_DIR=""
255
+ if [ -n "$SESSION_ID" ]; then
256
+ TASKS_DIR="${HOME}/.claude/tasks/${SESSION_ID}"
257
+ fi
258
+ LAST_WINDOW_NAME=""
259
+ POLL_INTERVAL=5
260
+ MAX_RUNTIME=$((12 * 3600)) # 12 hours safety limit
261
+ START_TIME=$(date +%s)
262
+
263
+ # Parse task files and find the most recent in-progress task subject
264
+ get_active_task() {
265
+ [ -d "$TASKS_DIR" ] || return
266
+ node -e "
267
+ const fs = require('fs'), path = require('path');
268
+ const dir = process.argv[1];
269
+ let best = { mtime: 0, subject: '' };
270
+ try {
271
+ for (const f of fs.readdirSync(dir)) {
272
+ if (!f.endsWith('.json') || f === '.lock') continue;
273
+ const fp = path.join(dir, f);
274
+ const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
275
+ if (data.status === 'in_progress') {
276
+ const mt = fs.statSync(fp).mtimeMs;
277
+ if (mt > best.mtime) {
278
+ best = { mtime: mt, subject: data.subject || data.activeForm || '' };
279
+ }
280
+ }
281
+ }
282
+ } catch(e) {}
283
+ if (best.subject) process.stdout.write(best.subject);
284
+ " "$TASKS_DIR" 2>/dev/null || true
285
+ }
286
+
287
+ truncate_name() {
288
+ local name="$1"
289
+ if [ ${#name} -gt $MAX_LEN ]; then
290
+ printf '%s' "${name:0:$((MAX_LEN - 1))}"
291
+ else
292
+ printf '%s' "$name"
293
+ fi
294
+ }
295
+
296
+ # Main polling loop
297
+ while true; do
298
+ # Safety timeout
299
+ NOW=$(date +%s)
300
+ if [ $((NOW - START_TIME)) -gt $MAX_RUNTIME ]; then
301
+ break
302
+ fi
303
+
304
+ # Check if our pane is still alive
305
+ if ! tmux list-panes -a -F '#{pane_id}' 2>/dev/null | grep -qF "$PANE_ID"; then
306
+ break
307
+ fi
308
+
309
+ # Re-validate that our session hasn't been claimed by another pane
310
+ if [ -n "$SESSION_ID" ] && is_claimed_by_other "$SESSION_ID"; then
311
+ SESSION_ID=""
312
+ TASKS_DIR=""
313
+ fi
314
+
315
+ # If we don't have a session ID yet, retry finding it
316
+ if [ -z "$SESSION_ID" ]; then
317
+ SESSION_ID=$(find_session_id || true)
318
+ if [ -n "$SESSION_ID" ]; then
319
+ tmux set-option -p @claude_session_id "$SESSION_ID" 2>/dev/null || true
320
+ claim_session "$SESSION_ID"
321
+ TASKS_DIR="${HOME}/.claude/tasks/${SESSION_ID}"
322
+ fi
323
+ fi
324
+
325
+ # Get active task subject (only if we have a valid session)
326
+ ACTIVE_TASK=""
327
+ if [ -n "$TASKS_DIR" ]; then
328
+ ACTIVE_TASK=$(get_active_task 2>/dev/null || true)
329
+ fi
330
+
331
+ if [ -n "$ACTIVE_TASK" ]; then
332
+ NEW_NAME=$(truncate_name "$ACTIVE_TASK")
333
+ if [ "$NEW_NAME" != "$LAST_WINDOW_NAME" ]; then
334
+ tmux rename-window -t "$PANE_ID" "$NEW_NAME" 2>/dev/null || true
335
+ LAST_WINDOW_NAME="$NEW_NAME"
336
+ fi
337
+ fi
338
+
339
+ sleep "$POLL_INTERVAL"
340
+ done
341
+
342
+ # Cleanup
343
+ cleanup_claims
344
+ rm -f "$PID_FILE"
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: legal-analyzer-a11y
3
+ description: Accessibility compliance analyzer for ADA, Section 508, and WCAG violations that trigger lawsuits
4
+ tools: Read, Glob, Grep
5
+ model: haiku
6
+ team_role: utility
7
+ ---
8
+
9
+
10
+ # Legal Analyzer: Accessibility Compliance
11
+
12
+ You are a specialized legal risk analyzer focused on **accessibility violations that trigger ADA and Section 508 lawsuits**. Your job is to find WCAG compliance gaps that create legal liability, not just UX improvements.
13
+
14
+ ---
15
+
16
+ ## Your Focus Areas
17
+
18
+ 1. **Images without alt text**: Missing alt attributes on images (WCAG 1.1.1)
19
+ 2. **Forms without labels**: Input fields without associated labels (WCAG 1.3.1)
20
+ 3. **Keyboard navigation**: Interactive elements not keyboard-accessible (WCAG 2.1.1)
21
+ 4. **Color contrast**: Insufficient contrast ratios in styles (WCAG 1.4.3)
22
+ 5. **ARIA attributes**: Missing ARIA on interactive/dynamic elements (WCAG 4.1.2)
23
+ 6. **Skip navigation**: No skip-to-content link (WCAG 2.4.1)
24
+ 7. **Media accessibility**: Videos/audio without captions or transcripts (WCAG 1.2.1)
25
+ 8. **Language declaration**: Missing lang attribute on HTML element (WCAG 3.1.1)
26
+
27
+ ---
28
+
29
+ ## Analysis Process
30
+
31
+ ### Step 1: Read the Target Code
32
+
33
+ Read the files you're asked to analyze. Focus on:
34
+ - HTML templates and JSX components
35
+ - CSS/style files (color values, contrast)
36
+ - Form components
37
+ - Navigation and layout components
38
+ - Media embedding code
39
+
40
+ ### Step 2: Look for These Patterns
41
+
42
+ **Pattern 1: Images without alt text**
43
+ ```jsx
44
+ // RISK: ADA lawsuit - decorative and content images must have alt
45
+ <img src={product.image} />
46
+ <img src="/hero.jpg" className="banner" />
47
+ ```
48
+
49
+ **Pattern 2: Form inputs without labels**
50
+ ```jsx
51
+ // RISK: Screen readers cannot identify form fields
52
+ <input type="text" placeholder="Search..." />
53
+ <input type="email" name="email" />
54
+ // No <label> or aria-label associated
55
+ ```
56
+
57
+ **Pattern 3: Click handlers on non-interactive elements**
58
+ ```jsx
59
+ // RISK: Keyboard users cannot activate this element
60
+ <div onClick={handleClick} className="card">
61
+ {content}
62
+ </div>
63
+ // Missing role="button", tabIndex, onKeyDown
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Output Format
69
+
70
+ For each potential issue found, output:
71
+
72
+ ```markdown
73
+ ### FINDING-{N}: {Brief Title}
74
+
75
+ **Location**: `{file}:{line}`
76
+ **Risk Level**: CRITICAL (lawsuit risk) | HIGH (regulatory fine) | MEDIUM (best practice gap) | LOW (advisory)
77
+ **Confidence**: HIGH | MEDIUM | LOW
78
+ **Legal Basis**: {ADA Title III / Section 508 / WCAG 2.1 Level AA criterion X.X.X / EN 301 549}
79
+
80
+ **Code**:
81
+ \`\`\`{language}
82
+ {relevant code snippet, 3-7 lines}
83
+ \`\`\`
84
+
85
+ **Issue**: {Clear explanation of the accessibility violation and legal risk}
86
+
87
+ **Remediation**:
88
+ - {Specific step to fix the issue}
89
+ - {Additional steps if needed}
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Important Rules
95
+
96
+ 1. **Be SPECIFIC**: Include exact file paths, line numbers, and WCAG criterion
97
+ 2. **Focus on legal risk**: Prioritize issues that trigger actual lawsuits (images, forms, keyboard access)
98
+ 3. **Verify before reporting**: Check if aria-label or sr-only text exists nearby
99
+ 4. **Count instances**: Note how many occurrences exist (systemic vs isolated)
100
+ 5. **Consider component patterns**: A missing alt in a reusable component affects every usage
101
+
102
+ ---
103
+
104
+ ## What NOT to Report
105
+
106
+ - Minor UX improvements without legal implications
107
+ - Color preferences or design opinions
108
+ - Performance optimizations
109
+ - Browser compatibility issues
110
+ - Issues where proper accessibility attributes are present