uv-suite 0.29.0 → 0.32.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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -46
  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 +146 -73
  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 +2 -2
  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 +35 -0
  27. package/hooks/uv-out-pointer.sh +16 -0
  28. package/hooks/uv-out-session.sh +24 -0
  29. package/hooks/watchtower-end.sh +23 -0
  30. package/hooks/watchtower-notify.sh +56 -0
  31. package/hooks/watchtower-send.sh +10 -3
  32. package/hooks/watchtower-tokens.sh +61 -0
  33. package/install.sh +93 -42
  34. package/package.json +6 -3
  35. package/personas/auto.json +59 -1
  36. package/personas/professional.json +65 -1
  37. package/personas/spike.json +51 -2
  38. package/personas/sport.json +63 -1
  39. package/settings.json +6 -2
  40. package/skills/architect/SKILL.md +109 -8
  41. package/skills/architect/specialists/distributed-systems.md +84 -0
  42. package/skills/architect/specialists/full-stack.md +92 -0
  43. package/skills/architect/specialists/llm-ai-engineering.md +86 -0
  44. package/skills/architect/specialists/ml-systems.md +81 -0
  45. package/skills/commit/SKILL.md +5 -2
  46. package/skills/confirm/SKILL.md +3 -3
  47. package/skills/investigate/SKILL.md +14 -4
  48. package/skills/lite/SKILL.md +45 -0
  49. package/skills/qa/SKILL.md +274 -0
  50. package/skills/review/SKILL.md +187 -8
  51. package/skills/review/specialists/api-contract.md +122 -0
  52. package/skills/review/specialists/architecture-trace.md +64 -0
  53. package/skills/review/specialists/data-migration.md +113 -0
  54. package/skills/review/specialists/maintainability.md +138 -0
  55. package/skills/review/specialists/performance.md +115 -0
  56. package/skills/review/specialists/security.md +132 -0
  57. package/skills/review/specialists/testing.md +109 -0
  58. package/skills/session/SKILL.md +87 -0
  59. package/skills/session/operations/auto.md +22 -0
  60. package/skills/session/operations/checkpoint.md +43 -0
  61. package/skills/session/operations/end.md +35 -0
  62. package/skills/session/operations/init.md +16 -0
  63. package/skills/session/operations/restore.md +16 -0
  64. package/skills/spec/SKILL.md +40 -1
  65. package/skills/test/SKILL.md +89 -0
  66. package/skills/test/specialists/eval.md +46 -0
  67. package/skills/test/specialists/integration.md +42 -0
  68. package/skills/test/specialists/unit.md +39 -0
  69. package/skills/understand/SKILL.md +118 -0
  70. package/skills/understand/modes/repo.md +38 -0
  71. package/skills/understand/modes/stack.md +41 -0
  72. package/skills/uv-help/SKILL.md +43 -20
  73. package/uv.sh +36 -3
  74. package/watchtower/README.md +73 -0
  75. package/watchtower/app/__init__.py +0 -0
  76. package/watchtower/app/__pycache__/__init__.cpython-312.pyc +0 -0
  77. package/watchtower/app/__pycache__/__init__.cpython-314.pyc +0 -0
  78. package/watchtower/app/__pycache__/db.cpython-312.pyc +0 -0
  79. package/watchtower/app/__pycache__/db.cpython-314.pyc +0 -0
  80. package/watchtower/app/__pycache__/main.cpython-312.pyc +0 -0
  81. package/watchtower/app/__pycache__/main.cpython-314.pyc +0 -0
  82. package/watchtower/app/__pycache__/models.cpython-312.pyc +0 -0
  83. package/watchtower/app/__pycache__/models.cpython-314.pyc +0 -0
  84. package/watchtower/app/db.py +129 -0
  85. package/watchtower/app/main.py +43 -0
  86. package/watchtower/app/models.py +54 -0
  87. package/watchtower/app/routers/__init__.py +0 -0
  88. package/watchtower/app/routers/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/watchtower/app/routers/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/watchtower/app/routers/__pycache__/control.cpython-312.pyc +0 -0
  91. package/watchtower/app/routers/__pycache__/control.cpython-314.pyc +0 -0
  92. package/watchtower/app/routers/__pycache__/ingest.cpython-312.pyc +0 -0
  93. package/watchtower/app/routers/__pycache__/ingest.cpython-314.pyc +0 -0
  94. package/watchtower/app/routers/__pycache__/query.cpython-312.pyc +0 -0
  95. package/watchtower/app/routers/__pycache__/query.cpython-314.pyc +0 -0
  96. package/watchtower/app/routers/__pycache__/settings.cpython-312.pyc +0 -0
  97. package/watchtower/app/routers/__pycache__/stream.cpython-312.pyc +0 -0
  98. package/watchtower/app/routers/__pycache__/stream.cpython-314.pyc +0 -0
  99. package/watchtower/app/routers/control.py +260 -0
  100. package/watchtower/app/routers/ingest.py +157 -0
  101. package/watchtower/app/routers/query.py +133 -0
  102. package/watchtower/app/routers/settings.py +34 -0
  103. package/watchtower/app/routers/stream.py +28 -0
  104. package/watchtower/app/services/__init__.py +0 -0
  105. package/watchtower/app/services/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/watchtower/app/services/__pycache__/__init__.cpython-314.pyc +0 -0
  107. package/watchtower/app/services/__pycache__/checkpoint.cpython-312.pyc +0 -0
  108. package/watchtower/app/services/__pycache__/checkpoint.cpython-314.pyc +0 -0
  109. package/watchtower/app/services/__pycache__/tmux.cpython-312.pyc +0 -0
  110. package/watchtower/app/services/__pycache__/tmux.cpython-314.pyc +0 -0
  111. package/watchtower/app/services/checkpoint.py +149 -0
  112. package/watchtower/app/services/tmux.py +54 -0
  113. package/watchtower/events.json +10344 -45
  114. package/watchtower/{auto-checkpoint-runner.js → legacy/auto-checkpoint-runner.js} +29 -2
  115. package/watchtower/requirements.txt +3 -0
  116. package/watchtower/static/dashboard.html +577 -0
  117. package/watchtower/watchtower.db +0 -0
  118. package/agents/claude-code/devops.md +0 -50
  119. package/agents/claude-code/security.md +0 -75
  120. package/agents/codex/anti-slop-guard.toml +0 -12
  121. package/agents/codex/architect.toml +0 -11
  122. package/agents/codex/cartographer.toml +0 -16
  123. package/agents/codex/devops.toml +0 -8
  124. package/agents/codex/eval-writer.toml +0 -11
  125. package/agents/codex/prototype-builder.toml +0 -10
  126. package/agents/codex/reviewer.toml +0 -16
  127. package/agents/codex/security.toml +0 -14
  128. package/agents/codex/spec-writer.toml +0 -11
  129. package/agents/codex/test-writer.toml +0 -13
  130. package/agents/cursor/anti-slop-guard.mdc +0 -22
  131. package/agents/cursor/architect.mdc +0 -24
  132. package/agents/cursor/cartographer.mdc +0 -28
  133. package/agents/cursor/devops.mdc +0 -16
  134. package/agents/cursor/eval-writer.mdc +0 -21
  135. package/agents/cursor/prototype-builder.mdc +0 -25
  136. package/agents/cursor/reviewer.mdc +0 -26
  137. package/agents/cursor/security.mdc +0 -20
  138. package/agents/cursor/spec-writer.mdc +0 -27
  139. package/agents/cursor/test-writer.mdc +0 -28
  140. package/agents/portable/anti-slop-guard.md +0 -71
  141. package/agents/portable/architect.md +0 -83
  142. package/agents/portable/cartographer.md +0 -64
  143. package/agents/portable/devops.md +0 -56
  144. package/agents/portable/eval-writer.md +0 -70
  145. package/agents/portable/prototype-builder.md +0 -70
  146. package/agents/portable/reviewer.md +0 -79
  147. package/agents/portable/security.md +0 -63
  148. package/agents/portable/spec-writer.md +0 -89
  149. package/agents/portable/test-writer.md +0 -56
  150. package/hooks/context-warning.sh +0 -4
  151. package/skills/auto-checkpoint/SKILL.md +0 -47
  152. package/skills/checkpoint/SKILL.md +0 -105
  153. package/skills/map-codebase/SKILL.md +0 -54
  154. package/skills/map-stack/SKILL.md +0 -121
  155. package/skills/restore/SKILL.md +0 -55
  156. package/skills/security-review/SKILL.md +0 -87
  157. package/skills/session-end/SKILL.md +0 -100
  158. package/skills/session-init/SKILL.md +0 -45
  159. package/skills/slop-check/SKILL.md +0 -40
  160. package/skills/write-evals/SKILL.md +0 -34
  161. package/skills/write-tests/SKILL.md +0 -54
  162. /package/watchtower/{auto-checkpoint-prompt.md → legacy/auto-checkpoint-prompt.md} +0 -0
  163. /package/watchtower/{dashboard.html → legacy/dashboard.html} +0 -0
  164. /package/watchtower/{server.js → legacy/server.js} +0 -0
  165. /package/watchtower/{snapshot-manager.js → legacy/snapshot-manager.js} +0 -0
