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 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 === 'install') {
158
+ if (command === 'watch') {
159
+ watch();
160
+ } else if (command === 'install') {
119
161
  install();
120
162
  } else if (command === 'info') {
121
163
  info();
@@ -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
- # Reflection prompt
40
- REFLECTION_MSG="Before closing: What shipped? What did you learn? What would you teach the agent for next time (add to CLAUDE.md or DANGER-ZONES.md)?"
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}${REFLECTION_MSG}"
42
+ FULL_MSG="${DURATION_MSG}${REVIEW_MSG}${CHECKPOINT_MSG}"
43
43
 
44
44
  cat <<EOF
45
45
  {
@@ -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.13.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
  ]
@@ -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
  },
@@ -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": [
@@ -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?"
@@ -51,3 +51,7 @@ $ARGUMENTS
51
51
  ### Acts plan
52
52
 
53
53
  !`cat uv-out/architecture/acts-plan.md 2>/dev/null | head -60 || echo "No acts plan found"`
54
+
55
+ ### Session checkpoint (what's in progress)
56
+
57
+ !`cat uv-out/checkpoints/latest.md 2>/dev/null | head -40 || echo "No checkpoint"`
@@ -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,11 @@
1
+ [
2
+ {
3
+ "event_type": "PostToolUse",
4
+ "session_id": "test-123",
5
+ "source_app": "uv-suite",
6
+ "tool_name": "Edit",
7
+ "cwd": "/tmp",
8
+ "_ts": 1776756371726,
9
+ "_id": "3f130976-6226-47ea-a477-18a16e194415"
10
+ }
11
+ ]
@@ -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
+ });