@rex_koh/subagent-budget-guard 0.1.2 → 0.1.4
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 +10 -1
- package/README.md +19 -5
- package/bin/setup.js +7 -0
- package/lib/guard.js +165 -13
- package/lib/verifier.js +51 -1
- package/package.json +1 -1
- package/skills/setup/SKILL.md +13 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "subagent-budget-guard",
|
|
3
3
|
"displayName": "Subagent Budget Guard",
|
|
4
4
|
"description": "Hard-deny subagent launches, record verified subagent usage, and enforce a session budget against Claude Code's 5-hour rate-limit percentage.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ClaudeSubAgentSuppressor"
|
|
8
8
|
},
|
|
@@ -31,6 +31,15 @@
|
|
|
31
31
|
"min": 0,
|
|
32
32
|
"required": true
|
|
33
33
|
},
|
|
34
|
+
"subagent_token_warning_threshold_percent": {
|
|
35
|
+
"type": "number",
|
|
36
|
+
"title": "Subagent token warning threshold percent",
|
|
37
|
+
"description": "Warn Claude to stop using subagents once verified subagent token usage reaches this percentage of max_subagent_tokens_per_session.",
|
|
38
|
+
"default": 95,
|
|
39
|
+
"min": 1,
|
|
40
|
+
"max": 100,
|
|
41
|
+
"required": true
|
|
42
|
+
},
|
|
34
43
|
"session_five_hour_budget_percent": {
|
|
35
44
|
"type": "number",
|
|
36
45
|
"title": "Session 5-hour budget percent",
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Subagent Budget Guard
|
|
2
2
|
|
|
3
|
-
Claude Code plugin that blocks subagents
|
|
3
|
+
Claude Code plugin that blocks subagents before setup, records verified subagent usage, and enforces a session budget against Claude Code's 5-hour usage percentage.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,13 +10,14 @@ Recommended Claude Code install:
|
|
|
10
10
|
/plugin marketplace add rexkoh425/ClaudeSubAgentSuppressor
|
|
11
11
|
/plugin install subagent-budget-guard@subagent-budget-tools
|
|
12
12
|
/reload-plugins
|
|
13
|
+
/subagent-budget-guard:setup
|
|
14
|
+
/reload-plugins
|
|
15
|
+
/subagent-budget-guard:verify
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Useful after install:
|
|
16
19
|
|
|
17
20
|
```text
|
|
18
|
-
/subagent-budget-guard:setup
|
|
19
|
-
/subagent-budget-guard:verify
|
|
20
21
|
/subagent-budget-guard:report
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -43,4 +44,17 @@ Offline verification:
|
|
|
43
44
|
node bin/verify.js --offline
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
The plugin is strict
|
|
47
|
+
The plugin is strict before setup: `max_concurrent_subagents` defaults to `0`, so normal subagent launches are blocked unless raised. Run `/subagent-budget-guard:setup` to replace the long `--config ...` install command with the recommended config:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
max_concurrent_subagents=1
|
|
51
|
+
max_subagent_tokens_per_session=100000
|
|
52
|
+
subagent_token_warning_threshold_percent=95
|
|
53
|
+
session_five_hour_budget_percent=25
|
|
54
|
+
absolute_five_hour_ceiling_percent=95
|
|
55
|
+
enforcement_enabled=true
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For existing installs, setup also removes obsolete `max_subagents_per_session` and `max_agent_team_tasks_per_session` options from this plugin's Claude settings.
|
|
59
|
+
|
|
60
|
+
`max_subagent_tokens_per_session` is enforced from verified `Agent.totalTokens` values after each completed subagent. `subagent_token_warning_threshold_percent` defaults to `95`; once verified subagent usage reaches that percentage, the plugin tells Claude to stop using subagents and blocks future subagent launches. Claude Code does not expose mid-run per-token subagent streaming to hooks, so a single running subagent can only be evaluated when it reports its final token total.
|
package/bin/setup.js
CHANGED
|
@@ -11,6 +11,13 @@ async function main() {
|
|
|
11
11
|
process.stdout.write(
|
|
12
12
|
[
|
|
13
13
|
'Subagent Budget Guard statusLine bridge installed.',
|
|
14
|
+
'Recommended plugin config applied:',
|
|
15
|
+
` max_concurrent_subagents=${result.pluginConfigOptions.max_concurrent_subagents}`,
|
|
16
|
+
` max_subagent_tokens_per_session=${result.pluginConfigOptions.max_subagent_tokens_per_session}`,
|
|
17
|
+
` subagent_token_warning_threshold_percent=${result.pluginConfigOptions.subagent_token_warning_threshold_percent}`,
|
|
18
|
+
` session_five_hour_budget_percent=${result.pluginConfigOptions.session_five_hour_budget_percent}`,
|
|
19
|
+
` absolute_five_hour_ceiling_percent=${result.pluginConfigOptions.absolute_five_hour_ceiling_percent}`,
|
|
20
|
+
` enforcement_enabled=${result.pluginConfigOptions.enforcement_enabled}`,
|
|
14
21
|
`Settings: ${result.settingsPath}`,
|
|
15
22
|
`Bridge state: ${result.bridgePath}`,
|
|
16
23
|
result.previousStatusLine
|
package/lib/guard.js
CHANGED
|
@@ -13,17 +13,33 @@ import {
|
|
|
13
13
|
} from 'node:fs/promises';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
|
|
16
20
|
export const PLUGIN_NAME = 'subagent-budget-guard';
|
|
21
|
+
export const PLUGIN_ID = 'subagent-budget-guard@subagent-budget-tools';
|
|
17
22
|
|
|
18
23
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
19
24
|
max_concurrent_subagents: 0,
|
|
20
25
|
max_subagent_tokens_per_session: 0,
|
|
26
|
+
subagent_token_warning_threshold_percent: 95,
|
|
21
27
|
session_five_hour_budget_percent: 25,
|
|
22
28
|
absolute_five_hour_ceiling_percent: 95,
|
|
23
29
|
enforcement_enabled: true
|
|
24
30
|
});
|
|
25
31
|
|
|
32
|
+
export const SETUP_CONFIG = Object.freeze({
|
|
33
|
+
...DEFAULT_CONFIG,
|
|
34
|
+
max_concurrent_subagents: 1,
|
|
35
|
+
max_subagent_tokens_per_session: 100000
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
export const CONFIG_KEYS = Object.freeze(Object.keys(DEFAULT_CONFIG));
|
|
39
|
+
export const REMOVED_CONFIG_KEYS = Object.freeze([
|
|
40
|
+
'max_subagents_per_session',
|
|
41
|
+
'max_agent_team_tasks_per_session'
|
|
42
|
+
]);
|
|
27
43
|
|
|
28
44
|
const NUMBER_KEYS = new Set(
|
|
29
45
|
CONFIG_KEYS.filter((key) => typeof DEFAULT_CONFIG[key] === 'number')
|
|
@@ -78,6 +94,10 @@ export function loadConfig(env = process.env) {
|
|
|
78
94
|
100,
|
|
79
95
|
config.absolute_five_hour_ceiling_percent
|
|
80
96
|
);
|
|
97
|
+
config.subagent_token_warning_threshold_percent = Math.min(
|
|
98
|
+
100,
|
|
99
|
+
Math.max(1, config.subagent_token_warning_threshold_percent)
|
|
100
|
+
);
|
|
81
101
|
|
|
82
102
|
return config;
|
|
83
103
|
}
|
|
@@ -87,7 +107,7 @@ export function getHomeDir(env = process.env) {
|
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
export function getPluginRoot(env = process.env) {
|
|
90
|
-
return env.CLAUDE_PLUGIN_ROOT ||
|
|
110
|
+
return env.CLAUDE_PLUGIN_ROOT || PACKAGE_ROOT;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
export function getDataDir(env = process.env) {
|
|
@@ -125,6 +145,9 @@ function initialState(sessionId) {
|
|
|
125
145
|
verifiedTokens: 0,
|
|
126
146
|
totalDurationMs: 0,
|
|
127
147
|
totalToolUseCount: 0,
|
|
148
|
+
tokenBudgetWarnings: 0,
|
|
149
|
+
tokenBudgetExceeded: false,
|
|
150
|
+
lastTokenBudgetNoticeAt: null,
|
|
128
151
|
runs: []
|
|
129
152
|
},
|
|
130
153
|
agentTeam: {
|
|
@@ -191,7 +214,23 @@ async function acquireLock(sessionId, env, timeoutMs = 3000) {
|
|
|
191
214
|
}
|
|
192
215
|
|
|
193
216
|
async function readState(sessionId, env) {
|
|
194
|
-
return readJson(stateFile(sessionId, env), initialState(sessionId));
|
|
217
|
+
return normalizeState(await readJson(stateFile(sessionId, env), initialState(sessionId)), sessionId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeState(state, sessionId) {
|
|
221
|
+
const fresh = initialState(sessionId);
|
|
222
|
+
state.subagents = { ...fresh.subagents, ...(state.subagents || {}) };
|
|
223
|
+
state.agentTeam = { ...fresh.agentTeam, ...(state.agentTeam || {}) };
|
|
224
|
+
state.rateLimits = {
|
|
225
|
+
...fresh.rateLimits,
|
|
226
|
+
...(state.rateLimits || {}),
|
|
227
|
+
fiveHour: {
|
|
228
|
+
...fresh.rateLimits.fiveHour,
|
|
229
|
+
...(state.rateLimits?.fiveHour || {})
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
state.events = Array.isArray(state.events) ? state.events : [];
|
|
233
|
+
return state;
|
|
195
234
|
}
|
|
196
235
|
|
|
197
236
|
async function updateState(sessionId, env, updater) {
|
|
@@ -298,12 +337,62 @@ function fiveHourBudgetDecision(state, config) {
|
|
|
298
337
|
return null;
|
|
299
338
|
}
|
|
300
339
|
|
|
340
|
+
function formatCount(value) {
|
|
341
|
+
return Number(value || 0).toLocaleString('en-US');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function subagentTokenBudgetStatus(state, config) {
|
|
345
|
+
const limit = config.max_subagent_tokens_per_session;
|
|
346
|
+
if (!limit || limit <= 0) return null;
|
|
347
|
+
|
|
348
|
+
const used = Number(state.subagents.verifiedTokens || 0);
|
|
349
|
+
const percent = limit > 0 ? (used / limit) * 100 : 0;
|
|
350
|
+
const warningThreshold = config.subagent_token_warning_threshold_percent;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
used,
|
|
354
|
+
limit,
|
|
355
|
+
percent,
|
|
356
|
+
warningThreshold,
|
|
357
|
+
warningTokens: Math.ceil((limit * warningThreshold) / 100),
|
|
358
|
+
atWarning: percent >= warningThreshold,
|
|
359
|
+
atCap: used >= limit
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function subagentTokenBudgetDecision(state, config, { includeWarning = true } = {}) {
|
|
364
|
+
if (!config.enforcement_enabled) return null;
|
|
365
|
+
const status = subagentTokenBudgetStatus(state, config);
|
|
366
|
+
if (!status) return null;
|
|
367
|
+
|
|
368
|
+
if (status.atCap) {
|
|
369
|
+
return {
|
|
370
|
+
severity: 'cap',
|
|
371
|
+
status,
|
|
372
|
+
reason: `Verified subagent token cap reached: ${formatCount(status.used)}/${formatCount(status.limit)} tokens (${status.percent.toFixed(1)}%). Stop using subagents and ask the user before continuing.`
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (includeWarning && status.atWarning) {
|
|
377
|
+
return {
|
|
378
|
+
severity: 'warning',
|
|
379
|
+
status,
|
|
380
|
+
reason: `Verified subagent token usage reached ${status.percent.toFixed(1)}% of the configured cap (${formatCount(status.used)}/${formatCount(status.limit)} tokens; warning threshold ${status.warningThreshold}%). Stop using subagents and ask the user before continuing.`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
301
387
|
function agentDenyReason(state, config) {
|
|
302
388
|
if (!config.enforcement_enabled) return null;
|
|
303
389
|
|
|
304
390
|
const budgetReason = fiveHourBudgetDecision(state, config);
|
|
305
391
|
if (budgetReason) return budgetReason;
|
|
306
392
|
|
|
393
|
+
const tokenBudgetReason = subagentTokenBudgetDecision(state, config);
|
|
394
|
+
if (tokenBudgetReason) return tokenBudgetReason.reason;
|
|
395
|
+
|
|
307
396
|
if (config.max_concurrent_subagents === 0) {
|
|
308
397
|
return 'Subagent launch denied: max_concurrent_subagents is 0.';
|
|
309
398
|
}
|
|
@@ -312,13 +401,6 @@ function agentDenyReason(state, config) {
|
|
|
312
401
|
return `Subagent launch denied: max_concurrent_subagents ${config.max_concurrent_subagents} already reached.`;
|
|
313
402
|
}
|
|
314
403
|
|
|
315
|
-
if (
|
|
316
|
-
config.max_subagent_tokens_per_session > 0 &&
|
|
317
|
-
state.subagents.verifiedTokens >= config.max_subagent_tokens_per_session
|
|
318
|
-
) {
|
|
319
|
-
return `Subagent launch denied: verified subagent tokens ${state.subagents.verifiedTokens} reached max_subagent_tokens_per_session ${config.max_subagent_tokens_per_session}.`;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
404
|
return null;
|
|
323
405
|
}
|
|
324
406
|
|
|
@@ -377,11 +459,13 @@ function usageTotal(usage = {}) {
|
|
|
377
459
|
|
|
378
460
|
export async function handlePostToolUseAgent(input, env = process.env) {
|
|
379
461
|
const sessionId = input?.session_id || 'unknown-session';
|
|
462
|
+
const config = loadConfig(env);
|
|
380
463
|
const response = input?.tool_response || {};
|
|
381
464
|
const status = response.status || 'unknown';
|
|
382
465
|
const totalTokens =
|
|
383
466
|
numberOrNull(response.totalTokens) ?? usageTotal(response.usage || {});
|
|
384
467
|
const verified = status === 'completed' && totalTokens > 0;
|
|
468
|
+
let tokenBudgetNotice = null;
|
|
385
469
|
|
|
386
470
|
await updateState(sessionId, env, (state) => {
|
|
387
471
|
const run = {
|
|
@@ -409,6 +493,14 @@ export async function handlePostToolUseAgent(input, env = process.env) {
|
|
|
409
493
|
state.subagents.verifiedTokens += totalTokens;
|
|
410
494
|
state.subagents.totalDurationMs += run.totalDurationMs;
|
|
411
495
|
state.subagents.totalToolUseCount += run.totalToolUseCount;
|
|
496
|
+
tokenBudgetNotice = subagentTokenBudgetDecision(state, config);
|
|
497
|
+
if (tokenBudgetNotice) {
|
|
498
|
+
state.subagents.tokenBudgetWarnings += 1;
|
|
499
|
+
state.subagents.lastTokenBudgetNoticeAt = nowIso();
|
|
500
|
+
if (tokenBudgetNotice.severity === 'cap') {
|
|
501
|
+
state.subagents.tokenBudgetExceeded = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
412
504
|
} else if (status === 'async_launched') {
|
|
413
505
|
state.subagents.backgroundLaunched += 1;
|
|
414
506
|
}
|
|
@@ -420,9 +512,22 @@ export async function handlePostToolUseAgent(input, env = process.env) {
|
|
|
420
512
|
verified,
|
|
421
513
|
totalTokens: run.totalTokens
|
|
422
514
|
});
|
|
515
|
+
if (tokenBudgetNotice) {
|
|
516
|
+
pushEvent(state, {
|
|
517
|
+
type: 'subagent-token-budget-notice',
|
|
518
|
+
severity: tokenBudgetNotice.severity,
|
|
519
|
+
used: tokenBudgetNotice.status.used,
|
|
520
|
+
limit: tokenBudgetNotice.status.limit,
|
|
521
|
+
percent: tokenBudgetNotice.status.percent
|
|
522
|
+
});
|
|
523
|
+
}
|
|
423
524
|
return state;
|
|
424
525
|
});
|
|
425
526
|
|
|
527
|
+
if (tokenBudgetNotice) {
|
|
528
|
+
return { exitCode: 2, stdout: null, stderr: tokenBudgetNotice.reason };
|
|
529
|
+
}
|
|
530
|
+
|
|
426
531
|
return { exitCode: 0, stdout: null, stderr: '' };
|
|
427
532
|
}
|
|
428
533
|
|
|
@@ -530,7 +635,10 @@ export async function handleUserPromptSubmit(input, env = process.env) {
|
|
|
530
635
|
const sessionId = input?.session_id || 'unknown-session';
|
|
531
636
|
const config = loadConfig(env);
|
|
532
637
|
const state = await readState(sessionId, env);
|
|
533
|
-
const
|
|
638
|
+
const tokenBudgetReason = subagentTokenBudgetDecision(state, config, {
|
|
639
|
+
includeWarning: false
|
|
640
|
+
});
|
|
641
|
+
const reason = tokenBudgetReason?.reason || fiveHourBudgetDecision(state, config);
|
|
534
642
|
|
|
535
643
|
if (!reason) {
|
|
536
644
|
return { exitCode: 0, stdout: null, stderr: '' };
|
|
@@ -588,6 +696,7 @@ export async function buildReport(sessionId, env = process.env) {
|
|
|
588
696
|
fiveHour.latestUsedPercentage !== null && fiveHour.baselineUsedPercentage !== null
|
|
589
697
|
? Math.max(0, fiveHour.latestUsedPercentage - fiveHour.baselineUsedPercentage)
|
|
590
698
|
: null;
|
|
699
|
+
const tokenBudget = subagentTokenBudgetStatus(state, config);
|
|
591
700
|
|
|
592
701
|
return {
|
|
593
702
|
plugin: PLUGIN_NAME,
|
|
@@ -596,6 +705,9 @@ export async function buildReport(sessionId, env = process.env) {
|
|
|
596
705
|
state,
|
|
597
706
|
summary: {
|
|
598
707
|
verifiedTokenLabel: `${state.subagents.verifiedTokens.toLocaleString('en-US')} verified tokens`,
|
|
708
|
+
subagentTokenBudget: tokenBudget
|
|
709
|
+
? `${formatCount(tokenBudget.used)}/${formatCount(tokenBudget.limit)} verified tokens (${tokenBudget.percent.toFixed(1)}%)`
|
|
710
|
+
: 'no verified-token cap',
|
|
599
711
|
activeSubagents: `${state.subagents.active}/${config.max_concurrent_subagents}`,
|
|
600
712
|
fiveHourBudget:
|
|
601
713
|
consumed === null
|
|
@@ -614,6 +726,7 @@ export function formatReport(report) {
|
|
|
614
726
|
`Enforcement: ${config.enforcement_enabled ? 'enabled' : 'disabled'}`,
|
|
615
727
|
`Subagents: allowed ${state.subagents.allowed}, denied ${state.subagents.denied}, active ${state.subagents.active}, lifecycle starts ${state.subagents.lifecycleStarted}, lifecycle stops ${state.subagents.lifecycleStopped}`,
|
|
616
728
|
`Verified usage: ${summary.verifiedTokenLabel}, ${state.subagents.totalToolUseCount} subagent tool calls, ${state.subagents.totalDurationMs} ms`,
|
|
729
|
+
`Subagent token budget: ${summary.subagentTokenBudget}`,
|
|
617
730
|
`Background launches: ${state.subagents.backgroundLaunched} lifecycle-counted, token totals pending`,
|
|
618
731
|
`Agent-team tasks: created ${state.agentTeam.created}, denied ${state.agentTeam.denied}, completed ${state.agentTeam.completed}`,
|
|
619
732
|
`5-hour budget: ${summary.fiveHourBudget}`
|
|
@@ -659,6 +772,42 @@ function isBridgeStatusLine(statusLine) {
|
|
|
659
772
|
);
|
|
660
773
|
}
|
|
661
774
|
|
|
775
|
+
function isPlainObject(value) {
|
|
776
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function applySetupPluginConfig(
|
|
780
|
+
settings,
|
|
781
|
+
{ pluginId = PLUGIN_ID, setupConfig = SETUP_CONFIG } = {}
|
|
782
|
+
) {
|
|
783
|
+
if (!isPlainObject(settings.pluginConfigs)) {
|
|
784
|
+
settings.pluginConfigs = {};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const currentEntry = isPlainObject(settings.pluginConfigs[pluginId])
|
|
788
|
+
? settings.pluginConfigs[pluginId]
|
|
789
|
+
: {};
|
|
790
|
+
const currentOptions = isPlainObject(currentEntry.options)
|
|
791
|
+
? currentEntry.options
|
|
792
|
+
: {};
|
|
793
|
+
const nextOptions = { ...currentOptions };
|
|
794
|
+
|
|
795
|
+
for (const key of REMOVED_CONFIG_KEYS) {
|
|
796
|
+
delete nextOptions[key];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (const key of CONFIG_KEYS) {
|
|
800
|
+
nextOptions[key] = setupConfig[key];
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
settings.pluginConfigs[pluginId] = {
|
|
804
|
+
...currentEntry,
|
|
805
|
+
options: nextOptions
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
return nextOptions;
|
|
809
|
+
}
|
|
810
|
+
|
|
662
811
|
export async function installStatusLineBridge({
|
|
663
812
|
homeDir = getHomeDir(),
|
|
664
813
|
pluginRoot = getPluginRoot(),
|
|
@@ -680,6 +829,7 @@ export async function installStatusLineBridge({
|
|
|
680
829
|
padding: existing?.padding ?? previousStatusLine?.padding ?? 0,
|
|
681
830
|
refreshInterval: existing?.refreshInterval ?? 5
|
|
682
831
|
};
|
|
832
|
+
const pluginConfigOptions = applySetupPluginConfig(settings);
|
|
683
833
|
|
|
684
834
|
settings.statusLine = nextStatusLine;
|
|
685
835
|
await writeJsonAtomic(settingsPath, settings);
|
|
@@ -696,7 +846,9 @@ export async function installStatusLineBridge({
|
|
|
696
846
|
settingsPath,
|
|
697
847
|
bridgePath,
|
|
698
848
|
command,
|
|
699
|
-
previousStatusLine
|
|
849
|
+
previousStatusLine,
|
|
850
|
+
pluginConfigApplied: true,
|
|
851
|
+
pluginConfigOptions
|
|
700
852
|
};
|
|
701
853
|
}
|
|
702
854
|
|
|
@@ -751,8 +903,8 @@ export async function renderStatusLine(input, {
|
|
|
751
903
|
|
|
752
904
|
const guardSegment =
|
|
753
905
|
fiveHour.latestUsedPercentage === null
|
|
754
|
-
? `SBG agents ${report.state.subagents.active}/${report.config.max_concurrent_subagents} | 5h unknown`
|
|
755
|
-
: `SBG agents ${report.state.subagents.active}/${report.config.max_concurrent_subagents} | 5h ${fiveHour.latestUsedPercentage.toFixed(1)}%`;
|
|
906
|
+
? `SBG agents ${report.state.subagents.active}/${report.config.max_concurrent_subagents} | tokens ${report.summary.subagentTokenBudget} | 5h unknown`
|
|
907
|
+
: `SBG agents ${report.state.subagents.active}/${report.config.max_concurrent_subagents} | tokens ${report.summary.subagentTokenBudget} | 5h ${fiveHour.latestUsedPercentage.toFixed(1)}%`;
|
|
756
908
|
|
|
757
909
|
return previous ? `${previous} | ${guardSegment}` : guardSegment;
|
|
758
910
|
}
|
package/lib/verifier.js
CHANGED
|
@@ -5,6 +5,9 @@ import path from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
CONFIG_KEYS,
|
|
8
|
+
PLUGIN_ID,
|
|
9
|
+
REMOVED_CONFIG_KEYS,
|
|
10
|
+
SETUP_CONFIG,
|
|
8
11
|
buildReport,
|
|
9
12
|
handlePostToolUseAgent,
|
|
10
13
|
handlePreToolUseAgent,
|
|
@@ -87,7 +90,7 @@ export async function runOfflineVerification({
|
|
|
87
90
|
entry.source?.package === '@rex_koh/subagent-budget-guard',
|
|
88
91
|
'marketplace npm package mismatch'
|
|
89
92
|
);
|
|
90
|
-
assert(entry.source?.version === '0.1.
|
|
93
|
+
assert(entry.source?.version === '0.1.4', 'marketplace npm version mismatch');
|
|
91
94
|
return marketplacePath;
|
|
92
95
|
});
|
|
93
96
|
} else {
|
|
@@ -287,6 +290,53 @@ export async function runOfflineVerification({
|
|
|
287
290
|
}
|
|
288
291
|
});
|
|
289
292
|
|
|
293
|
+
await withCheck(result, 'setup-applies-plugin-config', async () => {
|
|
294
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-data-'));
|
|
295
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-home-'));
|
|
296
|
+
try {
|
|
297
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
298
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
299
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
300
|
+
await mkdir(claudeDir, { recursive: true });
|
|
301
|
+
await writeFile(
|
|
302
|
+
settingsPath,
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
pluginConfigs: {
|
|
305
|
+
[PLUGIN_ID]: {
|
|
306
|
+
options: {
|
|
307
|
+
max_subagents_per_session: 9,
|
|
308
|
+
max_concurrent_subagents: 0,
|
|
309
|
+
max_agent_team_tasks_per_session: 4,
|
|
310
|
+
max_subagent_tokens_per_session: 0,
|
|
311
|
+
enforcement_enabled: false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
await installStatusLineBridge({
|
|
319
|
+
homeDir,
|
|
320
|
+
pluginRoot: root,
|
|
321
|
+
pluginData: dataDir
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const settings = await readJson(settingsPath);
|
|
325
|
+
const options = settings.pluginConfigs?.[PLUGIN_ID]?.options;
|
|
326
|
+
assert(options, `missing pluginConfigs.${PLUGIN_ID}.options`);
|
|
327
|
+
for (const key of CONFIG_KEYS) {
|
|
328
|
+
assert(options[key] === SETUP_CONFIG[key], `setup config ${key} mismatch`);
|
|
329
|
+
}
|
|
330
|
+
for (const key of REMOVED_CONFIG_KEYS) {
|
|
331
|
+
assert(!(key in options), `obsolete option ${key} was not removed`);
|
|
332
|
+
}
|
|
333
|
+
return `${PLUGIN_ID} recommended setup config applied`;
|
|
334
|
+
} finally {
|
|
335
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
336
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
290
340
|
result.ok = result.failures.length === 0;
|
|
291
341
|
return result;
|
|
292
342
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rex_koh/subagent-budget-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Claude Code plugin that blocks subagents by default, records verified subagent usage, and enforces 5-hour usage budgets.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ClaudeSubAgentSuppressor",
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Install or refresh the Subagent Budget Guard statusLine bridge
|
|
2
|
+
description: Install or refresh the Subagent Budget Guard statusLine bridge and apply the recommended plugin config.
|
|
3
3
|
disable-model-invocation: true
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -11,7 +11,18 @@ Run this command:
|
|
|
11
11
|
node "${CLAUDE_PLUGIN_ROOT}/bin/setup.js"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
This applies the recommended config in Claude settings:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
max_concurrent_subagents=1
|
|
18
|
+
max_subagent_tokens_per_session=100000
|
|
19
|
+
subagent_token_warning_threshold_percent=95
|
|
20
|
+
session_five_hour_budget_percent=25
|
|
21
|
+
absolute_five_hour_ceiling_percent=95
|
|
22
|
+
enforcement_enabled=true
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then tell the user to run `/reload-plugins`, interact with Claude Code once so the statusLine bridge receives fresh session JSON, and run:
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
28
|
node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
|