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.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -8
- package/lib/api-server.js +3 -2
- package/lib/feedback.js +9 -2
- package/lib/flag-detection.js +4 -2
- package/lib/git-operations.js +4 -2
- package/lib/lazy-require.js +59 -0
- package/lib/process-executor.js +24 -9
- package/lib/skill-loader.js +11 -3
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +12 -0
- package/scripts/agileflow-welcome.js +146 -90
- package/scripts/claude-tmux.sh +42 -6
- package/scripts/damage-control-multi-agent.js +14 -10
- package/scripts/lib/bus-utils.js +3 -1
- package/scripts/lib/configure-detect.js +12 -9
- package/scripts/lib/configure-features.js +128 -7
- package/scripts/lib/configure-repair.js +6 -5
- package/scripts/lib/context-formatter.js +13 -3
- package/scripts/lib/damage-control-utils.js +5 -1
- package/scripts/lib/lifecycle-detector.js +5 -3
- package/scripts/lib/process-cleanup.js +8 -4
- package/scripts/lib/scale-detector.js +47 -8
- package/scripts/lib/signal-detectors.js +117 -59
- package/scripts/lib/task-registry.js +5 -1
- package/scripts/lib/team-events.js +4 -4
- package/scripts/messaging-bridge.js +7 -1
- package/scripts/ralph-loop.js +10 -8
- package/scripts/smart-detect.js +32 -11
- package/scripts/team-manager.js +86 -1
- package/scripts/tmux-task-name.sh +105 -0
- package/scripts/tmux-task-watcher.sh +344 -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/team-lead.md +50 -13
- package/src/core/commands/babysit.md +75 -42
- package/src/core/commands/blockers.md +7 -7
- package/src/core/commands/configure.md +15 -61
- package/src/core/commands/discovery/brief.md +363 -0
- package/src/core/commands/discovery/new.md +395 -0
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/legal/audit.md +446 -0
- package/src/core/commands/logic/audit.md +5 -5
- package/src/core/commands/review.md +7 -1
- package/src/core/commands/rpi.md +61 -26
- package/src/core/commands/sprint.md +7 -6
- package/src/core/commands/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- package/src/core/templates/product-brief.md +136 -0
- package/tools/cli/installers/ide/claude-code.js +69 -2
- package/src/core/agents/configuration/archival.md +0 -350
- package/src/core/agents/configuration/attribution.md +0 -343
- package/src/core/agents/configuration/ci.md +0 -1103
- package/src/core/agents/configuration/damage-control.md +0 -375
- package/src/core/agents/configuration/git-config.md +0 -537
- package/src/core/agents/configuration/hooks.md +0 -623
- package/src/core/agents/configuration/precompact.md +0 -302
- package/src/core/agents/configuration/status-line.md +0 -557
- package/src/core/agents/configuration/verify.md +0 -618
- package/src/core/agents/configuration-damage-control.md +0 -259
- package/src/core/agents/configuration-visual-e2e.md +0 -339
package/scripts/team-manager.js
CHANGED
|
@@ -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 =
|
|
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
|