uv-suite 0.26.3 → 0.26.5
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/README.md +110 -86
- package/bin/cli.js +260 -133
- package/hooks/confirm-prompt.sh +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
# UV Suite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A portable layer that turns Claude Code, Cursor, or Codex into a labeled, observable, anti-slop dev environment — with named sessions, a real-time observability dashboard, anti-slop guardrails, and per-session memory across launches.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g uv-suite
|
|
9
|
-
|
|
9
|
+
uvs claude pro # auto-installs into the current project on first launch
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Or with npx:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx uv-suite install
|
|
15
|
+
npx uv-suite install # explicit install only
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
> **Note:** The CLI is `uvs`, not `uv`. The name `uv` belongs to Astral's Python package manager and likely already exists on your system.
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
uvs claude pro # Claude Code, Professional persona
|
|
24
|
+
uvs codex auto # Codex, Auto persona
|
|
25
|
+
uvs pro # Shorthand for uvs claude pro
|
|
26
|
+
uvs install # Explicit install (also runs automatically on launch)
|
|
27
|
+
uvs watch # Open the Watchtower observability dashboard
|
|
28
|
+
uvs info # Show what's installed
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
On every `uvs` launch, you'll be prompted to label the session:
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
```
|
|
34
|
+
Label this session (Enter to skip — you'll be reminded):
|
|
35
|
+
name: payments retry refactor
|
|
36
|
+
kind [long/outcome]: outcome
|
|
37
|
+
purpose: ship retry-on-5xx for the Stripe webhook handler
|
|
38
|
+
priority [low/med/high]: high
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Skip any field with Enter. If you skip the name, `/session-init` will be suggested every few prompts until you label it. Set `UVS_NO_PROMPT=1` to suppress prompts entirely.
|
|
42
|
+
|
|
43
|
+
## Sessions and Watchtower
|
|
44
|
+
|
|
45
|
+
Each `uvs` launch generates a `UVS_SESSION_ID` and writes metadata to `.uv-suite-state/sessions/<id>.json`. This unlocks:
|
|
46
|
+
|
|
47
|
+
- **Concurrent terminals don't collide.** Two `uvs` launches in the same repo run as distinct sessions with separate names, checkpoints, and dashboard rows.
|
|
48
|
+
- **`uvs watch` shows them all.** The Watchtower dashboard at `localhost:4200` streams every tool call across every session in real time — labeled by your name, sorted by priority (high to top, low dimmed), color-coded by persona.
|
|
49
|
+
- **Per-session checkpoints.** `/checkpoint` writes to `uv-out/checkpoints/<sid>/`, and `/restore` auto-picks the current session's latest. Pass a session id prefix or name to restore from a different one.
|
|
50
|
+
- **Status line shows it all.** The Claude Code status bar shows session name, persona, priority, and elapsed time continuously.
|
|
51
|
+
|
|
52
|
+
### Watchtower at a glance
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Sessions Events Tool calls Errors Need human
|
|
56
|
+
4 1,247 914 2 0
|
|
57
|
+
|
|
58
|
+
[payments retry [auto] [P:high] [outcome] ] (147)
|
|
59
|
+
[infra cleanup [pro] [P:med] [long-running] ] (382)
|
|
60
|
+
[exec deck [spike] [P:low] [outcome] ] (89)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Hooks fire on every Claude Code event (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `Stop`, `PermissionRequest`, ...) and forward to the dashboard with the session metadata merged in. Zero dependencies — vanilla Node + SSE.
|
|
64
|
+
|
|
65
|
+
## Personas
|
|
66
|
+
|
|
67
|
+
Four modes for different contexts. Pick one when you start a session.
|
|
32
68
|
|
|
33
69
|
```
|
|
34
70
|
Spike Sport Professional Auto
|
|
@@ -43,12 +79,7 @@ Writes New files Anything Anything Anything
|
|
|
43
79
|
only (reviewed) (autonomous)
|
|
44
80
|
Edits Blocked Allowed Allowed Allowed
|
|
45
81
|
|
|
46
|
-
Hooks 1 1 8 3
|
|
47
|
-
doc-slop lint all lint, block,
|
|
48
|
-
timer
|
|
49
|
-
|
|
50
82
|
Guardrails Doc slop None All 6 All 6
|
|
51
|
-
|
|
52
83
|
Human gates After each End only Every Act Final output
|
|
53
84
|
map boundary only
|
|
54
85
|
```
|
|
@@ -56,7 +87,7 @@ Human gates After each End only Every Act Final output
|
|
|
56
87
|
### When to use what
|
|
57
88
|
|
|
58
89
|
| Situation | Mode | Why |
|
|
59
|
-
|
|
90
|
+
|---|---|---|
|
|
60
91
|
| Joining a new codebase | **Spike** | Understand before changing. Writes docs, not code. |
|
|
61
92
|
| Architecture review | **Spike** | Map the system, write ADRs, document findings. |
|
|
62
93
|
| Prototyping a demo | **Sport** | Move fast, iterate freely, quality comes later. |
|
|
@@ -71,121 +102,114 @@ Human gates After each End only Every Act Final output
|
|
|
71
102
|
- Spike → Auto (research thoroughly, then let the agent execute)
|
|
72
103
|
- Sport → Professional (prototype fast, switch to rigor when it matters)
|
|
73
104
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
| Category | Count | What |
|
|
77
|
-
|----------|-------|------|
|
|
78
|
-
| Agents | 10 | Subagent definitions for Claude Code, Cursor, and Codex |
|
|
79
|
-
| Skills | 10 | Slash commands with dynamic context injection |
|
|
80
|
-
| Hooks | 8 | Auto-lint, slop check, danger zones, session tracking, destructive blocks |
|
|
81
|
-
| Guardrails | 6 | Anti-slop rules (comments, overengineering, tests, docs, architecture, errors) |
|
|
82
|
-
| Personas | 4 | Spike, Sport, Professional, Auto |
|
|
83
|
-
|
|
84
|
-
## Three Subsystems
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
UV Index UV Acts UV Guard
|
|
88
|
-
Understand Build Review
|
|
89
|
-
Learn Deliver Harden
|
|
90
|
-
Remember Present Protect
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
**UV Index** maps codebases using [Graphify](https://github.com/safishamsi/graphify) knowledge graphs, captures context, builds persistent memory.
|
|
94
|
-
|
|
95
|
-
**UV Acts** delivers software in sequential phases (Acts) with parallel tasks, human-in-the-loop cycle budgets, and spec-driven development.
|
|
96
|
-
|
|
97
|
-
**UV Guard** catches AI slop in real time, reviews code for security (OWASP, [Semgrep](https://github.com/semgrep/semgrep)), and enforces danger zones.
|
|
98
|
-
|
|
99
|
-
## Skills
|
|
105
|
+
## Skills (slash commands)
|
|
100
106
|
|
|
101
107
|
| Command | What it does |
|
|
102
|
-
|
|
108
|
+
|---|---|
|
|
103
109
|
| `/map-codebase [dir]` | Build a knowledge graph of the codebase |
|
|
104
110
|
| `/map-stack [dir]` | Map multiple services and their connections |
|
|
105
111
|
| `/spec [requirements]` | Write a technical specification |
|
|
106
112
|
| `/architect [spec]` | Design architecture, decompose into Acts |
|
|
107
|
-
| `/
|
|
113
|
+
| `/prototype [concept]` | Build a static React prototype |
|
|
108
114
|
| `/write-tests [file]` | Generate tests matching project conventions |
|
|
109
115
|
| `/write-evals [prompt]` | Write AI/LLM evaluation cases ([DeepEval](https://github.com/confident-ai/deepeval) compatible) |
|
|
116
|
+
| `/review` | Code review: correctness, security, performance, slop |
|
|
110
117
|
| `/slop-check` | Detect 6 categories of AI-generated slop |
|
|
111
|
-
| `/prototype [concept]` | Build a static React prototype |
|
|
112
118
|
| `/security-review` | OWASP audit, dependency scan, secret detection |
|
|
119
|
+
| `/investigate` | Systematic root-cause debugging |
|
|
120
|
+
| `/commit` | Review → test → slop-check → commit (and optionally PR) |
|
|
121
|
+
| `/checkpoint [label]` | Save session state to `uv-out/checkpoints/<sid>/` |
|
|
122
|
+
| `/restore [sid-prefix\|name]` | Load the latest checkpoint for the current (or named) session |
|
|
123
|
+
| `/session-init [name\|--kind\|--purpose\|--priority]` | Label or relabel the current session |
|
|
124
|
+
| `/confirm [on\|off\|<n>]` | Toggle reframe-and-confirm for prompts over `<n>` words |
|
|
125
|
+
| `/uv-help` | List every skill, agent, hook, guardrail, and persona |
|
|
113
126
|
|
|
114
|
-
## Hooks
|
|
127
|
+
## Hooks (lifecycle automation)
|
|
115
128
|
|
|
116
|
-
Fire automatically. You never invoke these.
|
|
129
|
+
Fire automatically on Claude Code events. You never invoke these.
|
|
117
130
|
|
|
118
131
|
| Hook | Fires on | What it does |
|
|
119
|
-
|
|
132
|
+
|---|---|---|
|
|
120
133
|
| auto-lint | File write | Runs prettier, ruff, or gofmt |
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
134
|
+
| slop-grep | File write | Greps for obvious slop patterns (over-commented code, vague docs) |
|
|
135
|
+
| doc-slop-grep | File write | Catches vague adjectives in markdown |
|
|
136
|
+
| danger-zone-check | File edit | Warns if file is in DANGER-ZONES.md |
|
|
137
|
+
| block-destructive | Bash command | Blocks `rm -rf /`, force push to main, `DROP TABLE` |
|
|
138
|
+
| confirm-prompt | UserPromptSubmit | For prompts over the threshold, requires Claude to restate before any work starts |
|
|
139
|
+
| session-label-nag | UserPromptSubmit | Reminds you to run `/session-init` every Nth prompt while the session has no name |
|
|
140
|
+
| context-warning | PostToolUse | Warns when context usage crosses thresholds |
|
|
141
|
+
| watchtower-send | All events | Forwards every event (with session metadata) to `localhost:4200` |
|
|
142
|
+
| session-start | SessionStart | Records start time, fires bootstrap event with session metadata |
|
|
143
|
+
| session-timer | PostToolUse | Reminders at 45 / 90 / 180 minutes |
|
|
144
|
+
| session-end | Stop | Shows duration, today's total, reflection prompt |
|
|
145
|
+
| session-review-reminder | Stop | Nudges you to review uncommitted changes |
|
|
146
|
+
| status-line | Continuous | Renders session label, persona, priority, and timer in the Claude Code status bar |
|
|
128
147
|
|
|
129
148
|
## Agents
|
|
130
149
|
|
|
131
|
-
10 agents, each in 4 formats (Claude Code, Cursor, Codex,
|
|
150
|
+
10 agents, each in 4 formats (Claude Code, Cursor, Codex, portable):
|
|
132
151
|
|
|
133
152
|
| Agent | Subsystem | Model | Cycle Budget |
|
|
134
|
-
|
|
135
|
-
| Cartographer |
|
|
136
|
-
| Spec Writer |
|
|
137
|
-
| Architect |
|
|
138
|
-
| Reviewer |
|
|
139
|
-
| Test Writer |
|
|
140
|
-
| Eval Writer |
|
|
141
|
-
| Anti-Slop Guard |
|
|
142
|
-
| Prototype Builder |
|
|
143
|
-
| DevOps |
|
|
144
|
-
| Security |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| Cartographer | Index | Opus | 1 |
|
|
155
|
+
| Spec Writer | Acts | Opus | 1 |
|
|
156
|
+
| Architect | Acts | Opus | 2 |
|
|
157
|
+
| Reviewer | Guard | Opus | 1 |
|
|
158
|
+
| Test Writer | Acts | Sonnet | 3 |
|
|
159
|
+
| Eval Writer | Acts | Opus | 2 |
|
|
160
|
+
| Anti-Slop Guard | Guard | Opus | 1 |
|
|
161
|
+
| Prototype Builder | Acts | Sonnet | 3 |
|
|
162
|
+
| DevOps | Acts | Opus | 2 |
|
|
163
|
+
| Security | Guard | Opus | 1 |
|
|
145
164
|
|
|
146
165
|
## Artifacts
|
|
147
166
|
|
|
148
167
|
Agents write persistent output to `uv-out/`. Each agent reads prior artifacts automatically.
|
|
149
168
|
|
|
150
|
-
|
|
|
151
|
-
|
|
169
|
+
| Output | Read by |
|
|
170
|
+
|---|---|
|
|
152
171
|
| `uv-out/map-codebase.md` | /architect, /review, /security-review |
|
|
153
172
|
| `uv-out/specs/*.md` | /architect, /write-tests, /write-evals |
|
|
154
173
|
| `uv-out/architecture/*.md` | /review, /write-tests, /slop-check |
|
|
155
174
|
| `uv-out/review-*.md` | /slop-check, /security-review |
|
|
175
|
+
| `uv-out/checkpoints/<sid>/*.md` | /restore |
|
|
156
176
|
|
|
157
177
|
## Integrations
|
|
158
178
|
|
|
159
179
|
| Tool | Used by | Purpose |
|
|
160
|
-
|
|
180
|
+
|---|---|---|
|
|
161
181
|
| [Graphify](https://github.com/safishamsi/graphify) | Cartographer | Knowledge graph from codebase via Tree-sitter |
|
|
162
|
-
| [Semgrep](https://github.com/semgrep/semgrep) | Security
|
|
163
|
-
| [Gitleaks](https://github.com/gitleaks/gitleaks) | Security
|
|
164
|
-
| [Trivy](https://github.com/aquasecurity/trivy) | Security
|
|
182
|
+
| [Semgrep](https://github.com/semgrep/semgrep) | Security | SAST with 4000+ OWASP-mapped rules |
|
|
183
|
+
| [Gitleaks](https://github.com/gitleaks/gitleaks) | Security | Secret detection in git repos |
|
|
184
|
+
| [Trivy](https://github.com/aquasecurity/trivy) | Security | Dependency vulnerability scanning |
|
|
165
185
|
| [DeepEval](https://github.com/confident-ai/deepeval) | Eval Writer | Pytest-compatible LLM evaluation |
|
|
166
|
-
| [Playwright](https://playwright.dev/docs/getting-started-mcp) | Prototype
|
|
186
|
+
| [Playwright](https://playwright.dev/docs/getting-started-mcp) | Prototype, Test Writer | Browser automation and e2e testing |
|
|
167
187
|
|
|
168
188
|
## Project Structure After Install
|
|
169
189
|
|
|
170
190
|
```
|
|
171
191
|
.claude/
|
|
172
|
-
settings.json
|
|
173
|
-
agents/
|
|
174
|
-
skills/
|
|
175
|
-
hooks/
|
|
176
|
-
rules/
|
|
177
|
-
personas/
|
|
178
|
-
.codex/agents/
|
|
179
|
-
.cursor/rules/
|
|
180
|
-
AGENTS.md
|
|
181
|
-
DANGER-ZONES.md
|
|
182
|
-
uv-
|
|
192
|
+
settings.json Permissions and hooks (seeded from your persona on first install)
|
|
193
|
+
agents/ 10 agent definitions
|
|
194
|
+
skills/ 17 slash commands
|
|
195
|
+
hooks/ 14 hook scripts + 2 helpers
|
|
196
|
+
rules/ 6 anti-slop guardrails (Pro / Auto only)
|
|
197
|
+
personas/ 4 persona configs
|
|
198
|
+
.codex/agents/ 10 Codex agent definitions
|
|
199
|
+
.cursor/rules/ 10 Cursor rule definitions
|
|
200
|
+
AGENTS.md Codex instruction file
|
|
201
|
+
DANGER-ZONES.md Risky areas (commit this)
|
|
202
|
+
.uv-suite-state/ Session metadata + counters (gitignored)
|
|
203
|
+
current-session.txt
|
|
204
|
+
sessions/<sid>.json
|
|
205
|
+
uv-out/ Agent output artifacts (gitignored)
|
|
206
|
+
checkpoints/<sid>/ Per-session checkpoints
|
|
183
207
|
```
|
|
184
208
|
|
|
185
209
|
## Documentation
|
|
186
210
|
|
|
187
211
|
| Document | What it covers |
|
|
188
|
-
|
|
212
|
+
|---|---|
|
|
189
213
|
| [usage-guide.md](usage-guide.md) | Full SDLC mapped to exact commands |
|
|
190
214
|
| [personas.md](personas.md) | 4 personas, 7 knobs, when to use each |
|
|
191
215
|
| [practices.md](practices.md) | Working principles (honesty, parallelism, scope, completion) |
|
package/bin/cli.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execSync, spawn } = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const fs = require(
|
|
3
|
+
const { execSync, spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const readline = require("readline");
|
|
6
8
|
|
|
7
|
-
const UV_SUITE_DIR = path.resolve(__dirname,
|
|
9
|
+
const UV_SUITE_DIR = path.resolve(__dirname, "..");
|
|
8
10
|
const args = process.argv.slice(2);
|
|
9
11
|
const command = args[0];
|
|
10
|
-
const pkg = require(path.join(UV_SUITE_DIR,
|
|
12
|
+
const pkg = require(path.join(UV_SUITE_DIR, "package.json"));
|
|
11
13
|
|
|
12
|
-
const PERSONAS = [
|
|
13
|
-
const TOOLS = [
|
|
14
|
+
const PERSONAS = ["spike", "sport", "pro", "professional", "auto"];
|
|
15
|
+
const TOOLS = ["claude", "codex"];
|
|
14
16
|
|
|
15
17
|
function usage() {
|
|
16
18
|
console.log(`
|
|
@@ -57,208 +59,333 @@ function info() {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function install() {
|
|
60
|
-
const installScript = path.join(UV_SUITE_DIR,
|
|
62
|
+
const installScript = path.join(UV_SUITE_DIR, "install.sh");
|
|
61
63
|
if (!fs.existsSync(installScript)) {
|
|
62
|
-
console.error(
|
|
64
|
+
console.error("Error: install.sh not found at", installScript);
|
|
63
65
|
process.exit(1);
|
|
64
66
|
}
|
|
65
|
-
const installArgs = args.slice(1).join(
|
|
67
|
+
const installArgs = args.slice(1).join(" ");
|
|
66
68
|
try {
|
|
67
|
-
execSync(`bash "${installScript}" ${installArgs}`, { stdio:
|
|
69
|
+
execSync(`bash "${installScript}" ${installArgs}`, { stdio: "inherit" });
|
|
68
70
|
} catch (e) {
|
|
69
71
|
process.exit(e.status || 1);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
function normPersona(p) {
|
|
74
|
-
if (p ===
|
|
76
|
+
if (p === "pro" || p === "professional") return "professional";
|
|
75
77
|
if (PERSONAS.includes(p)) return p;
|
|
76
78
|
return null;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function personaLabel(p) {
|
|
80
82
|
const labels = {
|
|
81
|
-
spike:
|
|
82
|
-
sport:
|
|
83
|
-
professional:
|
|
84
|
-
auto:
|
|
83
|
+
spike: "Spike — research & docs (Opus, max)",
|
|
84
|
+
sport: "Sport — lightweight (Sonnet, high)",
|
|
85
|
+
professional: "Professional — full rigor (all hooks, all guardrails)",
|
|
86
|
+
auto: "Auto — autonomous (max, everything approved)",
|
|
85
87
|
};
|
|
86
88
|
return labels[p] || p;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
91
|
+
// Sync package-owned files (hooks, skills, personas, agents, optional guardrails)
|
|
92
|
+
// from the installed npm package into the project's .claude/. Idempotent — runs
|
|
93
|
+
// every launch so users on older versions pick up new hooks and slash commands
|
|
94
|
+
// after `npm install -g uv-suite@latest` without needing `uvs install` again.
|
|
95
|
+
// settings.json is preserved if it exists (user customizations).
|
|
96
|
+
function syncPackageFiles(persona) {
|
|
97
|
+
const srcDir = UV_SUITE_DIR;
|
|
98
|
+
const targetDir = path.resolve(".claude");
|
|
99
|
+
const hooksDir = path.join(targetDir, "hooks");
|
|
100
|
+
const personasDir = path.join(targetDir, "personas");
|
|
101
|
+
const wasFreshInstall =
|
|
102
|
+
!fs.existsSync(personasDir) || !fs.existsSync(hooksDir);
|
|
103
|
+
|
|
104
|
+
for (const dir of ["agents", "skills", "hooks", "rules", "personas"]) {
|
|
105
|
+
fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
const agentsSrc = path.join(srcDir, "agents", "claude-code");
|
|
109
|
+
if (fs.existsSync(agentsSrc)) {
|
|
110
|
+
for (const f of fs.readdirSync(agentsSrc)) {
|
|
111
|
+
fs.copyFileSync(
|
|
112
|
+
path.join(agentsSrc, f),
|
|
113
|
+
path.join(targetDir, "agents", f),
|
|
114
|
+
);
|
|
113
115
|
}
|
|
116
|
+
}
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fs.chmodSync(dest, 0o755);
|
|
122
|
-
}
|
|
118
|
+
const hooksSrc = path.join(srcDir, "hooks");
|
|
119
|
+
if (fs.existsSync(hooksSrc)) {
|
|
120
|
+
for (const f of fs.readdirSync(hooksSrc)) {
|
|
121
|
+
const dest = path.join(targetDir, "hooks", f);
|
|
122
|
+
fs.copyFileSync(path.join(hooksSrc, f), dest);
|
|
123
|
+
fs.chmodSync(dest, 0o755);
|
|
123
124
|
}
|
|
125
|
+
}
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fs.copyFileSync(skillFile, path.join(destDir, 'SKILL.md'));
|
|
134
|
-
}
|
|
127
|
+
const skillsSrc = path.join(srcDir, "skills");
|
|
128
|
+
if (fs.existsSync(skillsSrc)) {
|
|
129
|
+
for (const d of fs.readdirSync(skillsSrc)) {
|
|
130
|
+
const skillFile = path.join(skillsSrc, d, "SKILL.md");
|
|
131
|
+
if (fs.existsSync(skillFile)) {
|
|
132
|
+
const destDir = path.join(targetDir, "skills", d);
|
|
133
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
134
|
+
fs.copyFileSync(skillFile, path.join(destDir, "SKILL.md"));
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
if (persona === "professional" || persona === "auto") {
|
|
140
|
+
const guardSrc = path.join(srcDir, "guardrails");
|
|
141
|
+
if (fs.existsSync(guardSrc)) {
|
|
142
|
+
for (const f of fs.readdirSync(guardSrc)) {
|
|
143
|
+
fs.copyFileSync(
|
|
144
|
+
path.join(guardSrc, f),
|
|
145
|
+
path.join(targetDir, "rules", f),
|
|
146
|
+
);
|
|
145
147
|
}
|
|
146
148
|
}
|
|
149
|
+
}
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
const personasSrc = path.join(srcDir, "personas");
|
|
152
|
+
if (fs.existsSync(personasSrc)) {
|
|
153
|
+
for (const f of fs.readdirSync(personasSrc)) {
|
|
154
|
+
fs.copyFileSync(
|
|
155
|
+
path.join(personasSrc, f),
|
|
156
|
+
path.join(targetDir, "personas", f),
|
|
157
|
+
);
|
|
154
158
|
}
|
|
159
|
+
}
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
// settings.json is user-owned. Only seed it on fresh install.
|
|
162
|
+
const personaFile = path.join(targetDir, "personas", `${persona}.json`);
|
|
163
|
+
const settingsFile = path.join(targetDir, "settings.json");
|
|
164
|
+
if (
|
|
165
|
+
wasFreshInstall &&
|
|
166
|
+
fs.existsSync(personaFile) &&
|
|
167
|
+
!fs.existsSync(settingsFile)
|
|
168
|
+
) {
|
|
169
|
+
fs.copyFileSync(personaFile, settingsFile);
|
|
170
|
+
}
|
|
162
171
|
|
|
172
|
+
if (wasFreshInstall) {
|
|
173
|
+
console.log(
|
|
174
|
+
"UV Suite not installed in this project. Installing core files...",
|
|
175
|
+
);
|
|
163
176
|
console.log(` Installed: agents, skills, hooks, guardrails, personas`);
|
|
164
|
-
console.log(
|
|
177
|
+
console.log("");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function prompt(rl, question) {
|
|
182
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeKind(s) {
|
|
186
|
+
const v = (s || "").toLowerCase().trim();
|
|
187
|
+
if (["l", "long", "long-running"].includes(v)) return "long-running";
|
|
188
|
+
if (["o", "outcome"].includes(v)) return "outcome";
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizePriority(s) {
|
|
193
|
+
const v = (s || "").toLowerCase().trim();
|
|
194
|
+
if (["l", "low"].includes(v)) return "low";
|
|
195
|
+
if (["m", "med", "medium"].includes(v)) return "med";
|
|
196
|
+
if (["h", "high"].includes(v)) return "high";
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Generate a UVS_SESSION_ID, prompt for metadata (name/kind/purpose/priority),
|
|
201
|
+
// write it to .uv-suite-state/sessions/<sid>.json, and return the id + name.
|
|
202
|
+
// Skipping (Enter) leaves a field empty; the session-label-nag.sh hook will
|
|
203
|
+
// remind the user to run /session-init mid-flight.
|
|
204
|
+
async function setupSession(persona) {
|
|
205
|
+
const projectDir = process.cwd();
|
|
206
|
+
const stateDir = path.join(projectDir, ".uv-suite-state");
|
|
207
|
+
const sessionsDir = path.join(stateDir, "sessions");
|
|
208
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
209
|
+
|
|
210
|
+
const sid = crypto.randomUUID();
|
|
211
|
+
let name = "";
|
|
212
|
+
let kind = "";
|
|
213
|
+
let purpose = "";
|
|
214
|
+
let priority = "";
|
|
215
|
+
|
|
216
|
+
if (process.stdin.isTTY && !process.env.UVS_NO_PROMPT) {
|
|
217
|
+
const rl = readline.createInterface({
|
|
218
|
+
input: process.stdin,
|
|
219
|
+
output: process.stdout,
|
|
220
|
+
});
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log("Label this session (Enter to skip — you'll be reminded):");
|
|
223
|
+
name = (await prompt(rl, " name: ")).trim();
|
|
224
|
+
const kindRaw = await prompt(rl, " kind [long/outcome]: ");
|
|
225
|
+
purpose = (await prompt(rl, " purpose: ")).trim();
|
|
226
|
+
const priorityRaw = await prompt(rl, " priority [low/med/high]: ");
|
|
227
|
+
rl.close();
|
|
228
|
+
kind = normalizeKind(kindRaw);
|
|
229
|
+
priority = normalizePriority(priorityRaw);
|
|
165
230
|
}
|
|
231
|
+
|
|
232
|
+
const meta = {
|
|
233
|
+
uvs_session_id: sid,
|
|
234
|
+
name,
|
|
235
|
+
kind,
|
|
236
|
+
purpose,
|
|
237
|
+
priority,
|
|
238
|
+
persona,
|
|
239
|
+
cwd: projectDir,
|
|
240
|
+
started_at: Math.floor(Date.now() / 1000),
|
|
241
|
+
};
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
path.join(sessionsDir, `${sid}.json`),
|
|
244
|
+
JSON.stringify(meta, null, 2),
|
|
245
|
+
);
|
|
246
|
+
fs.writeFileSync(path.join(stateDir, "current-session.txt"), sid);
|
|
247
|
+
|
|
248
|
+
return { sid, name };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Backwards-compat shim — older code in this file still references this name.
|
|
252
|
+
function ensureInstalled(persona) {
|
|
253
|
+
syncPackageFiles(persona);
|
|
166
254
|
}
|
|
167
255
|
|
|
168
|
-
function launchClaude(persona, extra) {
|
|
169
|
-
|
|
170
|
-
const settings = path.resolve(
|
|
256
|
+
async function launchClaude(persona, extra) {
|
|
257
|
+
syncPackageFiles(persona);
|
|
258
|
+
const settings = path.resolve(".claude/personas", `${persona}.json`);
|
|
171
259
|
if (!fs.existsSync(settings)) {
|
|
172
|
-
console.error(
|
|
260
|
+
console.error(
|
|
261
|
+
`Error: installation failed. Run 'uvs install --persona ${persona}' manually.`,
|
|
262
|
+
);
|
|
173
263
|
process.exit(1);
|
|
174
264
|
}
|
|
265
|
+
const { sid, name } = await setupSession(persona);
|
|
266
|
+
console.log("");
|
|
175
267
|
console.log(`UV Suite | Claude Code | ${personaLabel(persona)}`);
|
|
176
|
-
console.log(
|
|
177
|
-
|
|
178
|
-
child
|
|
268
|
+
console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
|
|
269
|
+
console.log("");
|
|
270
|
+
const child = spawn("claude", ["--settings", settings, ...extra], {
|
|
271
|
+
stdio: "inherit",
|
|
272
|
+
env: { ...process.env, UVS_SESSION_ID: sid },
|
|
273
|
+
});
|
|
274
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
179
275
|
}
|
|
180
276
|
|
|
181
|
-
function launchCodex(persona, extra) {
|
|
277
|
+
async function launchCodex(persona, extra) {
|
|
182
278
|
const approvalMap = {
|
|
183
|
-
spike: [
|
|
184
|
-
sport: [
|
|
185
|
-
professional: [
|
|
186
|
-
auto: [
|
|
279
|
+
spike: ["--model", "o3", "--approval-mode", "suggest"],
|
|
280
|
+
sport: ["--approval-mode", "auto-edit"],
|
|
281
|
+
professional: ["--approval-mode", "suggest"],
|
|
282
|
+
auto: ["--approval-mode", "full-auto"],
|
|
187
283
|
};
|
|
188
|
-
const codexArgs = approvalMap[persona] || [
|
|
284
|
+
const codexArgs = approvalMap[persona] || ["--approval-mode", "suggest"];
|
|
285
|
+
const { sid, name } = await setupSession(persona);
|
|
286
|
+
console.log("");
|
|
189
287
|
console.log(`UV Suite | Codex | ${personaLabel(persona)}`);
|
|
190
|
-
console.log(
|
|
191
|
-
|
|
192
|
-
child
|
|
288
|
+
console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
|
|
289
|
+
console.log("");
|
|
290
|
+
const child = spawn("codex", [...codexArgs, ...extra], {
|
|
291
|
+
stdio: "inherit",
|
|
292
|
+
env: { ...process.env, UVS_SESSION_ID: sid },
|
|
293
|
+
});
|
|
294
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
193
295
|
}
|
|
194
296
|
|
|
195
297
|
function watch() {
|
|
196
|
-
const serverScript = path.join(UV_SUITE_DIR,
|
|
298
|
+
const serverScript = path.join(UV_SUITE_DIR, "watchtower", "server.js");
|
|
197
299
|
if (!fs.existsSync(serverScript)) {
|
|
198
|
-
console.error(
|
|
300
|
+
console.error("Error: watchtower server not found at", serverScript);
|
|
199
301
|
process.exit(1);
|
|
200
302
|
}
|
|
201
303
|
|
|
202
|
-
const bg = args.includes(
|
|
203
|
-
console.log(
|
|
204
|
-
console.log(
|
|
205
|
-
|
|
304
|
+
const bg = args.includes("--bg") || args.includes("--background");
|
|
305
|
+
console.log("UV Suite Watchtower starting...");
|
|
306
|
+
console.log(
|
|
307
|
+
"Dashboard: http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200),
|
|
308
|
+
);
|
|
309
|
+
console.log("");
|
|
206
310
|
|
|
207
311
|
if (bg) {
|
|
208
|
-
const child = spawn(
|
|
209
|
-
stdio:
|
|
312
|
+
const child = spawn("node", [serverScript], {
|
|
313
|
+
stdio: "ignore",
|
|
210
314
|
detached: true,
|
|
211
315
|
});
|
|
212
316
|
child.unref();
|
|
213
317
|
console.log(`Running in background (PID: ${child.pid})`);
|
|
214
|
-
console.log(
|
|
318
|
+
console.log("Stop with: kill " + child.pid);
|
|
215
319
|
|
|
216
320
|
// Open browser
|
|
217
|
-
const opener =
|
|
218
|
-
|
|
321
|
+
const opener =
|
|
322
|
+
process.platform === "darwin"
|
|
323
|
+
? "open"
|
|
324
|
+
: process.platform === "win32"
|
|
325
|
+
? "start"
|
|
326
|
+
: "xdg-open";
|
|
327
|
+
spawn(
|
|
328
|
+
opener,
|
|
329
|
+
["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
|
|
330
|
+
{ stdio: "ignore" },
|
|
331
|
+
);
|
|
219
332
|
} else {
|
|
220
333
|
// Foreground — open browser after a short delay
|
|
221
334
|
setTimeout(() => {
|
|
222
|
-
const opener =
|
|
223
|
-
|
|
335
|
+
const opener =
|
|
336
|
+
process.platform === "darwin"
|
|
337
|
+
? "open"
|
|
338
|
+
: process.platform === "win32"
|
|
339
|
+
? "start"
|
|
340
|
+
: "xdg-open";
|
|
341
|
+
spawn(
|
|
342
|
+
opener,
|
|
343
|
+
["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
|
|
344
|
+
{ stdio: "ignore" },
|
|
345
|
+
);
|
|
224
346
|
}, 1000);
|
|
225
347
|
|
|
226
|
-
const child = spawn(
|
|
227
|
-
child.on(
|
|
348
|
+
const child = spawn("node", [serverScript], { stdio: "inherit" });
|
|
349
|
+
child.on("exit", (code) => process.exit(code || 0));
|
|
228
350
|
}
|
|
229
351
|
}
|
|
230
352
|
|
|
231
353
|
// --- Parse and route ---
|
|
232
354
|
|
|
233
|
-
if (!command || command ===
|
|
355
|
+
if (!command || command === "--help" || command === "-h") {
|
|
234
356
|
usage();
|
|
235
357
|
process.exit(0);
|
|
236
358
|
}
|
|
237
359
|
|
|
238
|
-
|
|
239
|
-
watch
|
|
240
|
-
|
|
241
|
-
install
|
|
242
|
-
|
|
243
|
-
info
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
360
|
+
(async () => {
|
|
361
|
+
if (command === "watch") {
|
|
362
|
+
watch();
|
|
363
|
+
} else if (command === "install") {
|
|
364
|
+
install();
|
|
365
|
+
} else if (command === "info") {
|
|
366
|
+
info();
|
|
367
|
+
} else if (TOOLS.includes(command)) {
|
|
368
|
+
// uvs claude pro, uvs codex auto
|
|
369
|
+
const persona = normPersona(args[1] || "pro");
|
|
370
|
+
if (!persona) {
|
|
371
|
+
console.error(`Unknown persona: ${args[1]}`);
|
|
372
|
+
console.error("Available: spike, sport, pro, auto");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
const extra = args.slice(2);
|
|
376
|
+
if (command === "claude") await launchClaude(persona, extra);
|
|
377
|
+
else await launchCodex(persona, extra);
|
|
378
|
+
} else if (normPersona(command)) {
|
|
379
|
+
// uvs pro (shorthand for uvs claude pro)
|
|
380
|
+
const persona = normPersona(command);
|
|
381
|
+
const extra = args.slice(1);
|
|
382
|
+
await launchClaude(persona, extra);
|
|
383
|
+
} else {
|
|
384
|
+
console.error(`Unknown command: ${command}`);
|
|
385
|
+
usage();
|
|
250
386
|
process.exit(1);
|
|
251
387
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
else launchCodex(persona, extra);
|
|
255
|
-
} else if (normPersona(command)) {
|
|
256
|
-
// uv pro (shorthand for uv claude pro)
|
|
257
|
-
const persona = normPersona(command);
|
|
258
|
-
const extra = args.slice(1);
|
|
259
|
-
launchClaude(persona, extra);
|
|
260
|
-
} else {
|
|
261
|
-
console.error(`Unknown command: ${command}`);
|
|
262
|
-
usage();
|
|
388
|
+
})().catch((err) => {
|
|
389
|
+
console.error(err);
|
|
263
390
|
process.exit(1);
|
|
264
|
-
}
|
|
391
|
+
});
|
package/hooks/confirm-prompt.sh
CHANGED
|
@@ -40,8 +40,19 @@ WORDS=$(echo "$PROMPT" | wc -w | tr -d ' ')
|
|
|
40
40
|
[ "$WORDS" -le "$THRESHOLD" ] && exit 0
|
|
41
41
|
|
|
42
42
|
# Emit Claude Code hook output. additionalContext is appended to the system
|
|
43
|
-
# context for this turn.
|
|
44
|
-
|
|
43
|
+
# context for this turn. The instructions below shape the *style* of the
|
|
44
|
+
# confirmation, not just whether one happens — the response should set
|
|
45
|
+
# context, break the ask into bullets, and end with an explicit invitation
|
|
46
|
+
# for the user to redirect before any work starts.
|
|
47
|
+
ADDITIONAL=$(printf '[uv-suite confirm-mode] The user prompt is %s words (threshold %s). Before any work or tool calls, write a confirmation in this exact shape:
|
|
48
|
+
|
|
49
|
+
1. Open with one short sentence that restates the goal in your own words — set the frame for what you think the user is asking for.
|
|
50
|
+
2. Then a bulleted breakdown (3-7 bullets, one line each) covering: concrete deliverables, the key decisions or assumptions you intend to make, and any open questions or scope choices the user might want to redirect.
|
|
51
|
+
3. End with a single explicit prompt: "Want me to change anything before I start, or should I go ahead?"
|
|
52
|
+
|
|
53
|
+
Do not propose implementation steps, write code, or call tools yet. The point is to confirm understanding so the user can correct course cheaply. Only proceed once the user confirms.
|
|
54
|
+
|
|
55
|
+
The user can disable this with /confirm off or change the threshold with /confirm <number>.' "$WORDS" "$THRESHOLD")
|
|
45
56
|
|
|
46
57
|
if command -v jq >/dev/null 2>&1; then
|
|
47
58
|
jq -nc --arg ctx "$ADDITIONAL" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$ctx}}'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.5",
|
|
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",
|