package/bin/cli.js CHANGED
@@ -34,6 +34,7 @@ function usage() {
34
34
  Monitoring:
35
35
  uvs watch Start Watchtower dashboard (open browser)
36
36
  uvs watch --bg Start Watchtower in background
37
+ uvs watch --legacy Start the legacy Node Watchtower
37
38
 
38
39
  Personas:
39
40
  spike Research & docs (Opus, max effort)
@@ -182,19 +183,19 @@ function prompt(rl, question) {
182
183
  return new Promise((resolve) => rl.question(question, resolve));
183
184
  }
184
185
 
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 "";
186
+ // Prompt for one of a fixed set of values (enum). Accepts the number, the full
187
+ // value, or a unique prefix; Enter skips (returns ""). Re-prompts on invalid input.
188
+ async function promptChoice(rl, label, options) {
189
+ const menu = options.map((o, i) => `${i + 1}=${o}`).join(" ");
190
+ for (;;) {
191
+ const raw = (await prompt(rl, `${label} (${menu}, Enter to skip): `)).trim().toLowerCase();
192
+ if (!raw) return "";
193
+ const n = parseInt(raw, 10);
194
+ if (n >= 1 && n <= options.length) return options[n - 1];
195
+ const matches = options.filter((o) => o === raw || o.startsWith(raw));
196
+ if (matches.length === 1) return matches[0];
197
+ console.log(` ? pick one of: ${options.join(", ")} (or its number)`);
198
+ }
198
199
  }
199
200
 
200
201
  // Generate a UVS_SESSION_ID, prompt for metadata (name/kind/purpose/priority),
@@ -220,13 +221,11 @@ async function setupSession(persona) {
220
221
  });
