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.

Mod cho Claude Code, chạy sống trong phiên làm việc của dự án này.
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.
1. Chặn trước khi chạy. Mọi lệnh khớp scripts/run_*.sh đều bị kiểm:
| Điều kiện | Hành động |
|---|---|
Cây bẩn (ngoài experiments/) | chặn, kèm số mục bẩn và lý do |
| GPU đang có job | chặ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ệnh | cho 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.
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ộ.
.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.
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.
hooks/register.tsx 147 lines1/**
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}
147types/index.d.ts 19 lines1export 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