SLOPSHOPPER

low3dgs-guard

Chặn chạy thí nghiệm khi cây làm việc bẩn hoặc GPU đang bận, và hiện trạng thái dự án Low-3DGS trên một dòng.

newbandguardprocesstimer
v0.1.0no licenseupdated 2026-10-03BuiTienDunghe/Low-3DGS/.claude/mods/low3dgs-guard
A shopper browsing a rack in a slop shop
README

low3dgs-guard

Mod cho Claude Code, chạy sống trong phiên làm việc của dự án này.

Nó giải quyết gì

Một loạt thí nghiệm ở đây tốn 45–100 phút GPU. Nếu nó chạy khi cây làm việc đang bẩn, run_meta.json ghi dirty: true, và theo đúng quy ước của chính dự án (docs/VERSIONING.md) kết quả đó không tái lập được — phát hiện ra sau một tiếng rưỡi thì đã muộn. Trong một phiên làm việc, chuyện này suýt xảy ra ba lần.

Nó làm gì

1. Chặn trước khi chạy. Mọi lệnh khớp scripts/run_*.sh đều bị kiểm:

Điều kiệnHành động
Cây bẩn (ngoài experiments/)chặn, kèm số mục bẩn và lý do
GPU đang có jobchặn — hai job trên card 4 GB tranh bộ nhớ, làm hỏng mọi số đo VRAM
Có ALLOW_DIRTY=1 ở đầu lệnhcho qua

Chặn xảy ra trước khi script nạp ảnh, tức mất 0 phút chứ không phải 45.

Phép kiểm bẩn loại trừ experiments/: chính lần chạy ghi kết quả vào đó, nên nếu tính cả thì không lần chạy nào sạch được. Thứ cần biết là mã nguồn lúc chạy có khớp commit không.

2. Một dòng trạng thái trên ô nhập: tag hiện tại, cây sạch/bẩn, VRAM, số job đang chạy. Làm tươi mỗi 20 giây nên vẫn sống trong lúc loạt dài chạy nền. Dim khi ổn, lên màu khi không.

Bật

claude --plugin-dir .claude/mods

Hoặc đặt CLAUDE_CODE_PLUGIN_DIRS trỏ vào thư mục đó.

Đang phát triển thì chép sang ~/.claude/dev-mods/<session-id>/ để nó tự nạp lại mỗi khi sửa file; nhớ chép ngược lại đây khi xong, vì hai bản không tự đồng bộ.

Type cho editor

.claude-plugin/types/ là file sinh theo build, không commit (engine tự đặt .gitignore ở đó). Tạo lại bằng lệnh /plugin-types trong Claude Code.

Đã kiểm

Cả ba đường đã thử thật trong phiên: cây sạch → cho qua · cây bẩn 1 mục → chặn đúng thông điệp · cây bẩn + ALLOW_DIRTY=1 → cho qua.