221
222
  console.log("");
222
223
  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]: ");
224
+ name = (await prompt(rl, " name: ")).trim();
225
+ kind = await promptChoice(rl, " kind", ["long-running", "outcome"]);
226
+ purpose = (await prompt(rl, " purpose: ")).trim();
227
+ priority = await promptChoice(rl, " priority", ["low", "med", "high"]);
227
228
  rl.close();
228
- kind = normalizeKind(kindRaw);
229
- priority = normalizePriority(priorityRaw);
230
229
  }
231
230
 
232
231
  const meta = {
@@ -253,6 +252,47 @@ function ensureInstalled(persona) {
253
252
  syncPackageFiles(persona);
254
253
  }
255
254
 
255
+ // Launch `tool` so Watchtower can control it: wrap it in a transparent tmux session
256
+ // (dedicated socket `uvs`) and register the handle, so the dashboard can checkpoint /
257
+ // close / approve it. Falls back to a direct spawn when tmux is unavailable, UVS_NO_TMUX
258
+ // is set, or we're already inside the wrapper.
259
+ function launchWrapped(tool, toolArgs, sid) {
260
+ const { spawnSync } = require("child_process");
261
+ const env = { ...process.env, UVS_SESSION_ID: sid };
262
+ const canTmux =
263
+ !process.env.UVS_NO_TMUX &&
264
+ !process.env.UVS_IN_TMUX &&
265
+ spawnSync("tmux", ["-V"], { stdio: "ignore" }).status === 0;
266
+
267
+ if (canTmux) {
268
+ const tname = "uvs_" + sid;
269
+ const wtUrl = process.env.UVS_WATCHTOWER_URL || "http://localhost:4200";
270
+ const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
271
+ const inner =
272
+ "UVS_SESSION_ID=" + shq(sid) + " UVS_IN_TMUX=1 exec " + [tool, ...toolArgs].map(shq).join(" ");
273
+ const mk = spawnSync(
274
+ "tmux", ["-L", "uvs", "new-session", "-d", "-s", tname, "-c", process.cwd(), inner],
275
+ { stdio: "ignore" },
276
+ );
277
+ if (mk.status === 0) {
278
+ spawnSync("tmux", ["-L", "uvs", "set", "-t", tname, "status", "off"], { stdio: "ignore" });
279
+ spawnSync(
280
+ "curl", ["-s", "-m", "2", wtUrl + "/sessions/register",
281
+ "-H", "Content-Type: application/json",
282
+ "-d", JSON.stringify({ id: sid, tmux_target: tname, pid: process.pid, cwd: process.cwd() })],
283
+ { stdio: "ignore" },
284
+ );
285
+ const att = spawn("tmux", ["-L", "uvs", "attach", "-t", tname], { stdio: "inherit" });
286
+ att.on("exit", (code) => process.exit(code || 0));
287
+ return;
288
+ }
289
+ // tmux new-session failed — fall through to a direct spawn.
290
+ }
291
+
292
+ const child = spawn(tool, toolArgs, { stdio: "inherit", env });
293
+ child.on("exit", (code) => process.exit(code || 0));
294
+ }
295
+
256
296
  async function launchClaude(persona, extra) {
257
297
  syncPackageFiles(persona);
258
298
  const settings = path.resolve(".claude/personas", `${persona}.json`);
@@ -267,11 +307,7 @@ async function launchClaude(persona, extra) {
267
307
  console.log(`UV Suite | Claude Code | ${personaLabel(persona)}`);
268
308
  console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
269
309
  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));
310
+ launchWrapped("claude", ["--settings", settings, ...extra], sid);
275
311
  }
276
312
 
277
313
  async function launchCodex(persona, extra) {
@@ -287,65 +323,102 @@ async function launchCodex(persona, extra) {
287
323
  console.log(`UV Suite | Codex | ${personaLabel(persona)}`);
288
324
  console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
289
325
  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));
326
+ launchWrapped("codex", [...codexArgs, ...extra], sid);
295
327
  }
296
328
 
