uv-suite 0.13.0 → 0.17.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/bin/cli.js +43 -1
- package/hooks/session-end.sh +3 -3
- package/hooks/session-start.sh +4 -0
- package/hooks/watchtower-send.sh +24 -0
- package/install.sh +2 -2
- package/package.json +2 -1
- package/personas/auto.json +2 -1
- package/personas/professional.json +6 -0
- package/personas/spike.json +6 -0
- package/personas/sport.json +6 -0
- package/skills/architect/SKILL.md +4 -0
- package/skills/checkpoint/SKILL.md +80 -0
- package/skills/commit/SKILL.md +91 -0
- package/skills/investigate/SKILL.md +110 -0
- package/skills/restore/SKILL.md +22 -0
- package/skills/review/SKILL.md +4 -0
- package/skills/uv-help/SKILL.md +114 -0
- package/skills/write-tests/SKILL.md +4 -0
- package/watchtower/dashboard.html +257 -0
- package/watchtower/events.json +11 -0
- package/watchtower/server.js +148 -0
package/bin/cli.js
CHANGED
|
@@ -29,6 +29,10 @@ function usage() {
|
|
|
29
29
|
uvs install --persona sport
|
|
30
30
|
uvs info Show what's installed
|
|
31
31
|
|
|
32
|
+
Monitoring:
|
|
33
|
+
uvs watch Start Watchtower dashboard (open browser)
|
|
34
|
+
uvs watch --bg Start Watchtower in background
|
|
35
|
+
|
|
32
36
|
Personas:
|
|
33
37
|
spike Research & docs (Opus, max effort)
|
|
34
38
|
sport New projects (Sonnet, high effort)
|
|
@@ -108,6 +112,42 @@ function launchCodex(persona, extra) {
|
|
|
108
112
|
child.on('exit', (code) => process.exit(code || 0));
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function watch() {
|
|
116
|
+
const serverScript = path.join(UV_SUITE_DIR, 'watchtower', 'server.js');
|
|
117
|
+
if (!fs.existsSync(serverScript)) {
|
|
118
|
+
console.error('Error: watchtower server not found at', serverScript);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const bg = args.includes('--bg') || args.includes('--background');
|
|
123
|
+
console.log('UV Suite Watchtower starting...');
|
|
124
|
+
console.log('Dashboard: http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200));
|
|
125
|
+
console.log('');
|
|
126
|
+
|
|
127
|
+
if (bg) {
|
|
128
|
+
const child = spawn('node', [serverScript], {
|
|
129
|
+
stdio: 'ignore',
|
|
130
|
+
detached: true,
|
|
131
|
+
});
|
|
132
|
+
child.unref();
|
|
133
|
+
console.log(`Running in background (PID: ${child.pid})`);
|
|
134
|
+
console.log('Stop with: kill ' + child.pid);
|
|
135
|
+
|
|
136
|
+
// Open browser
|
|
137
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
138
|
+
spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
|
|
139
|
+
} else {
|
|
140
|
+
// Foreground — open browser after a short delay
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
143
|
+
spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
|
|
144
|
+
}, 1000);
|
|
145
|
+
|
|
146
|
+
const child = spawn('node', [serverScript], { stdio: 'inherit' });
|
|
147
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
111
151
|
// --- Parse and route ---
|
|
112
152
|
|
|
113
153
|
if (!command || command === '--help' || command === '-h') {
|
|
@@ -115,7 +155,9 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
115
155
|
process.exit(0);
|
|
116
156
|
}
|
|
117
157
|
|
|
118
|
-
if (command === '
|
|
158
|
+
if (command === 'watch') {
|
|
159
|
+
watch();
|
|
160
|
+
} else if (command === 'install') {
|
|
119
161
|
install();
|
|
120
162
|
} else if (command === 'info') {
|
|
121
163
|
info();
|
package/hooks/session-end.sh
CHANGED
|
@@ -36,10 +36,10 @@ if [ -n "$STAGED" ] || [ -n "$UNSTAGED" ] || [ -n "$UNTRACKED" ]; then
|
|
|
36
36
|
REVIEW_MSG="Uncommitted changes — consider /review and /slop-check before committing. "
|
|
37
37
|
fi
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
# Checkpoint prompt
|
|
40
|
+
CHECKPOINT_MSG="Run /checkpoint to save session state for next time. Run /restore at the start of your next session."
|
|
41
41
|
|
|
42
|
-
FULL_MSG="${DURATION_MSG}${REVIEW_MSG}${
|
|
42
|
+
FULL_MSG="${DURATION_MSG}${REVIEW_MSG}${CHECKPOINT_MSG}"
|
|
43
43
|
|
|
44
44
|
cat <<EOF
|
|
45
45
|
{
|
package/hooks/session-start.sh
CHANGED
|
@@ -15,4 +15,8 @@ if [ ! -f "$TODAY_FILE" ]; then
|
|
|
15
15
|
echo "0" > "$TODAY_FILE"
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
|
+
# Send to watchtower
|
|
19
|
+
echo '{"session_id":"'$(cat "$STATE_DIR/session-start.txt")'","cwd":"'${CLAUDE_PROJECT_DIR:-.}'"}' | \
|
|
20
|
+
"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/watchtower-send.sh" "SessionStart" 2>/dev/null
|
|
21
|
+
|
|
18
22
|
exit 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# UV Suite Hook Helper: Send event to Watchtower server
|
|
3
|
+
# Called by other hooks. Non-blocking. Fails silently if server not running.
|
|
4
|
+
#
|
|
5
|
+
# Usage: echo "$INPUT" | .claude/hooks/watchtower-send.sh "EventType"
|
|
6
|
+
|
|
7
|
+
EVENT_TYPE="${1:-Unknown}"
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
WATCHTOWER_URL="${UVS_WATCHTOWER_URL:-http://localhost:4200}"
|
|
10
|
+
|
|
11
|
+
# Extract useful fields from the hook input
|
|
12
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
13
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
14
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null)
|
|
15
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
16
|
+
SOURCE_APP=$(basename "$CWD" 2>/dev/null)
|
|
17
|
+
|
|
18
|
+
# Send to watchtower (non-blocking, fire-and-forget)
|
|
19
|
+
curl -s -X POST "$WATCHTOWER_URL/events" \
|
|
20
|
+
-H "Content-Type: application/json" \
|
|
21
|
+
-d "{\"event_type\":\"$EVENT_TYPE\",\"session_id\":\"$SESSION_ID\",\"source_app\":\"$SOURCE_APP\",\"tool_name\":\"$TOOL_NAME\",\"tool_input\":$TOOL_INPUT,\"cwd\":\"$CWD\"}" \
|
|
22
|
+
&>/dev/null &
|
|
23
|
+
|
|
24
|
+
exit 0
|
package/install.sh
CHANGED
|
@@ -208,7 +208,7 @@ This project uses [UV Suite](https://github.com/utsavanand/uv-suite) v${UV_VERSI
|
|
|
208
208
|
|
|
209
209
|
### Skills
|
|
210
210
|
|
|
211
|
-
/map-codebase, /map-stack, /spec, /architect, /review, /write-tests, /write-evals, /slop-check, /prototype, /security-review
|
|
211
|
+
/map-codebase, /map-stack, /spec, /architect, /review, /write-tests, /write-evals, /slop-check, /prototype, /security-review, /checkpoint, /restore
|
|
212
212
|
|
|
213
213
|
### Artifacts
|
|
214
214
|
|
|
@@ -242,7 +242,7 @@ ${HOOKS_TEXT}
|
|
|
242
242
|
|
|
243
243
|
**Planning:** Use plan mode for complex tasks. Break work small enough to complete in under 50% context.
|
|
244
244
|
|
|
245
|
-
**Session:** /compact at ~50% context. Past 90 min, take a break.
|
|
245
|
+
**Session:** /compact at ~50% context. Past 90 min, take a break. Run /checkpoint before ending a session. Run /restore at the start of the next one.
|
|
246
246
|
|
|
247
247
|
### Launching sessions
|
|
248
248
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
|
|
5
5
|
"author": "Utsav Anand",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"portable-standards/",
|
|
38
38
|
"settings.json",
|
|
39
39
|
"uv.sh",
|
|
40
|
+
"watchtower/",
|
|
40
41
|
"install.sh",
|
|
41
42
|
"README.md"
|
|
42
43
|
]
|
package/personas/auto.json
CHANGED
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
{
|
|
76
76
|
"matcher": "*",
|
|
77
77
|
"hooks": [
|
|
78
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-timer.sh", "timeout": 5 }
|
|
78
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-timer.sh", "timeout": 5 },
|
|
79
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse", "timeout": 2, "async": true }
|
|
79
80
|
]
|
|
80
81
|
},
|
|
81
82
|
{
|
|
@@ -92,6 +92,12 @@
|
|
|
92
92
|
"type": "command",
|
|
93
93
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-timer.sh",
|
|
94
94
|
"timeout": 5
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
|
|
99
|
+
"timeout": 2,
|
|
100
|
+
"async": true
|
|
95
101
|
}
|
|
96
102
|
]
|
|
97
103
|
},
|
package/personas/spike.json
CHANGED
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
}
|
|
46
46
|
],
|
|
47
47
|
"PostToolUse": [
|
|
48
|
+
{
|
|
49
|
+
"matcher": "*",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse", "timeout": 2, "async": true }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
48
54
|
{
|
|
49
55
|
"matcher": "Write",
|
|
50
56
|
"hooks": [
|
package/personas/sport.json
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
33
|
"PostToolUse": [
|
|
34
|
+
{
|
|
35
|
+
"matcher": "*",
|
|
36
|
+
"hooks": [
|
|
37
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse", "timeout": 2, "async": true }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
34
40
|
{
|
|
35
41
|
"matcher": "Edit|Write",
|
|
36
42
|
"hooks": [
|
|
@@ -36,3 +36,7 @@ $ARGUMENTS
|
|
|
36
36
|
!`ls uv-out/specs/*.md 2>/dev/null | head -5 || echo "No specs found"`
|
|
37
37
|
|
|
38
38
|
!`cat $(ls -t uv-out/specs/*.md 2>/dev/null | head -1) 2>/dev/null | head -80 || echo ""`
|
|
39
|
+
|
|
40
|
+
### Session checkpoint
|
|
41
|
+
|
|
42
|
+
!`cat uv-out/checkpoints/latest.md 2>/dev/null | head -40 || echo "No checkpoint"`
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checkpoint
|
|
3
|
+
description: >
|
|
4
|
+
Save a checkpoint of the current session — what was done, key decisions, current state,
|
|
5
|
+
and what's next. Use before ending a session, before /compact, or at any natural breakpoint.
|
|
6
|
+
The next session auto-loads the latest checkpoint.
|
|
7
|
+
argument-hint: "[optional-label]"
|
|
8
|
+
user-invocable: true
|
|
9
|
+
allowed-tools:
|
|
10
|
+
- Write(*)
|
|
11
|
+
- Read(*)
|
|
12
|
+
- Bash(git status *)
|
|
13
|
+
- Bash(git diff *)
|
|
14
|
+
- Bash(git log *)
|
|
15
|
+
- Bash(date *)
|
|
16
|
+
- Bash(ls *)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Write a checkpoint to uv-out/checkpoints/
|
|
20
|
+
|
|
21
|
+
Create the directory `uv-out/checkpoints/` if it doesn't exist.
|
|
22
|
+
|
|
23
|
+
Write a file named `uv-out/checkpoints/YYYY-MM-DD-HHMM.md` (using the current timestamp).
|
|
24
|
+
|
|
25
|
+
Also write/overwrite `uv-out/checkpoints/latest.md` with the same content (so the next session can always find the most recent checkpoint).
|
|
26
|
+
|
|
27
|
+
## Label
|
|
28
|
+
|
|
29
|
+
$ARGUMENTS
|
|
30
|
+
|
|
31
|
+
If a label was provided, include it in the filename: `uv-out/checkpoints/YYYY-MM-DD-HHMM-[label].md`
|
|
32
|
+
|
|
33
|
+
## What to capture
|
|
34
|
+
|
|
35
|
+
Review the full conversation so far and write a structured checkpoint with these exact sections:
|
|
36
|
+
|
|
37
|
+
```markdown
|
|
38
|
+
# Checkpoint: [date] [time] [label if provided]
|
|
39
|
+
|
|
40
|
+
## What was accomplished
|
|
41
|
+
- [Bullet list of concrete things done this session]
|
|
42
|
+
- [Be specific: "Added webhook retry logic to PaymentService" not "worked on payments"]
|
|
43
|
+
|
|
44
|
+
## Key decisions made
|
|
45
|
+
- [Decision]: [Why] — [What was considered and rejected]
|
|
46
|
+
- [Only include decisions that affect future work]
|
|
47
|
+
|
|
48
|
+
## Current state
|
|
49
|
+
- Branch: [current git branch]
|
|
50
|
+
- Uncommitted changes: [yes/no, summary if yes]
|
|
51
|
+
- Tests: [passing/failing/not run]
|
|
52
|
+
- Blockers: [any unresolved issues]
|
|
53
|
+
|
|
54
|
+
## Files modified
|
|
55
|
+
- [List key files changed, not every file]
|
|
56
|
+
|
|
57
|
+
## What's next
|
|
58
|
+
- [Immediate next step — what the next session should start with]
|
|
59
|
+
- [Remaining tasks from the current Act/plan]
|
|
60
|
+
|
|
61
|
+
## Context the next session needs
|
|
62
|
+
- [Anything non-obvious that would be lost without this checkpoint]
|
|
63
|
+
- [Workarounds in place, temporary decisions, "this looks wrong but it's intentional because..."]
|
|
64
|
+
- [Environment setup notes if relevant]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Git state to capture
|
|
68
|
+
|
|
69
|
+
!`git branch --show-current 2>/dev/null || echo "not a git repo"`
|
|
70
|
+
|
|
71
|
+
!`git status --short 2>/dev/null | head -20 || echo "no git"`
|
|
72
|
+
|
|
73
|
+
!`git log --oneline -5 2>/dev/null || echo "no git history"`
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- Be specific. "Worked on auth" is useless. "Added JWT refresh token rotation with 7-day expiry" is useful.
|
|
78
|
+
- Capture WHY decisions were made, not just what. The next session needs the rationale.
|
|
79
|
+
- Keep it under 80 lines. This isn't a novel — it's a handoff.
|
|
80
|
+
- Every checkpoint overwrites `latest.md` so the next session always finds the freshest state.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: >
|
|
4
|
+
Review, test, slop-check, then commit and optionally open a PR.
|
|
5
|
+
The ship pipeline: one command from "code complete" to "committed and reviewed."
|
|
6
|
+
argument-hint: "[commit-message or 'pr']"
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools:
|
|
9
|
+
- Read(*)
|
|
10
|
+
- Grep(*)
|
|
11
|
+
- Glob(*)
|
|
12
|
+
- Write(*)
|
|
13
|
+
- Bash(git *)
|
|
14
|
+
- Bash(npm test *)
|
|
15
|
+
- Bash(npm run test *)
|
|
16
|
+
- Bash(npm run lint *)
|
|
17
|
+
- Bash(npx jest *)
|
|
18
|
+
- Bash(npx vitest *)
|
|
19
|
+
- Bash(pytest *)
|
|
20
|
+
- Bash(go test *)
|
|
21
|
+
- Bash(cargo test *)
|
|
22
|
+
- Bash(gh pr *)
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Intent
|
|
26
|
+
|
|
27
|
+
$ARGUMENTS
|
|
28
|
+
|
|
29
|
+
## Current state
|
|
30
|
+
|
|
31
|
+
!`git status --short 2>/dev/null | head -30 || echo "not a git repo"`
|
|
32
|
+
|
|
33
|
+
!`git diff --cached --stat 2>/dev/null || echo "nothing staged"`
|
|
34
|
+
|
|
35
|
+
!`git diff --stat 2>/dev/null || echo "no unstaged changes"`
|
|
36
|
+
|
|
37
|
+
## Pipeline — run these steps in order
|
|
38
|
+
|
|
39
|
+
### 1. Run tests
|
|
40
|
+
|
|
41
|
+
Find and run the project's test command. If tests fail, fix the failures before continuing. If they can't be fixed in 2 attempts, stop and report.
|
|
42
|
+
|
|
43
|
+
### 2. Run lint
|
|
44
|
+
|
|
45
|
+
If a lint command exists (npm run lint, ruff check, etc.), run it. Fix auto-fixable issues.
|
|
46
|
+
|
|
47
|
+
### 3. Quick slop check
|
|
48
|
+
|
|
49
|
+
Scan changed files for the most obvious patterns:
|
|
50
|
+
- `toBeTruthy()` / `toBeDefined()` in test files
|
|
51
|
+
- Bare `except: pass` in Python
|
|
52
|
+
|
|
53
|
+
Don't run the full /slop-check agent — just grep for mechanical patterns.
|
|
54
|
+
|
|
55
|
+
### 4. Review the diff
|
|
56
|
+
|
|
57
|
+
Read the full diff. Check for:
|
|
58
|
+
- Correctness: does it do what it should?
|
|
59
|
+
- Security: any obvious issues? (hardcoded secrets, missing auth checks)
|
|
60
|
+
- Completeness: anything half-done or TODO'd?
|
|
61
|
+
|
|
62
|
+
If you find issues, fix them. If they need human input, stop and ask.
|
|
63
|
+
|
|
64
|
+
### 5. Stage and commit
|
|
65
|
+
|
|
66
|
+
- Stage the relevant files (not `.env`, not `node_modules`, not build artifacts)
|
|
67
|
+
- Write a commit message. If the user provided one in $ARGUMENTS, use it. Otherwise, write a concise message that describes the WHY, not the WHAT.
|
|
68
|
+
- Commit.
|
|
69
|
+
|
|
70
|
+
### 6. Open PR (if requested)
|
|
71
|
+
|
|
72
|
+
If the user said "pr" in their arguments, or if on a feature branch:
|
|
73
|
+
- Push the branch
|
|
74
|
+
- Open a PR with `gh pr create`
|
|
75
|
+
- Title: short, under 70 chars
|
|
76
|
+
- Body: summary bullets + test plan
|
|
77
|
+
|
|
78
|
+
### 7. Checkpoint
|
|
79
|
+
|
|
80
|
+
After committing, write a checkpoint to `uv-out/checkpoints/latest.md` with what was committed.
|
|
81
|
+
|
|
82
|
+
## Danger zones
|
|
83
|
+
|
|
84
|
+
!`cat DANGER-ZONES.md 2>/dev/null || echo "No DANGER-ZONES.md"`
|
|
85
|
+
|
|
86
|
+
## Rules
|
|
87
|
+
|
|
88
|
+
- If tests fail and can't be fixed quickly, STOP. Don't commit broken code.
|
|
89
|
+
- If the diff touches a danger zone file, flag it before committing.
|
|
90
|
+
- Prefer separate commits per logical change if there are unrelated changes staged.
|
|
91
|
+
- Never commit .env, credentials, or secrets.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: investigate
|
|
3
|
+
description: >
|
|
4
|
+
Systematic root-cause debugging. Traces data flow, tests hypotheses,
|
|
5
|
+
narrows scope. Stops after 3 failed attempts and escalates.
|
|
6
|
+
Use when something is broken and you don't know why.
|
|
7
|
+
argument-hint: "[bug-description or error-message]"
|
|
8
|
+
user-invocable: true
|
|
9
|
+
context: fork
|
|
10
|
+
agent: reviewer
|
|
11
|
+
model: claude-opus-4-6
|
|
12
|
+
effort: max
|
|
13
|
+
allowed-tools:
|
|
14
|
+
- Read(*)
|
|
15
|
+
- Grep(*)
|
|
16
|
+
- Glob(*)
|
|
17
|
+
- Bash(git log *)
|
|
18
|
+
- Bash(git diff *)
|
|
19
|
+
- Bash(git blame *)
|
|
20
|
+
- Bash(git show *)
|
|
21
|
+
- Bash(npm test *)
|
|
22
|
+
- Bash(npm run *)
|
|
23
|
+
- Bash(npx *)
|
|
24
|
+
- Bash(pytest *)
|
|
25
|
+
- Bash(go test *)
|
|
26
|
+
- Bash(cargo test *)
|
|
27
|
+
- Bash(curl *)
|
|
28
|
+
- Bash(node *)
|
|
29
|
+
- Bash(python *)
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## The bug
|
|
33
|
+
|
|
34
|
+
$ARGUMENTS
|
|
35
|
+
|
|
36
|
+
## Project context
|
|
37
|
+
|
|
38
|
+
!`cat CLAUDE.md 2>/dev/null || echo "No CLAUDE.md"`
|
|
39
|
+
|
|
40
|
+
## Codebase map
|
|
41
|
+
|
|
42
|
+
!`cat uv-out/map-codebase.md 2>/dev/null | head -60 || echo "No codebase map"`
|
|
43
|
+
|
|
44
|
+
## Recent changes (potential cause)
|
|
45
|
+
|
|
46
|
+
!`git log --oneline -15 2>/dev/null || echo "no git"`
|
|
47
|
+
|
|
48
|
+
## Latest checkpoint
|
|
49
|
+
|
|
50
|
+
!`cat uv-out/checkpoints/latest.md 2>/dev/null | head -30 || echo "No checkpoint"`
|
|
51
|
+
|
|
52
|
+
## Investigation methodology
|
|
53
|
+
|
|
54
|
+
Follow this process strictly:
|
|
55
|
+
|
|
56
|
+
### Phase 1: Reproduce
|
|
57
|
+
|
|
58
|
+
Before investigating, reproduce the bug. Run the failing test, hit the failing endpoint, trigger the error. If you can't reproduce it, say so and ask for reproduction steps.
|
|
59
|
+
|
|
60
|
+
### Phase 2: Narrow scope
|
|
61
|
+
|
|
62
|
+
Form a hypothesis about WHERE the bug is:
|
|
63
|
+
1. Read the error message/stack trace carefully
|
|
64
|
+
2. Identify the failing component (which file, which function, which layer)
|
|
65
|
+
3. Check recent changes to that component (`git log --oneline [file]`)
|
|
66
|
+
4. Check if the component was modified in the last 5 commits (`git diff HEAD~5 [file]`)
|
|
67
|
+
|
|
68
|
+
State your hypothesis explicitly: "I think the bug is in [X] because [Y]."
|
|
69
|
+
|
|
70
|
+
### Phase 3: Test the hypothesis
|
|
71
|
+
|
|
72
|
+
Verify your hypothesis with the smallest possible test:
|
|
73
|
+
- Add a log/print statement
|
|
74
|
+
- Write a focused test case
|
|
75
|
+
- Run a specific command that isolates the behavior
|
|
76
|
+
|
|
77
|
+
If the hypothesis is wrong, form a new one. Track what you've ruled out.
|
|
78
|
+
|
|
79
|
+
### Phase 4: Fix or escalate
|
|
80
|
+
|
|
81
|
+
If you found the root cause within 3 attempts: fix it, run the tests, verify.
|
|
82
|
+
|
|
83
|
+
If you haven't found it after 3 attempts:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
## Stuck: [bug description]
|
|
87
|
+
|
|
88
|
+
Ruled out:
|
|
89
|
+
1. [Hypothesis 1] — wrong because [evidence]
|
|
90
|
+
2. [Hypothesis 2] — wrong because [evidence]
|
|
91
|
+
3. [Hypothesis 3] — wrong because [evidence]
|
|
92
|
+
|
|
93
|
+
Remaining possibilities:
|
|
94
|
+
- [What I haven't checked yet]
|
|
95
|
+
|
|
96
|
+
What I need:
|
|
97
|
+
- [Specific question or access needed from the human]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Rules
|
|
101
|
+
|
|
102
|
+
- NO FIXES WITHOUT INVESTIGATION. Don't guess and patch. Find the root cause first.
|
|
103
|
+
- State hypotheses explicitly before testing them.
|
|
104
|
+
- Track what you've ruled out so you don't revisit.
|
|
105
|
+
- 3 attempts max. Then escalate with structured findings.
|
|
106
|
+
- If the bug is in code you don't understand, run /map-codebase on that area first.
|
|
107
|
+
|
|
108
|
+
## Artifact output
|
|
109
|
+
|
|
110
|
+
Write investigation findings to `uv-out/investigate-YYYY-MM-DD.md`. Include: bug description, hypotheses tested, root cause found (or not), fix applied (or escalation).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: restore
|
|
3
|
+
description: >
|
|
4
|
+
Restore the latest checkpoint from a previous session. Shows what was done,
|
|
5
|
+
key decisions, current state, and what's next. Use at the start of a new session.
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools:
|
|
8
|
+
- Read(*)
|
|
9
|
+
- Bash(ls *)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Latest checkpoint
|
|
13
|
+
|
|
14
|
+
!`cat uv-out/checkpoints/latest.md 2>/dev/null || echo "No checkpoint found. Run /checkpoint to create one."`
|
|
15
|
+
|
|
16
|
+
## All checkpoints
|
|
17
|
+
|
|
18
|
+
!`ls -la uv-out/checkpoints/*.md 2>/dev/null | tail -10 || echo "No checkpoints directory"`
|
|
19
|
+
|
|
20
|
+
## Instructions
|
|
21
|
+
|
|
22
|
+
Read the checkpoint above. Summarize it to the user in 3-4 sentences: what was done, what's the current state, and what's next. Then ask: "Ready to pick up from here, or do you want to take a different direction?"
|
package/skills/review/SKILL.md
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: uv-help
|
|
3
|
+
description: >
|
|
4
|
+
Show all UV Suite skills, agents, hooks, guardrails, and personas.
|
|
5
|
+
Use when you want to know what's available or how to use a specific feature.
|
|
6
|
+
argument-hint: "[skill-name or topic]"
|
|
7
|
+
user-invocable: true
|
|
8
|
+
allowed-tools:
|
|
9
|
+
- Read(*)
|
|
10
|
+
- Glob(*)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## UV Suite Help
|
|
14
|
+
|
|
15
|
+
$ARGUMENTS
|
|
16
|
+
|
|
17
|
+
If the user asked about a specific skill or topic, focus on that. Otherwise, show the full overview below.
|
|
18
|
+
|
|
19
|
+
## Active persona
|
|
20
|
+
|
|
21
|
+
!`grep "Active persona" CLAUDE.md 2>/dev/null || echo "Unknown — check .claude/settings.json"`
|
|
22
|
+
|
|
23
|
+
## All available skills
|
|
24
|
+
|
|
25
|
+
Every skill accepts free-form arguments to direct the agent. Examples shown below.
|
|
26
|
+
|
|
27
|
+
### Understand
|
|
28
|
+
|
|
29
|
+
| Skill | What it does | Example |
|
|
30
|
+
|-------|-------------|---------|
|
|
31
|
+
| `/map-codebase [focus]` | Build a knowledge graph of the codebase | `/map-codebase focus on the auth flow and session management` |
|
|
32
|
+
| `/map-stack [dir]` | Map multiple services and their connections | `/map-stack show how layer3-max calls layer2-pie` |
|
|
33
|
+
|
|
34
|
+
### Plan
|
|
35
|
+
|
|
36
|
+
| Skill | What it does | Example |
|
|
37
|
+
|-------|-------------|---------|
|
|
38
|
+
| `/spec [requirements]` | Write a technical specification | `/spec webhook retry with exponential backoff, max 3 retries` |
|
|
39
|
+
| `/architect [spec]` | Design architecture, decompose into Acts | `/architect design for horizontal scaling, expect 10x traffic` |
|
|
40
|
+
|
|
41
|
+
### Build
|
|
42
|
+
|
|
43
|
+
| Skill | What it does | Example |
|
|
44
|
+
|-------|-------------|---------|
|
|
45
|
+
| `/write-tests [target]` | Generate tests matching project conventions | `/write-tests src/auth/login.ts focus on error paths` |
|
|
46
|
+
| `/write-evals [prompt]` | Write AI/LLM evaluation cases | `/write-evals test the search ranking prompt for adversarial inputs` |
|
|
47
|
+
| `/prototype [concept]` | Build a static React prototype | `/prototype event booking app with calendar and payment flow` |
|
|
48
|
+
|
|
49
|
+
### Review
|
|
50
|
+
|
|
51
|
+
| Skill | What it does | Example |
|
|
52
|
+
|-------|-------------|---------|
|
|
53
|
+
| `/review [focus]` | Code review: correctness, security, perf, slop | `/review pay attention to the new database migration` |
|
|
54
|
+
| `/slop-check [target]` | Detect 6 categories of AI-generated slop | `/slop-check src/components/ check for over-engineering` |
|
|
55
|
+
| `/security-review [target]` | OWASP audit, dependency scan, secret detection | `/security-review src/payments/ focus on webhook signature validation` |
|
|
56
|
+
|
|
57
|
+
### Ship
|
|
58
|
+
|
|
59
|
+
| Skill | What it does | Example |
|
|
60
|
+
|-------|-------------|---------|
|
|
61
|
+
| `/commit [message]` | Test, lint, review, commit (optionally open PR) | `/commit "Add webhook retry logic" pr` |
|
|
62
|
+
| `/investigate [bug]` | Root-cause debugging (3 attempts then escalate) | `/investigate search returns stale results after reindex` |
|
|
63
|
+
|
|
64
|
+
### Session
|
|
65
|
+
|
|
66
|
+
| Skill | What it does | Example |
|
|
67
|
+
|-------|-------------|---------|
|
|
68
|
+
| `/checkpoint [label]` | Save session state for next time | `/checkpoint auth-refactor` |
|
|
69
|
+
| `/restore` | Load latest checkpoint at session start | `/restore` |
|
|
70
|
+
|
|
71
|
+
## Agents (spawned by skills)
|
|
72
|
+
|
|
73
|
+
| Agent | Model | Used by |
|
|
74
|
+
|-------|-------|---------|
|
|
75
|
+
| Cartographer | Opus | /map-codebase, /map-stack |
|
|
76
|
+
| Spec Writer | Opus | /spec |
|
|
77
|
+
| Architect | Opus | /architect |
|
|
78
|
+
| Reviewer | Opus | /review, /investigate |
|
|
79
|
+
| Test Writer | Sonnet | /write-tests |
|
|
80
|
+
| Eval Writer | Opus | /write-evals |
|
|
81
|
+
| Anti-Slop Guard | Opus | /slop-check |
|
|
82
|
+
| Prototype Builder | Sonnet | /prototype |
|
|
83
|
+
| DevOps | Opus | (direct invocation) |
|
|
84
|
+
| Security | Opus | /security-review |
|
|
85
|
+
|
|
86
|
+
## Hooks (automatic, you don't invoke these)
|
|
87
|
+
|
|
88
|
+
!`ls .claude/hooks/*.sh 2>/dev/null | while read f; do echo "- $(basename $f)"; done || echo "No hooks installed"`
|
|
89
|
+
|
|
90
|
+
## Guardrails (anti-slop rules, active as context)
|
|
91
|
+
|
|
92
|
+
!`ls .claude/rules/*.md 2>/dev/null | while read f; do echo "- $(basename $f .md)"; done || echo "No guardrails installed"`
|
|
93
|
+
|
|
94
|
+
## Personas
|
|
95
|
+
|
|
96
|
+
| Persona | Launch | For |
|
|
97
|
+
|---------|--------|-----|
|
|
98
|
+
| Spike | `uvs spike` | Research, docs, architecture analysis |
|
|
99
|
+
| Sport | `uvs sport` | New projects, fast prototyping |
|
|
100
|
+
| Professional | `uvs pro` | Production code, full review rigor |
|
|
101
|
+
| Auto | `uvs auto` | Autonomous execution, clear specs |
|
|
102
|
+
|
|
103
|
+
## Artifacts
|
|
104
|
+
|
|
105
|
+
All agent output goes to `uv-out/`. Agents read each other's prior output automatically.
|
|
106
|
+
|
|
107
|
+
!`ls uv-out/*.md uv-out/**/*.md 2>/dev/null | head -15 || echo "No artifacts yet — run a skill to generate some"`
|
|
108
|
+
|
|
109
|
+
## Tips
|
|
110
|
+
|
|
111
|
+
- **Direct the agent:** Every skill accepts arguments. "/review" does a generic review. "/review focus on the error handling in the retry logic" gives targeted results.
|
|
112
|
+
- **Run in parallel:** "Run /review, /slop-check, and /security-review in parallel" — Claude spawns all three simultaneously.
|
|
113
|
+
- **Checkpoint before stopping:** "/checkpoint" saves your session state. "/restore" loads it next time.
|
|
114
|
+
- **Use the right persona:** `uvs spike` for research, `uvs pro` for production code, `uvs auto` to let it run.
|
|
@@ -48,3 +48,7 @@ $ARGUMENTS
|
|
|
48
48
|
### Acts plan (current task context)
|
|
49
49
|
|
|
50
50
|
!`cat uv-out/architecture/acts-plan.md 2>/dev/null | head -40 || echo "No acts plan found"`
|
|
51
|
+
|
|
52
|
+
### Session checkpoint
|
|
53
|
+
|
|
54
|
+
!`cat uv-out/checkpoints/latest.md 2>/dev/null | head -40 || echo "No checkpoint"`
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>UV Suite Watchtower</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; background: #000; color: #f5f5f7; }
|
|
10
|
+
.header { padding: 16px 24px; border-bottom: 1px solid #2d2d2f; display: flex; align-items: center; justify-content: space-between; }
|
|
11
|
+
.header h1 { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
|
|
12
|
+
.header .status { font-size: 11px; color: #86868b; }
|
|
13
|
+
.header .status .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 4px; }
|
|
14
|
+
.header .status .dot.on { background: #30d158; }
|
|
15
|
+
.header .status .dot.off { background: #ff453a; }
|
|
16
|
+
|
|
17
|
+
.filters { padding: 8px 24px; border-bottom: 1px solid #1d1d1f; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
18
|
+
.filters select, .filters button { background: #1d1d1f; color: #a1a1a6; border: 1px solid #2d2d2f; border-radius: 6px; padding: 4px 10px; font-size: 11px; cursor: pointer; }
|
|
19
|
+
.filters select:focus, .filters button:hover { border-color: #424245; color: #f5f5f7; }
|
|
20
|
+
.filters button.active { background: #0071e3; color: #fff; border-color: #0071e3; }
|
|
21
|
+
|
|
22
|
+
.stats { padding: 12px 24px; display: flex; gap: 24px; border-bottom: 1px solid #1d1d1f; }
|
|
23
|
+
.stat { text-align: center; }
|
|
24
|
+
.stat .n { font-size: 20px; font-weight: 600; }
|
|
25
|
+
.stat .l { font-size: 10px; color: #86868b; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
|
|
26
|
+
|
|
27
|
+
.sessions { padding: 12px 24px; border-bottom: 1px solid #1d1d1f; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
28
|
+
.session-tag { padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; cursor: pointer; border: 1px solid transparent; }
|
|
29
|
+
.session-tag:hover { border-color: #424245; }
|
|
30
|
+
.session-tag.active { border-color: #fff; }
|
|
31
|
+
|
|
32
|
+
.timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh - 220px); }
|
|
33
|
+
.event { padding: 6px 24px; display: grid; grid-template-columns: 70px 100px 120px 1fr 80px; gap: 8px; align-items: center; font-size: 12px; border-bottom: 1px solid #0d0d0d; }
|
|
34
|
+
.event:hover { background: #0d0d0d; }
|
|
35
|
+
.event .time { color: #6e6e73; font-variant-numeric: tabular-nums; font-family: 'SF Mono', monospace; font-size: 11px; }
|
|
36
|
+
.event .type { font-weight: 500; }
|
|
37
|
+
.event .session { font-size: 11px; border-radius: 8px; padding: 1px 8px; display: inline-block; }
|
|
38
|
+
.event .detail { color: #a1a1a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
39
|
+
.event .duration { color: #6e6e73; font-family: 'SF Mono', monospace; font-size: 11px; text-align: right; }
|
|
40
|
+
|
|
41
|
+
.empty { padding: 60px 24px; text-align: center; color: #6e6e73; }
|
|
42
|
+
.empty p { margin-top: 8px; font-size: 13px; }
|
|
43
|
+
|
|
44
|
+
/* Event type colors */
|
|
45
|
+
.type-SessionStart { color: #30d158; }
|
|
46
|
+
.type-SessionEnd, .type-Stop { color: #ff453a; }
|
|
47
|
+
.type-PreToolUse { color: #0a84ff; }
|
|
48
|
+
.type-PostToolUse { color: #64d2ff; }
|
|
49
|
+
.type-PostToolUseFailure { color: #ff6961; }
|
|
50
|
+
.type-UserPromptSubmit { color: #ffd60a; }
|
|
51
|
+
.type-SubagentStart { color: #bf5af2; }
|
|
52
|
+
.type-SubagentStop { color: #ac8ee0; }
|
|
53
|
+
.type-Notification { color: #ff9f0a; }
|
|
54
|
+
.type-PermissionRequest { color: #ff375f; }
|
|
55
|
+
.type-PreCompact { color: #6e6e73; }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
|
|
60
|
+
<div class="header">
|
|
61
|
+
<h1>UV Suite Watchtower</h1>
|
|
62
|
+
<div class="status"><span class="dot" id="statusDot"></span><span id="statusText">Connecting...</span></div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="stats">
|
|
66
|
+
<div class="stat"><div class="n" id="sessionCount">0</div><div class="l">Sessions</div></div>
|
|
67
|
+
<div class="stat"><div class="n" id="eventCount">0</div><div class="l">Events</div></div>
|
|
68
|
+
<div class="stat"><div class="n" id="toolCount">0</div><div class="l">Tool calls</div></div>
|
|
69
|
+
<div class="stat"><div class="n" id="errorCount">0</div><div class="l">Errors</div></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="sessions" id="sessionBar"></div>
|
|
73
|
+
|
|
74
|
+
<div class="filters">
|
|
75
|
+
<select id="filterType"><option value="">All events</option></select>
|
|
76
|
+
<select id="filterSession"><option value="">All sessions</option></select>
|
|
77
|
+
<button id="btnClear" onclick="clearEvents()">Clear</button>
|
|
78
|
+
<button id="btnAutoScroll" class="active" onclick="toggleAutoScroll()">Auto-scroll</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="timeline" id="timeline">
|
|
82
|
+
<div class="empty" id="emptyState">
|
|
83
|
+
<p><strong>Waiting for events</strong></p>
|
|
84
|
+
<p>Start a Claude Code session with UV Suite hooks.<br>Events will appear here in real-time.</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
const timeline = document.getElementById('timeline');
|
|
90
|
+
const emptyState = document.getElementById('emptyState');
|
|
91
|
+
const filterType = document.getElementById('filterType');
|
|
92
|
+
const filterSession = document.getElementById('filterSession');
|
|
93
|
+
const statusDot = document.getElementById('statusDot');
|
|
94
|
+
const statusText = document.getElementById('statusText');
|
|
95
|
+
const sessionBar = document.getElementById('sessionBar');
|
|
96
|
+
|
|
97
|
+
let events = [];
|
|
98
|
+
let sessions = {};
|
|
99
|
+
let autoScroll = true;
|
|
100
|
+
let selectedSession = '';
|
|
101
|
+
let selectedType = '';
|
|
102
|
+
let ws;
|
|
103
|
+
|
|
104
|
+
// Session colors
|
|
105
|
+
const palette = ['#0a84ff','#30d158','#ff9f0a','#bf5af2','#ff375f','#64d2ff','#ffd60a','#ac8ee0','#ff6961','#5e5ce6'];
|
|
106
|
+
let colorIdx = 0;
|
|
107
|
+
function sessionColor(id) {
|
|
108
|
+
if (!sessions[id]) {
|
|
109
|
+
sessions[id] = { color: palette[colorIdx++ % palette.length], count: 0, lastEvent: null };
|
|
110
|
+
updateSessionBar();
|
|
111
|
+
updateFilterSession();
|
|
112
|
+
}
|
|
113
|
+
sessions[id].count++;
|
|
114
|
+
sessions[id].lastEvent = Date.now();
|
|
115
|
+
return sessions[id].color;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatTime(ts) {
|
|
119
|
+
const d = new Date(ts);
|
|
120
|
+
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function shortSession(id) {
|
|
124
|
+
if (!id) return '—';
|
|
125
|
+
return id.length > 12 ? id.slice(0, 8) + '...' : id;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function eventDetail(ev) {
|
|
129
|
+
if (ev.tool_name) return ev.tool_name + (ev.tool_input?.command ? ': ' + ev.tool_input.command.slice(0, 60) : '');
|
|
130
|
+
if (ev.tool_input?.file_path) return ev.tool_input.file_path;
|
|
131
|
+
if (ev.source_app) return ev.source_app;
|
|
132
|
+
if (ev.message) return ev.message;
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderEvent(ev) {
|
|
137
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
138
|
+
const color = sessionColor(sid);
|
|
139
|
+
const div = document.createElement('div');
|
|
140
|
+
div.className = 'event';
|
|
141
|
+
div.innerHTML = `
|
|
142
|
+
<span class="time">${formatTime(ev._ts)}</span>
|
|
143
|
+
<span class="type type-${ev.event_type || ev.hook_event_name || ''}">${ev.event_type || ev.hook_event_name || '?'}</span>
|
|
144
|
+
<span class="session" style="background:${color}22;color:${color}">${shortSession(sid)}</span>
|
|
145
|
+
<span class="detail" title="${eventDetail(ev)}">${eventDetail(ev)}</span>
|
|
146
|
+
<span class="duration">${ev.duration_ms ? ev.duration_ms + 'ms' : ''}</span>
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
// Check filters
|
|
150
|
+
const typeMatch = !selectedType || (ev.event_type || ev.hook_event_name) === selectedType;
|
|
151
|
+
const sessMatch = !selectedSession || sid === selectedSession;
|
|
152
|
+
if (!typeMatch || !sessMatch) div.style.display = 'none';
|
|
153
|
+
|
|
154
|
+
return div;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function addEvent(ev) {
|
|
158
|
+
events.push(ev);
|
|
159
|
+
if (emptyState.parentNode) emptyState.remove();
|
|
160
|
+
timeline.appendChild(renderEvent(ev));
|
|
161
|
+
updateStats();
|
|
162
|
+
updateFilterType(ev);
|
|
163
|
+
if (autoScroll) timeline.scrollTop = timeline.scrollHeight;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function updateStats() {
|
|
167
|
+
document.getElementById('sessionCount').textContent = Object.keys(sessions).length;
|
|
168
|
+
document.getElementById('eventCount').textContent = events.length;
|
|
169
|
+
document.getElementById('toolCount').textContent = events.filter(e => (e.event_type || e.hook_event_name || '').includes('ToolUse')).length;
|
|
170
|
+
document.getElementById('errorCount').textContent = events.filter(e => (e.event_type || e.hook_event_name || '').includes('Failure')).length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function updateSessionBar() {
|
|
174
|
+
sessionBar.innerHTML = '';
|
|
175
|
+
for (const [id, s] of Object.entries(sessions)) {
|
|
176
|
+
const tag = document.createElement('span');
|
|
177
|
+
tag.className = 'session-tag' + (selectedSession === id ? ' active' : '');
|
|
178
|
+
tag.style.background = s.color + '22';
|
|
179
|
+
tag.style.color = s.color;
|
|
180
|
+
tag.textContent = shortSession(id) + ' (' + s.count + ')';
|
|
181
|
+
tag.onclick = () => { selectedSession = selectedSession === id ? '' : id; refilter(); updateSessionBar(); };
|
|
182
|
+
sessionBar.appendChild(tag);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const knownTypes = new Set();
|
|
187
|
+
function updateFilterType(ev) {
|
|
188
|
+
const t = ev.event_type || ev.hook_event_name;
|
|
189
|
+
if (t && !knownTypes.has(t)) {
|
|
190
|
+
knownTypes.add(t);
|
|
191
|
+
const opt = document.createElement('option');
|
|
192
|
+
opt.value = t; opt.textContent = t;
|
|
193
|
+
filterType.appendChild(opt);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function updateFilterSession() {
|
|
198
|
+
filterSession.innerHTML = '<option value="">All sessions</option>';
|
|
199
|
+
for (const id of Object.keys(sessions)) {
|
|
200
|
+
const opt = document.createElement('option');
|
|
201
|
+
opt.value = id; opt.textContent = shortSession(id);
|
|
202
|
+
filterSession.appendChild(opt);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function refilter() {
|
|
207
|
+
selectedType = filterType.value;
|
|
208
|
+
selectedSession = filterSession.value;
|
|
209
|
+
const rows = timeline.querySelectorAll('.event');
|
|
210
|
+
rows.forEach((row, i) => {
|
|
211
|
+
const ev = events[i];
|
|
212
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
213
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
214
|
+
const show = (!selectedType || type === selectedType) && (!selectedSession || sid === selectedSession);
|
|
215
|
+
row.style.display = show ? '' : 'none';
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
filterType.onchange = refilter;
|
|
220
|
+
filterSession.onchange = refilter;
|
|
221
|
+
|
|
222
|
+
function clearEvents() { events = []; sessions = {}; colorIdx = 0; timeline.innerHTML = ''; updateStats(); sessionBar.innerHTML = ''; }
|
|
223
|
+
function toggleAutoScroll() {
|
|
224
|
+
autoScroll = !autoScroll;
|
|
225
|
+
document.getElementById('btnAutoScroll').classList.toggle('active', autoScroll);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// WebSocket connection
|
|
229
|
+
function connect() {
|
|
230
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
231
|
+
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
232
|
+
|
|
233
|
+
ws.onopen = () => {
|
|
234
|
+
statusDot.className = 'dot on';
|
|
235
|
+
statusText.textContent = 'Connected';
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
ws.onclose = () => {
|
|
239
|
+
statusDot.className = 'dot off';
|
|
240
|
+
statusText.textContent = 'Disconnected — reconnecting...';
|
|
241
|
+
setTimeout(connect, 2000);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
ws.onmessage = (msg) => {
|
|
245
|
+
const data = JSON.parse(msg.data);
|
|
246
|
+
if (data.type === 'init' && data.events) {
|
|
247
|
+
data.events.forEach(addEvent);
|
|
248
|
+
} else {
|
|
249
|
+
addEvent(data);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
connect();
|
|
255
|
+
</script>
|
|
256
|
+
</body>
|
|
257
|
+
</html>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// UV Suite Watchtower — lightweight observability server
|
|
4
|
+
// Zero dependencies beyond Node.js (uses built-in http, fs, ws via raw upgrade)
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const PORT = process.env.UVS_WATCHTOWER_PORT || 4200;
|
|
11
|
+
const DATA_FILE = path.join(__dirname, 'events.json');
|
|
12
|
+
const MAX_EVENTS = 500;
|
|
13
|
+
|
|
14
|
+
// In-memory event store
|
|
15
|
+
let events = [];
|
|
16
|
+
try {
|
|
17
|
+
if (fs.existsSync(DATA_FILE)) {
|
|
18
|
+
events = JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
events = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// WebSocket clients
|
|
25
|
+
const clients = new Set();
|
|
26
|
+
|
|
27
|
+
function broadcast(event) {
|
|
28
|
+
const msg = JSON.stringify(event);
|
|
29
|
+
for (const ws of clients) {
|
|
30
|
+
try { ws.send(msg); } catch (e) { clients.delete(ws); }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveEvents() {
|
|
35
|
+
// Keep only the last MAX_EVENTS
|
|
36
|
+
if (events.length > MAX_EVENTS) {
|
|
37
|
+
events = events.slice(-MAX_EVENTS);
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(DATA_FILE, JSON.stringify(events, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Minimal WebSocket handshake (no dependency needed)
|
|
43
|
+
const crypto = require('crypto');
|
|
44
|
+
|
|
45
|
+
function upgradeToWebSocket(req, socket) {
|
|
46
|
+
const key = req.headers['sec-websocket-key'];
|
|
47
|
+
const accept = crypto.createHash('sha1')
|
|
48
|
+
.update(key + '258EAFA5-E914-47DA-95CA-5AB5DC085B11')
|
|
49
|
+
.digest('base64');
|
|
50
|
+
|
|
51
|
+
socket.write(
|
|
52
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
53
|
+
'Upgrade: websocket\r\n' +
|
|
54
|
+
'Connection: Upgrade\r\n' +
|
|
55
|
+
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
56
|
+
'\r\n'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const ws = {
|
|
60
|
+
send(data) {
|
|
61
|
+
const buf = Buffer.from(data);
|
|
62
|
+
const frame = [];
|
|
63
|
+
frame.push(0x81); // text frame
|
|
64
|
+
if (buf.length < 126) {
|
|
65
|
+
frame.push(buf.length);
|
|
66
|
+
} else if (buf.length < 65536) {
|
|
67
|
+
frame.push(126, (buf.length >> 8) & 0xff, buf.length & 0xff);
|
|
68
|
+
} else {
|
|
69
|
+
frame.push(127);
|
|
70
|
+
for (let i = 7; i >= 0; i--) frame.push((buf.length >> (i * 8)) & 0xff);
|
|
71
|
+
}
|
|
72
|
+
socket.write(Buffer.concat([Buffer.from(frame), buf]));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
clients.add(ws);
|
|
77
|
+
socket.on('close', () => clients.delete(ws));
|
|
78
|
+
socket.on('error', () => clients.delete(ws));
|
|
79
|
+
|
|
80
|
+
// Send recent events on connect
|
|
81
|
+
ws.send(JSON.stringify({ type: 'init', events: events.slice(-100) }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const server = http.createServer((req, res) => {
|
|
85
|
+
// CORS
|
|
86
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
87
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
88
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
89
|
+
|
|
90
|
+
if (req.method === 'OPTIONS') {
|
|
91
|
+
res.writeHead(200);
|
|
92
|
+
return res.end();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// POST /events — receive hook events
|
|
96
|
+
if (req.method === 'POST' && req.url === '/events') {
|
|
97
|
+
let body = '';
|
|
98
|
+
req.on('data', chunk => body += chunk);
|
|
99
|
+
req.on('end', () => {
|
|
100
|
+
try {
|
|
101
|
+
const event = JSON.parse(body);
|
|
102
|
+
event._ts = Date.now();
|
|
103
|
+
event._id = crypto.randomUUID();
|
|
104
|
+
events.push(event);
|
|
105
|
+
broadcast(event);
|
|
106
|
+
saveEvents();
|
|
107
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
108
|
+
res.end('{"ok":true}');
|
|
109
|
+
} catch (e) {
|
|
110
|
+
res.writeHead(400);
|
|
111
|
+
res.end('{"error":"invalid json"}');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// GET /events — fetch recent events
|
|
118
|
+
if (req.method === 'GET' && req.url.startsWith('/events')) {
|
|
119
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
120
|
+
res.end(JSON.stringify(events.slice(-100)));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GET / — serve dashboard
|
|
125
|
+
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
|
126
|
+
const html = fs.readFileSync(path.join(__dirname, 'dashboard.html'), 'utf-8');
|
|
127
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
128
|
+
res.end(html);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.writeHead(404);
|
|
133
|
+
res.end('not found');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
server.on('upgrade', (req, socket, head) => {
|
|
137
|
+
if (req.url === '/ws') {
|
|
138
|
+
upgradeToWebSocket(req, socket);
|
|
139
|
+
} else {
|
|
140
|
+
socket.destroy();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
server.listen(PORT, () => {
|
|
145
|
+
console.log(`UV Suite Watchtower running at http://localhost:${PORT}`);
|
|
146
|
+
console.log(`${events.length} events loaded from disk`);
|
|
147
|
+
console.log(`Waiting for hook events on POST http://localhost:${PORT}/events`);
|
|
148
|
+
});
|