Source 2 files
hooks/register.tsx 147 lines
1/**
2 * low3dgs-guard — mod cho dự án nén 3D Gaussian Splatting.
3 *
4 * Vì sao tồn tại: một loạt thí nghiệm ở đây mất 45–100 phút GPU. Nếu nó chạy khi
5 * cây làm việc BẨN thì `run_meta.json` ghi `dirty: true`, và theo đúng quy ước của
6 * chính dự án (docs/VERSIONING.md) kết quả đó KHÔNG tái lập được — phát hiện ra
7 * sau một tiếng rưỡi thì đã muộn. Trong một phiên làm việc chuyện này suýt xảy ra
8 * ba lần.
9 *
10 * Mod làm hai việc:
11 *   1. CHẶN lệnh chạy script thí nghiệm khi cây bẩn hoặc GPU đang bận.
12 *   2. Hiện trạng thái dự án trên một dòng ngay trên ô nhập: tag, cây sạch/bẩn,
13 *      VRAM, có gì đang chạy không. Im lặng khi mọi thứ ổn, lên màu khi không.
14 *
15 * Loại trừ `experiments/` khỏi phép kiểm bẩn: chính lần chạy ghi kết quả vào đó,
16 * nên nếu tính cả thì không lần chạy nào sạch được. Thứ cần biết là MÃ NGUỒN lúc
17 * chạy có khớp commit không.
18 */
19import { atom, read, update } from 'claude-code'
20import type { Register } from 'claude-code'
21
22import type { Health } from '../types'
23
24const health = atom({ plugin: 'low3dgs-guard', key: 'health' } as const, null)
25
26/** Lệnh khởi động một loạt thí nghiệm: scripts/run_*.sh */
27const EXPERIMENT = /scripts\/run_[A-Za-z0-9_]+\.sh/
28/** Lối thoát có chủ ý, phải gõ rõ ràng */
29const OVERRIDE = /ALLOW_DIRTY=1/
30
31const num = (s: string): number | null => {
32  const n = Number.parseInt(s.trim(), 10)
33  return Number.isNaN(n) ? null : n
34}
35
36async function probe($: any): Promise<Health> {
37  const at = await $.clock.now()
38  let tag: string | null = null
39  let dirty: number | null = null
40  let gpuMiB: number | null = null
41  let busy: number | null = null
42
43  try {
44    const r = await $.process.run(['git', 'describe', '--tags', '--abbrev=0'])
45    if (r.exitCode === 0) tag = r.stdout.trim() || null
46  } catch {}
47
48  try {
49    // ':!experiments' — loại trừ thư mục kết quả, xem phần đầu file
50    const r = await $.process.run(['git', 'status', '--porcelain', '--', ':!experiments'])
51    if (r.exitCode === 0) {
52      dirty = r.stdout.split('\n').filter((l: string) => l.trim() !== '').length
53    }
54  } catch {}
55
56  try {
57    const r = await $.process.run(
58      ['wsl', 'nvidia-smi', '--query-gpu=memory.used', '--format=csv,noheader,nounits'],
59      { timeoutMs: 8000 },
60    )
61    if (r.exitCode === 0) gpuMiB = num(r.stdout.split('\n')[0] ?? '')
62  } catch {}
63
64  try {
65    const r = await $.process.run(
66      ['wsl', 'nvidia-smi', '--query-compute-apps=pid', '--format=csv,noheader'],
67      { timeoutMs: 8000 },
68    )
69    if (r.exitCode === 0) {
70      busy = r.stdout.split('\n').filter((l: string) => l.trim() !== '').length
71    }
72  } catch {}
73
74  return { tag, dirty, gpuMiB, busy, at }
75}
76
77export const register: Register = on => {
78  on('session.start', async ($, e, next) => {
79    await update($, health, () => null)
80    probe($).then(h => update($, health, () => h)).catch(() => {})
81    // Làm tươi trong lúc loạt chạy dài đang chạy nền, để dòng trạng thái không đứng im
82    $.clock.every(20_000, async () => {
83      try {
84        const h = await probe($)
85        await update($, health, () => h)
86      } catch {}
87    })
88    return next(e)
89  })
90
91  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
92    const cmd = String(e.command ?? '')
93    if (!EXPERIMENT.test(cmd) || OVERRIDE.test(cmd)) return next(e)
94
95    const h = await probe($)
96    await update($, health, () => h)
97
98    if (h.dirty !== null && h.dirty > 0) {
99      return {
100        deny:
101          `low3dgs-guard: cây làm việc đang BẨN (${h.dirty} mục ngoài experiments/).\n` +
102          `Chạy bây giờ thì run_meta.json ghi dirty:true, và theo docs/VERSIONING.md\n` +
103          `kết quả KHÔNG tái lập được — loạt này tốn 45-100 phút GPU.\n` +
104          `Commit trước đã. Cố ý bỏ qua thì thêm ALLOW_DIRTY=1 vào đầu lệnh.`,
105      }
106    }
107    if (h.busy !== null && h.busy > 0) {
108      return {
109        deny:
110          `low3dgs-guard: GPU đang có ${h.busy} tiến trình chạy (${h.gpuMiB ?? '?'} MiB).\n` +
111          `Hai job cùng lúc trên card 4 GB sẽ tranh bộ nhớ và làm hỏng mọi số đo VRAM.\n` +
112          `Đợi nó xong, hoặc thêm ALLOW_DIRTY=1 nếu bạn biết mình đang làm gì.`,
113      }
114    }
115    return next(e)
116  })
117
118  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
119    const h = await read($, health)
120    if (h === null || e.props.hasSurvey) return next(e)
121
122    const { Box, Text } = $.ui.resolve(e)
123    const isDirty = h.dirty !== null && h.dirty > 0
124    const isBusy = h.busy !== null && h.busy > 0
125
126    return (
127      <Box>
128        <Text dimColor>{h.tag ?? 'low-3DGS'} </Text>
129        {isDirty ? (
130          <Text color="yellow" bold>
131            cây bẩn {h.dirty} mục — commit trước khi chạy{' '}
132          </Text>
133        ) : (
134          <Text dimColor>· cây sạch </Text>
135        )}
136        {isBusy ? (
137          <Text color="cyan" bold>
138            · ĐANG CHẠY {h.busy} job · GPU {h.gpuMiB ?? '?'} MiB
139          </Text>
140        ) : (
141          <Text dimColor>· GPU rảnh {h.gpuMiB ?? '?'} MiB</Text>
142        )}
143      </Box>
144    )
145  })
146}
147
types/index.d.ts 19 lines
1export type Health = {
2  /** tag gần nhất, ví dụ "v0.9.0"; null nếu không đọc được */
3  tag: string | null
4  /** số mục bẩn NGOÀI experiments/ — chính lần chạy ghi vào đó nên phải loại trừ */
5  dirty: number | null
6  /** VRAM đang dùng, MiB */
7  gpuMiB: number | null
8  /** số tiến trình đang chiếm GPU */
9  busy: number | null
10  /** thời điểm đo, ms từ epoch */
11  at: number
12}
13
14declare module 'claude-code' {
15  interface PluginState {
16    'low3dgs-guard': { health: Health | null }
17  }
18}
19