297
- function watch() {
298
- const serverScript = path.join(UV_SUITE_DIR, "watchtower", "server.js");
299
- if (!fs.existsSync(serverScript)) {
300
- console.error("Error: watchtower server not found at", serverScript);
301
- process.exit(1);
329
+ // Resolve how to run the Python app, provisioning deps on first run.
330
+ // Prefers `uv` (astral) if present; otherwise a venv at watchtower/.venv.
331
+ // Returns the argv prefix to which we append `uvicorn` args.
332
+ function ensurePyEnv(wtDir) {
333
+ const { spawnSync } = require("child_process");
334
+ const hasUv = spawnSync("uv", ["--version"], { stdio: "ignore" }).status === 0;
335
+ if (hasUv) {
336
+ // --native-tls: use the OS cert store so corporate SSL-inspection proxies don't break pypi.
337
+ return ["uv", "run", "--native-tls", "--python", "3.12", "--no-project", "--with-requirements", "requirements.txt", "--", "python", "-m"];
302
338
  }
339
+ const venv = path.join(wtDir, ".venv");
340
+ const py =
341
+ process.platform === "win32"
342
+ ? path.join(venv, "Scripts", "python.exe")
343
+ : path.join(venv, "bin", "python");
344
+ if (!fs.existsSync(py)) {
345
+ const python3 = spawnSync("python3", ["--version"], { stdio: "ignore" }).status === 0 ? "python3" : "python";
346
+ console.log("First run: creating Python env in watchtower/.venv (one-time)...");
347
+ if (spawnSync(python3, ["-m", "venv", ".venv"], { cwd: wtDir, stdio: "inherit" }).status !== 0) {
348
+ console.error("Failed to create venv. Install Python 3 (python3) and retry.");
349
+ process.exit(1);
350
+ }
351
+ console.log("Installing dependencies (fastapi, uvicorn, aiosqlite)...");
352
+ if (spawnSync(py, ["-m", "pip", "install", "-q", "-r", "requirements.txt"], { cwd: wtDir, stdio: "inherit" }).status !== 0) {
353
+ console.error("pip install failed (see output above).");
354
+ process.exit(1);
355
+ }
356
+ }
357
+ return [py, "-m"];
358
+ }
359
+
360
+ // If a Watchtower is already running on this port, stop it so we start fresh —
361
+ // uvicorn doesn't hot-reload, so a long-running process serves stale routes while
362
+ // the (disk-served) dashboard updates. Only kills a process that answers /health
363
+ // like a Watchtower, so we never kill an unrelated app on the port.
364
+ function restartIfRunning(port) {
365
+ const { spawnSync } = require("child_process");
366
+ if (process.platform === "win32") return;
367
+ const health = spawnSync("curl", ["-s", "-m", "1", `http://localhost:${port}/health`], { encoding: "utf8" });
368
+ if (!(health.stdout || "").includes("status")) return;
369
+ const pids = (spawnSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf8" }).stdout || "")
370
+ .trim().split(/\s+/).filter(Boolean);
371
+ if (!pids.length) return;
372
+ spawnSync("kill", pids);
373
+ console.log(`Restarting Watchtower (stopped existing PID ${pids.join(", ")})`);
374
+ spawnSync("sleep", ["1"]); // let the port free up before rebinding
375
+ }
376
+
377
+ function watch() {
378
+ const wtDir = path.join(UV_SUITE_DIR, "watchtower");
303
379
 
380
+ // Legacy fallback: the original Node Watchtower (no Postgres/Docker). `uvs watch --legacy`.
381
+ if (args.includes("--legacy")) {
382
+ const serverScript = path.join(wtDir, "legacy", "server.js");
383
+ if (!fs.existsSync(serverScript)) {
384
+ console.error("Error: legacy watchtower not found at", serverScript);
385
+ process.exit(1);
386
+ }
387
+ const lbg = args.includes("--bg") || args.includes("--background");
388
+ const lurl = "http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200);
389
+ const lopener =
390
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
391
+ console.log("UV Suite Watchtower (legacy Node) starting...");
392
+ console.log("Dashboard: " + lurl);
393
+ console.log("");
394
+ setTimeout(() => spawn(lopener, [lurl], { stdio: "ignore" }), 1000);
395
+ const lchild = spawn("node", [serverScript], { stdio: lbg ? "ignore" : "inherit", detached: lbg });
396
+ if (lbg) {
397
+ lchild.unref();
398
+ console.log(`Running in background (PID: ${lchild.pid}). Stop with: kill ${lchild.pid}`);
399
+ } else {
400
+ lchild.on("exit", (code) => process.exit(code || 0));
401
+ }
402
+ return;
403
+ }
304
404
  const bg = args.includes("--bg") || args.includes("--background");
405
+ const port = process.env.UVS_WATCHTOWER_PORT || 4200;
406
+ const url = "http://localhost:" + port;
407
+ const opener =
408
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
409
+
410
+ // Python + SQLite, run locally (no Docker, no database to install).
411
+ restartIfRunning(port);
305
412
  console.log("UV Suite Watchtower starting...");
306
- console.log(
307
- "Dashboard: http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200),
308
- );
413
+ console.log("Dashboard: " + url);
309
414
  console.log("");
310
-
415
+ const argv = [...ensurePyEnv(wtDir), "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", String(port)];
416
+ setTimeout(() => spawn(opener, [url], { stdio: "ignore" }), 1500);
417
+ const child = spawn(argv[0], argv.slice(1), { cwd: wtDir, stdio: bg ? "ignore" : "inherit", detached: bg });
311
418
  if (bg) {
312
- const child = spawn("node", [serverScript], {
313
- stdio: "ignore",
314
- detached: true,
315
- });
316
419
  child.unref();
317
- console.log(`Running in background (PID: ${child.pid})`);
318
- console.log("Stop with: kill " + child.pid);
319
-
320
- // Open browser
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
- );
420
+ console.log(`Running in background (PID: ${child.pid}). Stop with: kill ${child.pid}`);
332
421
  } else {
333
- // Foreground — open browser after a short delay
334
- setTimeout(() => {
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
- );
346
- }, 1000);
347
-
348
- const child = spawn("node", [serverScript], { stdio: "inherit" });
349
422
  child.on("exit", (code) => process.exit(code || 0));
350
423
  }
351
424
  }
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # UV Suite helper: read or change auto-checkpoint settings.
3
- # Used by the /auto-checkpoint slash command.
3
+ # Used by the /session auto slash command.
4
4
  #
5
5
  # Usage:
6
6
  # auto-checkpoint-helper.sh status
@@ -66,7 +66,7 @@ case "$ARG" in
66
66
  set_field interval_minutes "$ARG"
67
67
  echo "Auto-checkpoint interval: $ARG min (mode: $(get_field mode))"
68
68
  else
69
- echo "Usage: /auto-checkpoint [on | off | <minutes 1-1440> | status]"
69
+ echo "Usage: /session auto [on | off | <minutes 1-1440> | status]"
70
70
  exit 1
71
71
  fi
72
72
  ;;
@@ -5,7 +5,7 @@
5
5
  # Logs every tool call into a per-session activity log. When `interval_minutes`
6
6
  # have passed since the last mechanical checkpoint AND there has been activity
7
7
  # in the interval, writes a deterministic snapshot to
8
- # uv-out/checkpoints/<sid>/auto-<ts>-mechanical.md and forwards an
8
+ # uv-out/sessions/<sid>/checkpoints/auto-<ts>-mechanical.md and forwards an
9
9
  # AutoCheckpoint event to the watchtower.
10
10
  #
11
11
  # Tier B (semantic, claude -p) runs separately from the watchtower's timer.
@@ -96,7 +96,7 @@ fi
96
96
 
97
97
  # Resolve checkpoint dir + metadata via the existing helper
98
98
  CP_DIR=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$PROJECT_DIR/.claude/hooks/checkpoint-helper.sh" dir 2>/dev/null)
99
- [ -z "$CP_DIR" ] && CP_DIR="$PROJECT_DIR/uv-out/checkpoints/$SID" && mkdir -p "$CP_DIR"
99
+ [ -z "$CP_DIR" ] && CP_DIR="$PROJECT_DIR/uv-out/sessions/$SID/checkpoints" && mkdir -p "$CP_DIR"
100
100
 
101
101
  # Build the mechanical checkpoint body
102
102
  TS_FILE=$(date +%Y-%m-%d-%H%M)
@@ -160,7 +160,7 @@ GIT_LOG=$(cd "$PROJECT_DIR" && git log --oneline -5 2>/dev/null)
160
160
  fi
161
161
  } > "$CP_FILE"
