@rex_koh/subagent-budget-guard 0.1.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.
- package/.claude-plugin/plugin.json +76 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/hook.js +56 -0
- package/bin/report.js +23 -0
- package/bin/setup.js +26 -0
- package/bin/statusline.js +28 -0
- package/bin/verify.js +27 -0
- package/hooks/hooks.json +120 -0
- package/lib/guard.js +787 -0
- package/lib/verifier.js +373 -0
- package/package.json +51 -0
- package/skills/report/SKILL.md +18 -0
- package/skills/setup/SKILL.md +20 -0
- package/skills/verify/SKILL.md +20 -0
package/lib/verifier.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CONFIG_KEYS,
|
|
8
|
+
buildReport,
|
|
9
|
+
handlePostToolUseAgent,
|
|
10
|
+
handlePreToolUseAgent,
|
|
11
|
+
handleUserPromptSubmit,
|
|
12
|
+
installStatusLineBridge,
|
|
13
|
+
pathExists,
|
|
14
|
+
updateRateLimitFromStatusLine
|
|
15
|
+
} from './guard.js';
|
|
16
|
+
|
|
17
|
+
async function readJson(filePath) {
|
|
18
|
+
const { readFile } = await import('node:fs/promises');
|
|
19
|
+
const text = await readFile(filePath, 'utf8');
|
|
20
|
+
return JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pluginRoot(repoRoot) {
|
|
24
|
+
return path.join(repoRoot, 'plugins', 'subagent-budget-guard');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function withCheck(result, name, fn) {
|
|
28
|
+
try {
|
|
29
|
+
const detail = await fn();
|
|
30
|
+
result.checks.push({ name, ok: true, detail: detail || 'ok' });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
result.checks.push({ name, ok: false, detail: error.message });
|
|
33
|
+
result.failures.push(`${name}: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assert(condition, message) {
|
|
38
|
+
if (!condition) throw new Error(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runOfflineVerification({
|
|
42
|
+
repoRoot = process.cwd(),
|
|
43
|
+
env = process.env
|
|
44
|
+
} = {}) {
|
|
45
|
+
const result = {
|
|
46
|
+
mode: 'offline',
|
|
47
|
+
ok: false,
|
|
48
|
+
checks: [],
|
|
49
|
+
failures: []
|
|
50
|
+
};
|
|
51
|
+
const root = pluginRoot(repoRoot);
|
|
52
|
+
|
|
53
|
+
await withCheck(result, 'marketplace-manifest', async () => {
|
|
54
|
+
const marketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json');
|
|
55
|
+
const marketplace = await readJson(marketplacePath);
|
|
56
|
+
assert(marketplace.name === 'subagent-budget-tools', 'marketplace name mismatch');
|
|
57
|
+
assert(Array.isArray(marketplace.plugins), 'marketplace.plugins must be an array');
|
|
58
|
+
const entry = marketplace.plugins.find((plugin) => plugin.name === 'subagent-budget-guard');
|
|
59
|
+
assert(entry, 'subagent-budget-guard entry missing');
|
|
60
|
+
assert(entry.source?.source === 'npm', 'marketplace source must use npm');
|
|
61
|
+
assert(
|
|
62
|
+
entry.source?.package === '@rex_koh/subagent-budget-guard',
|
|
63
|
+
'marketplace npm package mismatch'
|
|
64
|
+
);
|
|
65
|
+
assert(entry.source?.version === '0.1.0', 'marketplace npm version mismatch');
|
|
66
|
+
return marketplacePath;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await withCheck(result, 'plugin-manifest-user-config', async () => {
|
|
70
|
+
const manifestPath = path.join(root, '.claude-plugin', 'plugin.json');
|
|
71
|
+
const manifest = await readJson(manifestPath);
|
|
72
|
+
assert(manifest.name === 'subagent-budget-guard', 'plugin name mismatch');
|
|
73
|
+
assert(
|
|
74
|
+
manifest.hooks === undefined,
|
|
75
|
+
'manifest.hooks must be omitted for default hooks/hooks.json to avoid duplicate loading'
|
|
76
|
+
);
|
|
77
|
+
assert(
|
|
78
|
+
manifest.skills === undefined,
|
|
79
|
+
'manifest.skills must be omitted for default skills/ scanning to avoid duplicate loading'
|
|
80
|
+
);
|
|
81
|
+
for (const key of CONFIG_KEYS) {
|
|
82
|
+
assert(manifest.userConfig?.[key], `missing userConfig.${key}`);
|
|
83
|
+
}
|
|
84
|
+
return manifestPath;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await withCheck(result, 'hooks-config', async () => {
|
|
88
|
+
const hooksPath = path.join(root, 'hooks', 'hooks.json');
|
|
89
|
+
const hooks = await readJson(hooksPath);
|
|
90
|
+
const requiredEvents = [
|
|
91
|
+
'PreToolUse',
|
|
92
|
+
'PostToolUse',
|
|
93
|
+
'SubagentStart',
|
|
94
|
+
'SubagentStop',
|
|
95
|
+
'TaskCreated',
|
|
96
|
+
'TaskCompleted',
|
|
97
|
+
'UserPromptSubmit'
|
|
98
|
+
];
|
|
99
|
+
for (const event of requiredEvents) {
|
|
100
|
+
assert(Array.isArray(hooks.hooks?.[event]), `missing hooks.${event}`);
|
|
101
|
+
}
|
|
102
|
+
assert(hooks.hooks.PreToolUse[0].matcher === 'Agent', 'PreToolUse must match Agent');
|
|
103
|
+
assert(hooks.hooks.PostToolUse[0].matcher === 'Agent', 'PostToolUse must match Agent');
|
|
104
|
+
return hooksPath;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await withCheck(result, 'script-paths', async () => {
|
|
108
|
+
const scripts = [
|
|
109
|
+
'bin/hook.js',
|
|
110
|
+
'bin/statusline.js',
|
|
111
|
+
'bin/setup.js',
|
|
112
|
+
'bin/report.js',
|
|
113
|
+
'bin/verify.js',
|
|
114
|
+
'lib/guard.js',
|
|
115
|
+
'lib/verifier.js',
|
|
116
|
+
'skills/setup/SKILL.md',
|
|
117
|
+
'skills/report/SKILL.md',
|
|
118
|
+
'skills/verify/SKILL.md'
|
|
119
|
+
];
|
|
120
|
+
for (const script of scripts) {
|
|
121
|
+
assert(await pathExists(path.join(root, script)), `missing ${script}`);
|
|
122
|
+
}
|
|
123
|
+
return `${scripts.length} files present`;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await withCheck(result, 'pretool-agent-denies-default', async () => {
|
|
127
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
|
|
128
|
+
try {
|
|
129
|
+
const checkEnv = {
|
|
130
|
+
...env,
|
|
131
|
+
CLAUDE_PLUGIN_DATA: dataDir,
|
|
132
|
+
CLAUDE_PLUGIN_ROOT: root
|
|
133
|
+
};
|
|
134
|
+
const output = await handlePreToolUseAgent(
|
|
135
|
+
{
|
|
136
|
+
session_id: 'offline-pretool',
|
|
137
|
+
hook_event_name: 'PreToolUse',
|
|
138
|
+
tool_name: 'Agent',
|
|
139
|
+
tool_input: { description: 'verify', subagent_type: 'Explore' }
|
|
140
|
+
},
|
|
141
|
+
checkEnv
|
|
142
|
+
);
|
|
143
|
+
assert(
|
|
144
|
+
output.stdout?.hookSpecificOutput?.permissionDecision === 'deny',
|
|
145
|
+
'Agent launch was not denied by default'
|
|
146
|
+
);
|
|
147
|
+
return output.stdout.hookSpecificOutput.permissionDecisionReason;
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await withCheck(result, 'posttool-agent-records-verified-tokens', async () => {
|
|
154
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
|
|
155
|
+
try {
|
|
156
|
+
const checkEnv = {
|
|
157
|
+
...env,
|
|
158
|
+
CLAUDE_PLUGIN_DATA: dataDir,
|
|
159
|
+
CLAUDE_PLUGIN_ROOT: root
|
|
160
|
+
};
|
|
161
|
+
await handlePostToolUseAgent(
|
|
162
|
+
{
|
|
163
|
+
session_id: 'offline-posttool',
|
|
164
|
+
hook_event_name: 'PostToolUse',
|
|
165
|
+
tool_name: 'Agent',
|
|
166
|
+
tool_input: { description: 'verify', subagent_type: 'Explore' },
|
|
167
|
+
tool_response: {
|
|
168
|
+
status: 'completed',
|
|
169
|
+
agentId: 'agent-verify',
|
|
170
|
+
totalTokens: 101,
|
|
171
|
+
totalToolUseCount: 2,
|
|
172
|
+
totalDurationMs: 300
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
checkEnv
|
|
176
|
+
);
|
|
177
|
+
const report = await buildReport('offline-posttool', checkEnv);
|
|
178
|
+
assert(report.state.subagents.verifiedTokens === 101, 'verified token count mismatch');
|
|
179
|
+
return report.summary.verifiedTokenLabel;
|
|
180
|
+
} finally {
|
|
181
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await withCheck(result, 'statusline-budget-blocks', async () => {
|
|
186
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
|
|
187
|
+
try {
|
|
188
|
+
const checkEnv = {
|
|
189
|
+
...env,
|
|
190
|
+
CLAUDE_PLUGIN_DATA: dataDir,
|
|
191
|
+
CLAUDE_PLUGIN_ROOT: root,
|
|
192
|
+
CLAUDE_PLUGIN_OPTION_session_five_hour_budget_percent: '3'
|
|
193
|
+
};
|
|
194
|
+
await updateRateLimitFromStatusLine(
|
|
195
|
+
{
|
|
196
|
+
session_id: 'offline-budget',
|
|
197
|
+
rate_limits: { five_hour: { used_percentage: 10, resets_at: 1 } }
|
|
198
|
+
},
|
|
199
|
+
checkEnv
|
|
200
|
+
);
|
|
201
|
+
await updateRateLimitFromStatusLine(
|
|
202
|
+
{
|
|
203
|
+
session_id: 'offline-budget',
|
|
204
|
+
rate_limits: { five_hour: { used_percentage: 13.5, resets_at: 1 } }
|
|
205
|
+
},
|
|
206
|
+
checkEnv
|
|
207
|
+
);
|
|
208
|
+
const output = await handleUserPromptSubmit(
|
|
209
|
+
{
|
|
210
|
+
session_id: 'offline-budget',
|
|
211
|
+
hook_event_name: 'UserPromptSubmit',
|
|
212
|
+
prompt: 'continue'
|
|
213
|
+
},
|
|
214
|
+
checkEnv
|
|
215
|
+
);
|
|
216
|
+
assert(output.stdout?.decision === 'block', 'prompt was not blocked');
|
|
217
|
+
return output.stdout.reason;
|
|
218
|
+
} finally {
|
|
219
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await withCheck(result, 'statusline-setup-wraps-existing-command', async () => {
|
|
224
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-data-'));
|
|
225
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-home-'));
|
|
226
|
+
try {
|
|
227
|
+
const { mkdir, writeFile, readFile } = await import('node:fs/promises');
|
|
228
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
229
|
+
await mkdir(claudeDir, { recursive: true });
|
|
230
|
+
await writeFile(
|
|
231
|
+
path.join(claudeDir, 'settings.json'),
|
|
232
|
+
JSON.stringify({
|
|
233
|
+
statusLine: {
|
|
234
|
+
type: 'command',
|
|
235
|
+
command: 'node old-statusline.js'
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
const setup = await installStatusLineBridge({
|
|
240
|
+
homeDir,
|
|
241
|
+
pluginRoot: root,
|
|
242
|
+
pluginData: dataDir
|
|
243
|
+
});
|
|
244
|
+
const settings = JSON.parse(await readFile(path.join(claudeDir, 'settings.json'), 'utf8'));
|
|
245
|
+
assert(settings.statusLine.command.includes('statusline.js'), 'bridge command missing');
|
|
246
|
+
assert(settings.statusLine.command.includes('--data'), 'bridge data arg missing');
|
|
247
|
+
return setup.bridgePath;
|
|
248
|
+
} finally {
|
|
249
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
250
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
result.ok = result.failures.length === 0;
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function commandExists(command) {
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
const child = spawn(process.platform === 'win32' ? 'where.exe' : 'which', [command], {
|
|
261
|
+
stdio: 'ignore',
|
|
262
|
+
windowsHide: true
|
|
263
|
+
});
|
|
264
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
265
|
+
child.on('error', () => resolve(false));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runCommand(command, args, options = {}) {
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const child = spawn(command, args, {
|
|
272
|
+
cwd: options.cwd,
|
|
273
|
+
shell: false,
|
|
274
|
+
windowsHide: true
|
|
275
|
+
});
|
|
276
|
+
let stdout = '';
|
|
277
|
+
let stderr = '';
|
|
278
|
+
child.stdout?.on('data', (chunk) => {
|
|
279
|
+
stdout += chunk;
|
|
280
|
+
});
|
|
281
|
+
child.stderr?.on('data', (chunk) => {
|
|
282
|
+
stderr += chunk;
|
|
283
|
+
});
|
|
284
|
+
child.on('exit', (code) => resolve({ code, stdout, stderr }));
|
|
285
|
+
child.on('error', (error) =>
|
|
286
|
+
resolve({ code: 1, stdout, stderr: `${stderr}\n${error.message}` })
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function runLiveVerification({
|
|
292
|
+
repoRoot = process.cwd(),
|
|
293
|
+
env = process.env
|
|
294
|
+
} = {}) {
|
|
295
|
+
const result = {
|
|
296
|
+
mode: 'live',
|
|
297
|
+
ok: false,
|
|
298
|
+
checks: [],
|
|
299
|
+
failures: [],
|
|
300
|
+
warnings: []
|
|
301
|
+
};
|
|
302
|
+
const offline = await runOfflineVerification({ repoRoot, env });
|
|
303
|
+
result.checks.push(...offline.checks);
|
|
304
|
+
result.failures.push(...offline.failures);
|
|
305
|
+
|
|
306
|
+
const root = pluginRoot(repoRoot);
|
|
307
|
+
const hasClaude = await commandExists('claude');
|
|
308
|
+
if (!hasClaude) {
|
|
309
|
+
result.warnings.push('claude executable was not found on PATH; skipped claude plugin validate and install-state checks.');
|
|
310
|
+
} else {
|
|
311
|
+
await withCheck(result, 'claude-plugin-validate', async () => {
|
|
312
|
+
const validate = await runCommand('claude', ['plugin', 'validate', root], {
|
|
313
|
+
cwd: repoRoot
|
|
314
|
+
});
|
|
315
|
+
assert(validate.code === 0, validate.stderr || validate.stdout || 'claude plugin validate failed');
|
|
316
|
+
return validate.stdout.trim() || 'claude plugin validate passed';
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await withCheck(result, 'claude-plugin-list', async () => {
|
|
320
|
+
const list = await runCommand('claude', ['plugin', 'list'], { cwd: repoRoot });
|
|
321
|
+
assert(list.code === 0, list.stderr || list.stdout || 'claude plugin list failed');
|
|
322
|
+
assert(
|
|
323
|
+
list.stdout.includes('subagent-budget-guard'),
|
|
324
|
+
'subagent-budget-guard is not installed'
|
|
325
|
+
);
|
|
326
|
+
assert(
|
|
327
|
+
!/subagent-budget-guard@subagent-budget-tools[\s\S]*failed to load/i.test(list.stdout),
|
|
328
|
+
'subagent-budget-guard is installed but failed to load'
|
|
329
|
+
);
|
|
330
|
+
return 'claude plugin list returned output';
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await withCheck(result, 'statusline-bridge-configured', async () => {
|
|
335
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
336
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
337
|
+
const settings = await readJson(settingsPath);
|
|
338
|
+
assert(
|
|
339
|
+
typeof settings.statusLine?.command === 'string' &&
|
|
340
|
+
settings.statusLine.command.includes('statusline.js') &&
|
|
341
|
+
settings.statusLine.command.includes('--data'),
|
|
342
|
+
'statusLine bridge is not installed; run /subagent-budget-guard:setup'
|
|
343
|
+
);
|
|
344
|
+
return settings.statusLine.command;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
result.ok = result.failures.length === 0;
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function formatVerificationResult(result) {
|
|
352
|
+
const lines = [
|
|
353
|
+
`Subagent Budget Guard ${result.mode} verification`,
|
|
354
|
+
result.ok ? 'PASS' : 'FAIL'
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
for (const check of result.checks) {
|
|
358
|
+
lines.push(`${check.ok ? 'PASS' : 'FAIL'} ${check.name}: ${check.detail}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const warning of result.warnings || []) {
|
|
362
|
+
lines.push(`WARN ${warning}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (result.failures.length > 0) {
|
|
366
|
+
lines.push('Failures:');
|
|
367
|
+
for (const failure of result.failures) {
|
|
368
|
+
lines.push(`- ${failure}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return lines.join('\n');
|
|
373
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rex_koh/subagent-budget-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code plugin that blocks subagents by default, records verified subagent usage, and enforces 5-hour usage budgets.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "ClaudeSubAgentSuppressor",
|
|
7
|
+
"homepage": "https://github.com/rexkoh425/ClaudeSubAgentSuppressor#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+ssh://git@github.com/rexkoh425/ClaudeSubAgentSuppressor.git",
|
|
11
|
+
"directory": "plugins/subagent-budget-guard"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rexkoh425/ClaudeSubAgentSuppressor/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude-code",
|
|
18
|
+
"claude-plugin",
|
|
19
|
+
"subagents",
|
|
20
|
+
"budget",
|
|
21
|
+
"tokens",
|
|
22
|
+
"rate-limit"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"files": [
|
|
26
|
+
".claude-plugin/",
|
|
27
|
+
"hooks/",
|
|
28
|
+
"skills/",
|
|
29
|
+
"bin/",
|
|
30
|
+
"lib/",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"bin": {
|
|
35
|
+
"subagent-budget-guard-report": "bin/report.js",
|
|
36
|
+
"subagent-budget-guard-setup": "bin/setup.js",
|
|
37
|
+
"subagent-budget-guard-verify": "bin/verify.js"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test test/*.test.js",
|
|
44
|
+
"verify:offline": "node bin/verify.js --offline",
|
|
45
|
+
"verify:live": "node bin/verify.js --live",
|
|
46
|
+
"prepack": "node --check bin/hook.js && node --check bin/statusline.js && node --check bin/setup.js && node --check bin/report.js && node --check bin/verify.js && node --check lib/guard.js && node --check lib/verifier.js"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show the current Subagent Budget Guard session report with subagent counts, verified token totals, and 5-hour budget state.
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Report Subagent Budget Guard Usage
|
|
7
|
+
|
|
8
|
+
Run this command:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/report.js"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If the user asks for machine-readable output, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/report.js" --json
|
|
18
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Install or refresh the Subagent Budget Guard statusLine bridge so 5-hour rate-limit percentages can be captured for enforcement.
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Setup Subagent Budget Guard
|
|
7
|
+
|
|
8
|
+
Run this command:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/setup.js"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then tell the user to interact with Claude Code once so the statusLine bridge receives fresh session JSON. After that, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The live verifier does not submit Claude prompts. It checks local plugin shape, Claude plugin validation when `claude` is on `PATH`, and whether the statusLine bridge is configured.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Verify the Subagent Budget Guard plugin without spending Claude quota, or run live local installation checks.
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify Subagent Budget Guard
|
|
7
|
+
|
|
8
|
+
For default offline verification, run:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --offline
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For live local installation checks, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The live verifier does not submit Claude prompts. It checks local plugin shape, `claude plugin validate` when available, plugin listing shape, and statusLine bridge setup.
|