uv-suite 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -35
  3. package/agents/claude-code/anti-slop-guard.md +14 -1
  4. package/agents/claude-code/architect.md +30 -4
  5. package/agents/claude-code/cartographer.md +18 -6
  6. package/agents/claude-code/eval-writer.md +7 -2
  7. package/agents/claude-code/reviewer.md +5 -1
  8. package/agents/claude-code/spec-writer.md +30 -7
  9. package/agents/generate.py +88 -0
  10. package/bin/cli.js +51 -48
  11. package/hooks/auto-checkpoint-helper.sh +2 -2
  12. package/hooks/auto-checkpoint.sh +3 -3
  13. package/hooks/auto-restore-on-start.sh +30 -0
  14. package/hooks/checkpoint-helper.sh +40 -35
  15. package/hooks/git-context.sh +41 -0
  16. package/hooks/lite-mode-inject.sh +26 -0
  17. package/hooks/session-end-helper.sh +2 -2
  18. package/hooks/session-end.sh +2 -2
  19. package/hooks/session-label-nag.sh +2 -2
  20. package/hooks/session-meta.sh +18 -1
  21. package/hooks/session-review-reminder.sh +2 -2
  22. package/hooks/session-start.sh +16 -0
  23. package/hooks/slop-grep.sh +12 -31
  24. package/hooks/uv-out-best.sh +20 -0
  25. package/hooks/uv-out-collect.sh +52 -0
  26. package/hooks/uv-out-notify.sh +28 -0
  27. package/hooks/uv-out-pointer.sh +16 -0
  28. package/hooks/uv-out-session.sh +24 -0
  29. package/hooks/watchtower-notify.sh +45 -0
  30. package/hooks/watchtower-send.sh +4 -0
  31. package/install.sh +93 -42
  32. package/package.json +2 -2
  33. package/personas/auto.json +40 -1
  34. package/personas/professional.json +46 -1
  35. package/personas/spike.json +32 -2
  36. package/personas/sport.json +44 -1
  37. package/settings.json +6 -2
  38. package/skills/architect/SKILL.md +109 -8
  39. package/skills/architect/specialists/distributed-systems.md +84 -0
  40. package/skills/architect/specialists/full-stack.md +92 -0
  41. package/skills/architect/specialists/llm-ai-engineering.md +86 -0
  42. package/skills/architect/specialists/ml-systems.md +81 -0
  43. package/skills/commit/SKILL.md +5 -2
  44. package/skills/confirm/SKILL.md +3 -3
  45. package/skills/investigate/SKILL.md +14 -4
  46. package/skills/lite/SKILL.md +45 -0
  47. package/skills/qa/SKILL.md +274 -0
  48. package/skills/review/SKILL.md +187 -8
  49. package/skills/review/specialists/api-contract.md +122 -0
  50. package/skills/review/specialists/architecture-trace.md +64 -0
  51. package/skills/review/specialists/data-migration.md +113 -0
  52. package/skills/review/specialists/maintainability.md +138 -0
  53. package/skills/review/specialists/performance.md +115 -0
  54. package/skills/review/specialists/security.md +132 -0
  55. package/skills/review/specialists/testing.md +109 -0
  56. package/skills/session/SKILL.md +87 -0
  57. package/skills/session/operations/auto.md +22 -0
  58. package/skills/session/operations/checkpoint.md +43 -0
  59. package/skills/session/operations/end.md +35 -0
  60. package/skills/session/operations/init.md +16 -0
  61. package/skills/session/operations/restore.md +16 -0
  62. package/skills/spec/SKILL.md +40 -1
  63. package/skills/test/SKILL.md +89 -0
  64. package/skills/test/specialists/eval.md +46 -0
  65. package/skills/test/specialists/integration.md +42 -0
  66. package/skills/test/specialists/unit.md +39 -0
  67. package/skills/understand/SKILL.md +118 -0
  68. package/skills/understand/modes/repo.md +38 -0
  69. package/skills/understand/modes/stack.md +41 -0
  70. package/skills/uv-help/SKILL.md +43 -20
  71. package/uv.sh +36 -3
  72. package/watchtower/Dockerfile +9 -0
  73. package/watchtower/README.md +78 -0
  74. package/watchtower/app/__init__.py +0 -0
  75. package/watchtower/app/__pycache__/__init__.cpython-314.pyc +0 -0
  76. package/watchtower/app/__pycache__/db.cpython-314.pyc +0 -0
  77. package/watchtower/app/__pycache__/main.cpython-314.pyc +0 -0
  78. package/watchtower/app/__pycache__/models.cpython-314.pyc +0 -0
  79. package/watchtower/app/db.py +85 -0
  80. package/watchtower/app/main.py +45 -0
  81. package/watchtower/app/models.py +49 -0
  82. package/watchtower/app/routers/__init__.py +0 -0
  83. package/watchtower/app/routers/__pycache__/__init__.cpython-314.pyc +0 -0
  84. package/watchtower/app/routers/__pycache__/control.cpython-314.pyc +0 -0
  85. package/watchtower/app/routers/__pycache__/ingest.cpython-314.pyc +0 -0
  86. package/watchtower/app/routers/__pycache__/query.cpython-314.pyc +0 -0
  87. package/watchtower/app/routers/__pycache__/stream.cpython-314.pyc +0 -0
  88. package/watchtower/app/routers/control.py +144 -0
  89. package/watchtower/app/routers/ingest.py +102 -0
  90. package/watchtower/app/routers/query.py +84 -0
  91. package/watchtower/app/routers/stream.py +30 -0
  92. package/watchtower/app/services/__init__.py +0 -0
  93. package/watchtower/app/services/__pycache__/__init__.cpython-314.pyc +0 -0
  94. package/watchtower/app/services/__pycache__/checkpoint.cpython-314.pyc +0 -0
  95. package/watchtower/app/services/__pycache__/tmux.cpython-314.pyc +0 -0
  96. package/watchtower/app/services/checkpoint.py +107 -0
  97. package/watchtower/app/services/tmux.py +54 -0
  98. package/watchtower/docker-compose.yml +22 -0
  99. package/watchtower/events.json +10373 -1
  100. package/watchtower/{auto-checkpoint-runner.js → legacy/auto-checkpoint-runner.js} +29 -2
  101. package/watchtower/{dashboard.html → legacy/dashboard.html} +261 -0
  102. package/watchtower/{server.js → legacy/server.js} +63 -0
  103. package/watchtower/legacy/snapshot-manager.js +305 -0
  104. package/watchtower/requirements.txt +3 -0
  105. package/watchtower/schema.sql +43 -0
  106. package/watchtower/static/dashboard.html +449 -0
  107. package/agents/claude-code/devops.md +0 -50
  108. package/agents/claude-code/security.md +0 -75
  109. package/agents/codex/anti-slop-guard.toml +0 -12
  110. package/agents/codex/architect.toml +0 -11
  111. package/agents/codex/cartographer.toml +0 -16
  112. package/agents/codex/devops.toml +0 -8
  113. package/agents/codex/eval-writer.toml +0 -11
  114. package/agents/codex/prototype-builder.toml +0 -10
  115. package/agents/codex/reviewer.toml +0 -16
  116. package/agents/codex/security.toml +0 -14
  117. package/agents/codex/spec-writer.toml +0 -11
  118. package/agents/codex/test-writer.toml +0 -13
  119. package/agents/cursor/anti-slop-guard.mdc +0 -22
  120. package/agents/cursor/architect.mdc +0 -24
  121. package/agents/cursor/cartographer.mdc +0 -28
  122. package/agents/cursor/devops.mdc +0 -16
  123. package/agents/cursor/eval-writer.mdc +0 -21
  124. package/agents/cursor/prototype-builder.mdc +0 -25
  125. package/agents/cursor/reviewer.mdc +0 -26
  126. package/agents/cursor/security.mdc +0 -20
  127. package/agents/cursor/spec-writer.mdc +0 -27
  128. package/agents/cursor/test-writer.mdc +0 -28
  129. package/agents/portable/anti-slop-guard.md +0 -71
  130. package/agents/portable/architect.md +0 -83
  131. package/agents/portable/cartographer.md +0 -64
  132. package/agents/portable/devops.md +0 -56
  133. package/agents/portable/eval-writer.md +0 -70
  134. package/agents/portable/prototype-builder.md +0 -70
  135. package/agents/portable/reviewer.md +0 -79
  136. package/agents/portable/security.md +0 -63
  137. package/agents/portable/spec-writer.md +0 -89
  138. package/agents/portable/test-writer.md +0 -56
  139. package/hooks/context-warning.sh +0 -4
  140. package/skills/auto-checkpoint/SKILL.md +0 -47
  141. package/skills/checkpoint/SKILL.md +0 -105
  142. package/skills/map-codebase/SKILL.md +0 -54
  143. package/skills/map-stack/SKILL.md +0 -121
  144. package/skills/restore/SKILL.md +0 -55
  145. package/skills/security-review/SKILL.md +0 -87
  146. package/skills/session-end/SKILL.md +0 -100
  147. package/skills/session-init/SKILL.md +0 -45
  148. package/skills/slop-check/SKILL.md +0 -40
  149. package/skills/write-evals/SKILL.md +0 -34
  150. package/skills/write-tests/SKILL.md +0 -54
  151. /package/watchtower/{auto-checkpoint-prompt.md → legacy/auto-checkpoint-prompt.md} +0 -0