162
162
 
163
- # Update latest.md so /restore picks it up
163
+ # Update latest.md so /session restore picks it up
164
164
  cp "$CP_FILE" "$CP_DIR/latest.md" 2>/dev/null
165
165
  echo "$NOW" > "$LAST_CP_FILE"
166
166
 
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # UV Suite Hook: SessionStart — when UVS_RESTORE_FROM is set, inject a
3
- # system-context instruction telling Claude to /restore that session as
3
+ # system-context instruction telling Claude to /session restore that session as
4
4
  # the first action of the new session.
5
5
  #
6
6
  # This is how the Watchtower's "Open in new terminal" restore button hooks
@@ -20,7 +20,7 @@ case "$UVS_RESTORE_FROM" in
20
20
  ;;
21
21
  esac
22
22
 
23
- MSG="[uv-suite auto-restore] This session was opened via the Watchtower restore flow. Your FIRST action must be to run \`/restore $UVS_RESTORE_FROM\` to load the prior session's latest checkpoint. After /restore completes, summarize what you picked up in 1-2 sentences, then wait for the user's next instruction."
23
+ MSG="[uv-suite auto-restore] This session was opened via the Watchtower restore flow. Your FIRST action must be to run \`/session restore $UVS_RESTORE_FROM\` to load the prior session's latest checkpoint. After /session restore completes, summarize what you picked up in 1-2 sentences, then wait for the user's next instruction."
24
24
 
25
25
  if command -v jq >/dev/null 2>&1; then
26
26
  jq -nc --arg ctx "$MSG" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$ctx}}'
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # UV Suite helper: locate per-session checkpoint paths and print metadata.
3
- # Used by the /checkpoint and /restore slash commands.
3
+ # Used by the /session checkpoint and /session restore slash commands.
4
4
  #
5
5
  # Usage:
6
6
  # checkpoint-helper.sh dir # ensure + print the dir for current session
@@ -16,11 +16,14 @@ resolve_paths() {
16
16
  if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
17
17
  SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
18
18
  fi
19
- CHECKPOINTS_ROOT="$PROJECT_DIR/uv-out/checkpoints"
20
- SESSION_CP_DIR=""
21
- [ -n "$SID" ] && SESSION_CP_DIR="$CHECKPOINTS_ROOT/$SID"
22
- META_FILE=""
23
- [ -n "$SID" ] && META_FILE="$STATE_DIR/sessions/$SID.json"
19
+ [ -z "$SID" ] && SID="no-session"
20
+ # Unified layout: everything a session produces lives under uv-out/sessions/<sid>/.
21
+ SESSIONS_ROOT="$PROJECT_DIR/uv-out/sessions"
22
+ SESSION_CP_DIR="$SESSIONS_ROOT/$SID/checkpoints"
23
+ # Legacy locations, read for backward compatibility (pre-unify checkpoints):
24
+ LEGACY_CP_ROOT="$PROJECT_DIR/uv-out/checkpoints"
25
+ LEGACY_SESSION_CP_DIR="$LEGACY_CP_ROOT/$SID"
26
+ META_FILE="$STATE_DIR/sessions/$SID.json"
24
27
  }
25
28
 
26
29
  print_meta_field() {
@@ -37,13 +40,8 @@ resolve_paths
37
40
 
38
41
  case "$1" in
39
42
  dir)
40
- if [ -n "$SESSION_CP_DIR" ]; then
41
- mkdir -p "$SESSION_CP_DIR"
42
- echo "$SESSION_CP_DIR"
43
- else
44
- mkdir -p "$CHECKPOINTS_ROOT"
45
- echo "$CHECKPOINTS_ROOT"
46
- fi
43
+ mkdir -p "$SESSION_CP_DIR"
44
+ echo "$SESSION_CP_DIR"
47
45
  ;;
48
46
  meta)
49
47
  echo "uvs_session_id=${SID:-}"
@@ -73,25 +71,32 @@ checkpoint_at: ${NOW}
73
71
  EOF
74
72
  ;;
75
73
  latest)
76
- if [ -n "$SESSION_CP_DIR" ] && [ -f "$SESSION_CP_DIR/latest.md" ]; then
74
+ if [ -f "$SESSION_CP_DIR/latest.md" ]; then
77
75
  cat "$SESSION_CP_DIR/latest.md"
78
- elif [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
79
- echo "(no per-session checkpoint for ${SID:-this session}; showing legacy global latest.md)"
76
+ elif [ -f "$LEGACY_SESSION_CP_DIR/latest.md" ]; then
77
+ echo "(no checkpoint at new path; showing legacy uv-out/checkpoints/${SID}/latest.md)"
78
+ echo
79
+ cat "$LEGACY_SESSION_CP_DIR/latest.md"
80
+ elif [ -f "$LEGACY_CP_ROOT/latest.md" ]; then
81
+ echo "(no per-session checkpoint for ${SID}; showing legacy global latest.md)"
80
82
  echo
81
- cat "$CHECKPOINTS_ROOT/latest.md"
83
+ cat "$LEGACY_CP_ROOT/latest.md"
82
84
  else
83
- echo "No checkpoint found at $CHECKPOINTS_ROOT. Run /checkpoint to create one."
85
+ echo "No checkpoint found for session ${SID}. Run /session checkpoint to create one."
84
86
  fi
85
87
  ;;
86
88
  list)
87
- [ ! -d "$CHECKPOINTS_ROOT" ] && { echo "No checkpoints directory at $CHECKPOINTS_ROOT"; exit 0; }
88
89
  found=0