@@ -0,0 +1,305 @@
1
+ // UV Suite Watchtower — snapshot + restore.
2
+ //
3
+ // A "snapshot" bundles the manifest of every active session at a moment in
4
+ // time (sid, cwd, persona, latest checkpoint path) plus a copy of the
5
+ // watchtower's recent event store. It also triggers an immediate Tier B
6
+ // auto-checkpoint for each session so the latest summary is captured.
7
+ //
8
+ // "Restore" doesn't restart Claude Code (we can't — processes are dead).
9
+ // On macOS / Linux it spawns a new terminal tab per session via the OS's
10
+ // AppleScript or terminal-emulator command, passing UVS_RESTORE_FROM=<old-sid>.
11
+ // The next `uvs` launch picks that up and auto-runs `/restore <old-sid>` on
12
+ // turn 1 via the SessionStart hook.
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const os = require("os");
17
+ const { spawn } = require("child_process");
18
+
19
+ const SNAPSHOTS_DIR = path.join(os.homedir(), ".uv-suite", "snapshots");
20
+ const ACTIVE_WINDOW_MS = 60 * 60 * 1000; // sessions with activity in last 1h
21
+
22
+ function ensureSnapshotsDir() {
23
+ fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
24
+ }
25
+
26
+ // Group recent events by uvs_session_id, take the most recent metadata for
27
+ // each. Returns one record per active session.
28
+ function activeSessionsFromEvents(events) {
29
+ const cutoff = Date.now() - ACTIVE_WINDOW_MS;
30
+ const bySession = new Map();
31
+ for (const ev of events) {
32
+ const sid = ev.uvs_session_id || ev.session_id;
33
+ if (!sid) continue;
34
+ if ((ev._ts || 0) < cutoff) continue;
35
+ // Skip if session was explicitly terminated
36
+ if (ev.lifecycle === "terminated") {
37
+ bySession.delete(sid);
38
+ continue;
39
+ }
40
+ const existing = bySession.get(sid) || {
41
+ uvs_session_id: sid,
42
+ session_id: ev.session_id || sid,
43
+ cwd: ev.cwd,
44
+ name: ev.session_name || "",
45
+ kind: ev.session_kind || "",
46
+ purpose: ev.session_purpose || "",
47
+ priority: ev.session_priority || "",
48
+ persona: ev.persona || "",
49
+ first_event_ts: ev._ts,
50
+ last_event_ts: ev._ts,
51
+ event_count: 0,
52
+ };
53
+ existing.last_event_ts = Math.max(existing.last_event_ts || 0, ev._ts || 0);
54
+ existing.first_event_ts = Math.min(
55
+ existing.first_event_ts || Infinity,
56
+ ev._ts || Infinity,
57
+ );
58
+ existing.event_count++;
59
+ // Refresh metadata fields from latest event (in case /session-init relabeled)
60
+ if (ev.session_name) existing.name = ev.session_name;
61
+ if (ev.session_kind) existing.kind = ev.session_kind;
62
+ if (ev.session_purpose) existing.purpose = ev.session_purpose;
63
+ if (ev.session_priority) existing.priority = ev.session_priority;
64
+ if (ev.persona) existing.persona = ev.persona;
65
+ if (ev.session_id) existing.session_id = ev.session_id;
66
+ bySession.set(sid, existing);
67
+ }
68
+ return [...bySession.values()];
69
+ }
70
+
71
+ // Find each session's latest checkpoint file (auto-* or final-*)
72
+ function findLatestCheckpoint(cwd, sid) {
73
+ const dir = path.join(cwd, "uv-out", "checkpoints", sid);
74
+ if (!fs.existsSync(dir)) return null;
75
+ try {
76
+ const files = fs
77
+ .readdirSync(dir)
78
+ .filter((f) => f.endsWith(".md") && f !== "latest.md");
79
+ if (files.length === 0) return null;
80
+ // Sort by mtime, newest first
81
+ files.sort((a, b) => {
82
+ const ma = fs.statSync(path.join(dir, a)).mtimeMs;
83
+ const mb = fs.statSync(path.join(dir, b)).mtimeMs;
84
+ return mb - ma;
85
+ });
86
+ return path.join(dir, files[0]);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ // Read first few lines of summary section from a checkpoint file for the
93
+ // snapshot's per-session preview.
94
+ function readCheckpointSummary(cpPath) {
95
+ if (!cpPath || !fs.existsSync(cpPath)) return "";
96
+ try {
97
+ const content = fs.readFileSync(cpPath, "utf-8");
98
+ // Strip frontmatter
99
+ const body = content.replace(/^---[\s\S]*?---\s*/m, "");
100
+ // Look for ## Summary section
101
+ const m = body.match(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##\s|\n#\s|$)/);
102
+ if (m) return m[1].trim().slice(0, 600);
103
+ // Fallback: first 400 chars of body
104
+ return body.trim().slice(0, 400);
105
+ } catch {
106
+ return "";
107
+ }
108
+ }
109
+
110
+ async function takeSnapshot({ runner, getEvents, broadcast }) {
111
+ ensureSnapshotsDir();
112
+
113
+ // Trigger an immediate Tier B tick so the snapshot bundles the freshest
114
+ // possible summaries. We don't block on broadcast; just await the tick.
115
+ if (runner && runner.tick) {
116
+ await runner.tick({ getEvents, broadcast });
117
+ }
118
+
119
+ const events = getEvents();
120
+ const sessions = activeSessionsFromEvents(events);
121
+
122
+ const ts = new Date();
123
+ const id = ts
124
+ .toISOString()
125
+ .replace(/[T:]/g, "-")
126
+ .replace(/\.\d+Z$/, "")
127
+ .replace(/-(\d\d)$/, "$1");
128
+ const dir = path.join(SNAPSHOTS_DIR, id);
129
+ fs.mkdirSync(dir, { recursive: true });
130
+
131
+ for (const s of sessions) {
132
+ s.latest_checkpoint = findLatestCheckpoint(s.cwd, s.uvs_session_id);
133
+ s.summary_preview = readCheckpointSummary(s.latest_checkpoint);
134
+ }
135
+
136
+ const manifest = {
137
+ id,
138
+ created_at: ts.toISOString(),
139
+ created_at_epoch: Math.floor(ts.getTime() / 1000),
140
+ sessions,
141
+ event_count: events.length,
142
+ };
143
+
144
+ fs.writeFileSync(
145
+ path.join(dir, "manifest.json"),
146
+ JSON.stringify(manifest, null, 2),
147
+ );
148
+ fs.writeFileSync(
149
+ path.join(dir, "events.json"),
150
+ JSON.stringify(events, null, 2),
151
+ );
152
+
153
+ // Broadcast a SnapshotTaken event so it shows up in the timeline
154
+ const event = {
155
+ event_type: "SnapshotTaken",
156
+ snapshot_id: id,
157
+ session_count: sessions.length,
158
+ _ts: ts.getTime(),
159
+ };
160
+ broadcast(event);
161
+
162
+ return manifest;
163
+ }
164
+
165
+ function listSnapshots() {
166
+ ensureSnapshotsDir();
167
+ const entries = [];
168
+ for (const name of fs.readdirSync(SNAPSHOTS_DIR).sort().reverse()) {
169
+ const manifestPath = path.join(SNAPSHOTS_DIR, name, "manifest.json");
170
+ try {
171
+ const m = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
172
+ entries.push({
173
+ id: m.id,
174
+ created_at: m.created_at,
175
+ session_count: m.sessions?.length || 0,
176
+ });
177
+ } catch {
178
+ // skip unreadable bundles
179
+ }
180
+ }
181
+ return entries;
182
+ }
183
+
184
+ function getSnapshot(id) {
185
+ const manifestPath = path.join(SNAPSHOTS_DIR, id, "manifest.json");
186
+ if (!fs.existsSync(manifestPath)) return null;
187
+ try {
188
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ // Spawn a new terminal tab restoring the given session.
195
+ // macOS: iTerm2 if TERM_PROGRAM=iTerm.app (or UVS_TERMINAL_APP=iterm),
196
+ // otherwise Terminal.app. Override either way with UVS_TERMINAL_APP.
197
+ // Linux: prefers gnome-terminal then x-terminal-emulator
198
+ // Other: returns { ok: false, command: <copy-paste string> }
199
+ function buildRestoreCommand(session) {
200
+ const persona = session.persona || "professional";
201
+ const sid = session.uvs_session_id;
202
+ const cwd = session.cwd;
203
+ // The new uvs launch reads UVS_RESTORE_FROM and the SessionStart hook
204
+ // injects "run /restore <sid>" on turn 1.
205
+ return `cd ${shellEscape(cwd)} && UVS_RESTORE_FROM=${shellEscape(sid)} uvs claude ${shellEscape(persona)}`;
206
+ }
207
+
208
+ function shellEscape(s) {
209
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
210
+ }
211
+
212
+ function appleScriptEscape(s) {
213
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
214
+ }
215
+
216
+ // Pick the macOS terminal app. UVS_TERMINAL_APP wins (values: iterm, terminal),
217
+ // else look at $TERM_PROGRAM, else default to Terminal.app.
218
+ function macTerminalApp() {
219
+ const explicit = (process.env.UVS_TERMINAL_APP || "").toLowerCase();
220
+ if (explicit === "iterm" || explicit === "iterm2") return "iterm";
221
+ if (explicit === "terminal") return "terminal";
222
+ const tp = process.env.TERM_PROGRAM || "";
223
+ if (tp === "iTerm.app") return "iterm";
224
+ return "terminal";
225
+ }
226
+
227
+ // iTerm2 AppleScript: opens a new tab in the front window if one exists,
228
+ // otherwise a new window. Both code paths end with `write text "..."`.
229
+ function iTermScript(command) {
230
+ const esc = appleScriptEscape(command);
231
+ return `
232
+ tell application "iTerm"
233
+ activate
234
+ if (count of windows) = 0 then
235
+ create window with default profile
236
+ else
237
+ tell current window to create tab with default profile
238
+ end if
239
+ tell current session of current window to write text "${esc}"
240
+ end tell
241
+ `;
242
+ }
243
+
244
+ function terminalAppScript(command) {
245
+ return `tell app "Terminal" to do script "${appleScriptEscape(command)}"`;
246
+ }
247
+
248
+ function openTerminalForSession(session) {
249
+ const command = buildRestoreCommand(session);
250
+ const platform = process.platform;
251
+
252
+ if (platform === "darwin") {
253
+ const app = macTerminalApp();
254
+ const script =
255
+ app === "iterm" ? iTermScript(command) : terminalAppScript(command);
256
+ const child = spawn("osascript", ["-e", script], {
257
+ stdio: "ignore",
258
+ detached: true,
259
+ });
260
+ child.unref();
261
+ return { ok: true, platform, terminal_app: app, command };
262
+ }
263
+
264
+ if (platform === "linux") {
265
+ // Try gnome-terminal first, fall back to x-terminal-emulator
266
+ const tryEmulator = (cmd, args) => {
267
+ try {
268
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
269
+ child.unref();
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ };
275
+ if (
276
+ tryEmulator("gnome-terminal", [
277
+ "--",
278
+ "bash",
279
+ "-c",
280
+ command + "; exec bash",
281
+ ]) ||
282
+ tryEmulator("x-terminal-emulator", [
283
+ "-e",
284
+ "bash",
285
+ "-c",
286
+ command + "; exec bash",
287
+ ])
288
+ ) {
289
+ return { ok: true, platform, command };
290
+ }
291
+ }
292
+
293
+ // Windows or unsupported — return the command so the dashboard can offer
294
+ // a "copy to clipboard" fallback.
295
+ return { ok: false, platform, command };
296
+ }
297
+
298
+ module.exports = {
299
+ takeSnapshot,
300
+ listSnapshots,
301
+ getSnapshot,
302
+ openTerminalForSession,
303
+ buildRestoreCommand,
304
+ SNAPSHOTS_DIR,
305
+ };
@@ -0,0 +1,3 @@
1
+ fastapi==0.115.6
2
+ uvicorn[standard]==0.34.0
3
+ asyncpg==0.30.0
@@ -0,0 +1,43 @@
1
+ -- Watchtower schema (idempotent — applied on startup).
2
+
3
+ CREATE TABLE IF NOT EXISTS sessions (
4
+ id text PRIMARY KEY, -- UVS session id
5
+ name text,
6
+ kind text,
7
+ purpose text,
8
+ priority text,
9
+ persona text,
10
+ cwd text,
11
+ worktree text,
12
+ branch text,
13
+ pid integer,
14
+ tmux_target text, -- e.g. "uvs_<sid>" (null if not owned)
15
+ state text DEFAULT 'active', -- active | idle | awaiting_human | terminated
16
+ started_at timestamptz DEFAULT now(),
17
+ ended_at timestamptz
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS events (
21
+ id bigserial PRIMARY KEY,
22
+ session_id text,
23
+ event_type text,
24
+ tool_name text,
25
+ command text,
26
+ payload jsonb,
27
+ created_at timestamptz DEFAULT now()
28
+ );
29
+ CREATE INDEX IF NOT EXISTS idx_events_session_created ON events (session_id, created_at DESC);
30
+ CREATE INDEX IF NOT EXISTS idx_events_created ON events (created_at DESC);
31
+
32
+ CREATE TABLE IF NOT EXISTS approvals (
33
+ id bigserial PRIMARY KEY,
34
+ session_id text,
35
+ tool_name text,
36
+ command text,
37
+ request jsonb,
38
+ status text DEFAULT 'pending', -- pending | approved | denied | expired
39
+ decided_by text,
40
+ created_at timestamptz DEFAULT now(),
41
+ decided_at timestamptz
42
+ );
43
+ CREATE INDEX IF NOT EXISTS idx_approvals_session_status ON approvals (session_id, status);