89
- for d in "$CHECKPOINTS_ROOT"/*/; do
90
- [ -d "$d" ] || continue
91
- cp_sid=$(basename "$d")
92
- cp_meta="$STATE_DIR/sessions/$cp_sid.json"
93
- cp_name=""
94
- cp_priority=""
90
+ seen=" "
91
+ emit_cp_entry() { # $1 = cp_sid $2 = checkpoint dir $3 = origin tag
92
+ local cp_sid="$1" d="$2" origin="$3"
93
+ [ -d "$d" ] || return 0
94
+ case "$seen" in *" $cp_sid "*) return 0 ;; esac # dedupe sid across new+legacy
95
+ local latest
96
+ latest=$(ls -t "$d"/*.md 2>/dev/null | head -1)
97
+ [ -z "$latest" ] && return 0
98
+ seen="$seen$cp_sid "
99
+ local cp_meta="$STATE_DIR/sessions/$cp_sid.json" cp_name="" cp_priority=""
95
100
  if [ -f "$cp_meta" ]; then
96
101
  if command -v jq >/dev/null 2>&1; then
97
102
  cp_name=$(jq -r '.name // ""' "$cp_meta" 2>/dev/null)
@@ -100,20 +105,20 @@ EOF
100
105
  cp_name=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' "$cp_meta" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\(.*\)"/\1/')
101
106
  fi
102
107
  fi
103
- latest=$(ls -t "$d"*.md 2>/dev/null | head -1)
104
- [ -z "$latest" ] && continue
108
+ local ts label mark
105
109
  ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$latest" 2>/dev/null || stat -c '%y' "$latest" 2>/dev/null | cut -c1-16)
106
110
  label="${cp_name:-(unlabeled)}"
107
111
  [ -n "$cp_priority" ] && label="$label [p:$cp_priority]"
108
- mark=" "
109
- [ "$cp_sid" = "$SID" ] && mark="*"
110
- echo "$mark ${cp_sid:0:8} $ts $label"
112
+ mark=" "; [ "$cp_sid" = "$SID" ] && mark="*"
113
+ echo "$mark ${cp_sid:0:8} $ts $label${origin}"
111
114
  found=1
112
- done
113
- [ "$found" -eq 0 ] && echo "No per-session checkpoints yet (current session: ${SID:-none})"
114
- # Note legacy global checkpoint if present
115
- if [ -f "$CHECKPOINTS_ROOT/latest.md" ]; then
116
- ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null || stat -c '%y' "$CHECKPOINTS_ROOT/latest.md" 2>/dev/null | cut -c1-16)
115
+ }
116
+ # New unified layout first, then legacy, deduped by sid.
117
+ for d in "$SESSIONS_ROOT"/*/checkpoints/; do emit_cp_entry "$(basename "$(dirname "$d")")" "$d" ""; done
118
+ for d in "$LEGACY_CP_ROOT"/*/; do emit_cp_entry "$(basename "$d")" "$d" " (legacy path)"; done
119
+ [ "$found" -eq 0 ] && echo "No per-session checkpoints yet (current session: ${SID})"
120
+ if [ -f "$LEGACY_CP_ROOT/latest.md" ]; then
121
+ ts=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$LEGACY_CP_ROOT/latest.md" 2>/dev/null || stat -c '%y' "$LEGACY_CP_ROOT/latest.md" 2>/dev/null | cut -c1-16)
117
122
  echo " legacy $ts (pre-metadata global latest.md)"
118
123
  fi
119
124
  ;;
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: resolve git worktree context for a directory, as a JSON object.
3
+ # Used to stamp session metadata so the Watchtower can tell apart sessions running
4
+ # in different worktrees of the same repo (e.g. a feature worktree vs the main checkout).
5
+ #
6
+ # Usage: git-context.sh [dir] (defaults to current directory)
7
+ # Prints: {} if not a git repo, else:
8
+ # {
9
+ # "git_worktree": "/abs/path/to/worktree-root",
10
+ # "git_branch": "feature-x",
11
+ # "git_main_repo": "/abs/path/to/main/checkout",
12
+ # "git_is_linked_worktree": true|false
13
+ # }
14
+
15
+ DIR="${1:-.}"
16
+ cd "$DIR" 2>/dev/null || { echo '{}'; exit 0; }
17
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo '{}'; exit 0; }
18
+
19
+ WT=$(git rev-parse --show-toplevel 2>/dev/null)
20
+ BR=$(git branch --show-current 2>/dev/null)
21
+ GITDIR=$(git rev-parse --absolute-git-dir 2>/dev/null)
22
+
23
+ # A linked worktree's git dir lives under <main>/.git/worktrees/<name>.
24
+ case "$GITDIR" in
25
+ */worktrees/*) LINKED=true ;;
26
+ *) LINKED=false ;;
27
+ esac
28
+
29
+ # Common dir points at the shared .git; its parent is the main checkout.
30
+ COMMON=$(git rev-parse --git-common-dir 2>/dev/null)
31
+ case "$COMMON" in /*) ;; *) COMMON="$WT/$COMMON" ;; esac
32
+ MAIN_REPO=$(cd "$(dirname "$COMMON")" 2>/dev/null && pwd)
33
+ [ -z "$MAIN_REPO" ] && MAIN_REPO="$WT"
34
+
35
+ if command -v jq >/dev/null 2>&1; then
36
+ jq -n --arg wt "$WT" --arg br "$BR" --arg main "$MAIN_REPO" --argjson linked "$LINKED" \
37
+ '{git_worktree: $wt, git_branch: $br, git_main_repo: $main, git_is_linked_worktree: $linked}'
38
+ else
39
+ printf '{"git_worktree":"%s","git_branch":"%s","git_main_repo":"%s","git_is_linked_worktree":%s}\n' \
40
+ "$WT" "$BR" "$MAIN_REPO" "$LINKED"
41
+ fi
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # UV Suite Hook: Lite mode — inject terseness instructions
3
+ # Event: UserPromptSubmit
4
+ # Activates when UVS_LITE=1 OR .uv-suite-state/lite-mode.txt contains "on".
5
+ # Reduces output verbosity to save tokens.
6
+
7
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
8
+ STATE_FILE="$STATE_DIR/lite-mode.txt"
9
+
10
+ is_lite="false"
11
+ [ "$UVS_LITE" = "1" ] && is_lite="true"
12
+ [ -f "$STATE_FILE" ] && [ "$(cat "$STATE_FILE" 2>/dev/null | tr -d '[:space:]')" = "on" ] && is_lite="true"
13
+
14
+ if [ "$is_lite" != "true" ]; then
15
+ exit 0
16
+ fi
17
+
18
+ cat <<'EOF'
19
+ {
20
+ "continue": true,
21
+ "hookSpecificOutput": {
22
+ "hookEventName": "UserPromptSubmit",
23
+ "additionalContext": "UV Suite lite mode is ACTIVE. Follow these rules for this turn:\n- No preamble. No \"I'll do X\" before doing it. Start with the action.\n- No end-of-turn summaries longer than one sentence.\n- No bullet lists unless the user explicitly asks for one.\n- No code comments unless the user asks.\n- No markdown headers (##, ###) in responses unless the user asks.\n- Inline single-sentence updates between tool calls; never multi-paragraph narration.\n- Cite file paths inline (file.ts:42), not as section headers.\n- Skip pleasantries, acknowledgments, and reassurances.\nThe user is token-constrained. Be useful, not thorough."
24
+ }
25
+ }
26
+ EOF
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # UV Suite helper: explicit session termination.
3
- # Used by the /session-end slash command.
3
+ # Used by the /session end slash command.
4
4
  #
5
5
  # Writes a "terminated_at" timestamp to the session metadata, fires a
6
6
  # SessionEnd event to the Watchtower so the dashboard updates the status
@@ -53,4 +53,4 @@ print(json.dumps({
53
53
  fi
54
54
 
55
55
  echo "Session ${SID:0:8} marked terminated at $NOW_ISO."
56
- echo "Run /checkpoint first if you want a final state snapshot, then exit the terminal."
56
+ echo "Run /session checkpoint first if you want a final state snapshot, then exit the terminal."
@@ -33,11 +33,11 @@ UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | head -5)
33
33
 
34
34
  REVIEW_MSG=""
35
35
  if [ -n "$STAGED" ] || [ -n "$UNSTAGED" ] || [ -n "$UNTRACKED" ]; then
36
- REVIEW_MSG="Uncommitted changes — consider /review and /slop-check before committing. "
36
+ REVIEW_MSG="Uncommitted changes — consider /review and /review --slop before committing. "
37
37
  fi
38
38
 
39
39
  # Checkpoint prompt
40
- CHECKPOINT_MSG="Run /checkpoint to save session state for next time. Run /restore at the start of your next session."
40
+ CHECKPOINT_MSG="Run /session checkpoint to save session state for next time. Run /session restore at the start of your next session."
41
41
 
42
42
  FULL_MSG="${DURATION_MSG}${REVIEW_MSG}${CHECKPOINT_MSG}"
43
43
 
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Reads the metadata file at .uv-suite-state/sessions/$UVS_SESSION_ID.json.
6
6
  # If the session has no name, injects an additionalContext nudge once every
7
- # Nth user prompt (default 3) so Claude reminds the user to run /session-init.
7
+ # Nth user prompt (default 3) so Claude reminds the user to run /session init.
8
8
  # Skips when the prompt is itself a slash command.
9
9
 
10
10
  INPUT=$(cat)
@@ -53,7 +53,7 @@ if [ "$COUNT" -ne 1 ] && [ "$REMAINDER" -ne 1 ]; then
53
53
  exit 0
54
54
  fi
55
55
 
56
- MSG="[uv-suite] This session has no name yet. Briefly remind the user to run /session-init to set name, kind (long-running/outcome), purpose, and priority — these label the session in the watchtower dashboard. One sentence is enough; then proceed with the user's request."
56
+ MSG="[uv-suite] This session has no name yet. Briefly remind the user to run /session init to set name, kind (long-running/outcome), purpose, and priority — these label the session in the watchtower dashboard. One sentence is enough; then proceed with the user's request."
57
57
 
58
58
  if command -v jq >/dev/null 2>&1; then
59
59
  jq -nc --arg ctx "$MSG" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$ctx}}'
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # UV Suite helper: read or write session metadata.
3
- # Used by the /session-init slash command.
3
+ # Used by the /session init slash command.
4
4
  #
5
5
  # Usage:
6
6
  # session-meta.sh show
@@ -40,6 +40,18 @@ print(json.dumps({
40
40
  ' > "$META"
41
41
  fi
42
42
 
43
+ # Refresh git worktree context (worktree path, branch, main repo) so the Watchtower
44
+ # can distinguish sessions running in different worktrees of the same repo.
45
+ GIT_JSON=$("$(dirname "$0")/git-context.sh" "${CLAUDE_PROJECT_DIR:-$(pwd)}" 2>/dev/null)
46
+ if [ -n "$GIT_JSON" ] && [ "$GIT_JSON" != "{}" ] && command -v jq >/dev/null 2>&1; then
47
+ TMP=$(mktemp)
48
+ if jq -s '.[0] * .[1]' "$META" <(printf '%s' "$GIT_JSON") > "$TMP" 2>/dev/null; then
49
+ mv "$TMP" "$META"
50
+ else
51
+ rm -f "$TMP"
52
+ fi
53
+ fi
54
+
43
55
  ACTION="$1"
44
56
  shift || true
45
57
 
@@ -53,12 +65,17 @@ kind = d.get("kind", "") or "(unset)"
53
65
  purpose = d.get("purpose", "") or "(unset)"
54
66
  priority = d.get("priority", "") or "(unset)"
55
67
  persona = d.get("persona", "") or "(unset)"
68
+ branch = d.get("git_branch", "") or "(unset)"
69
+ worktree = d.get("git_worktree", "") or "(unset)"
70
+ linked = d.get("git_is_linked_worktree", False)
56
71
  print(f"session: {sid}")
57
72
  print(f" name: {name}")
58
73
  print(f" kind: {kind}")
59
74
  print(f" purpose: {purpose}")
60
75
  print(f" priority: {priority}")
61
76
  print(f" persona: {persona}")
77
+ print(f" branch: {branch}")
78
+ print(f" worktree: {worktree}" + (" (linked)" if linked else ""))
62
79
  PY
63
80
  }
64
81
 
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # UV Suite Hook: Remind to review before ending session
3
3
  # Event: Stop
4
- # If there are uncommitted changes, reminds the user to run /review and /slop-check.
4
+ # If there are uncommitted changes, reminds the user to run /review and /review --slop.
5
5
 
6
6
  # Check for uncommitted changes
7
7
  STAGED=$(git diff --cached --stat 2>/dev/null)
@@ -28,7 +28,7 @@ fi
28
28
  cat <<EOF
29
29
  {
30
30
  "continue": true,
31
- "systemMessage": "SESSION END REMINDER: There are uncommitted changes in the working tree.\n\n${SUMMARY}\nConsider running /review and /slop-check before committing."
31
+ "systemMessage": "SESSION END REMINDER: There are uncommitted changes in the working tree.\n\n${SUMMARY}\nConsider running /review and /review --slop before committing."
32
32
  }
33
33
  EOF
34
34