@rex_koh/subagent-budget-guard 0.1.7 → 0.2.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.
@@ -1,8 +1,8 @@
1
1
  {
2
- "name": "subagent-budget-guard",
3
- "displayName": "Subagent Budget Guard",
2
+ "name": "agent-guard",
3
+ "displayName": "Agent 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.7",
5
+ "version": "0.2.0",
6
6
  "author": {
7
7
  "name": "ClaudeSubAgentSuppressor"
8
8
  },
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Subagent Budget Guard
1
+ # Agent Guard
2
2
 
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.
3
+ Claude Code plugin that guards subagent usage, records verified subagent tokens, and enforces a session budget against Claude Code's 5-hour usage percentage.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,17 +8,17 @@ Recommended Claude Code install:
8
8
 
9
9
  ```text
10
10
  /plugin marketplace add rexkoh425/ClaudeSubAgentSuppressor
11
- /plugin install subagent-budget-guard@subagent-budget-tools
12
- /subagent-budget-guard:setup
13
- /subagent-budget-guard:verify
11
+ /plugin install agent-guard@subagent-budget-tools
12
+ /agent-guard:init
13
+ /agent-guard:doctor
14
14
  ```
15
15
 
16
- After `/subagent-budget-guard:setup`, fully exit and reopen Claude Code before verification so the statusLine bridge from `settings.json` is active. Some Claude Code builds do not provide an in-session plugin reload command.
16
+ After `/agent-guard:init`, fully exit and reopen Claude Code before verification so the statusLine bridge from `settings.json` is active. Some Claude Code builds do not provide an in-session plugin reload command.
17
17
 
18
18
  Useful after install:
19
19
 
20
20
  ```text
21
- /subagent-budget-guard:report
21
+ /agent-guard:status
22
22
  ```
23
23
 
24
24
  ## NPM Package
@@ -29,7 +29,7 @@ Claude Code plugin discovery is marketplace-based, so npm is mainly useful as a
29
29
 
30
30
  ```bash
31
31
  npm install -g @rex_koh/subagent-budget-guard
32
- subagent-budget-guard-verify --offline
32
+ agent-guard doctor --offline
33
33
  ```
34
34
 
35
35
  Maintainer publish command:
@@ -44,7 +44,7 @@ Offline verification:
44
44
  node bin/verify.js --offline
45
45
  ```
46
46
 
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:
47
+ The plugin is strict before setup: `max_concurrent_subagents` defaults to `0`, so normal subagent launches are blocked unless raised. Run `/agent-guard:init` to choose defaults or custom values:
48
48
 
49
49
  ```text
50
50
  max_concurrent_subagents=1
@@ -57,4 +57,22 @@ enforcement_enabled=true
57
57
 
58
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
59
 
60
+ The setup skill can also ask for custom values. For direct terminal setup, use:
61
+
62
+ ```bash
63
+ agent-guard init
64
+ ```
65
+
66
+ Or pass explicit values:
67
+
68
+ ```bash
69
+ agent-guard init \
70
+ --config max_concurrent_subagents=2 \
71
+ --config max_subagent_tokens_per_session=250000 \
72
+ --config subagent_token_warning_threshold_percent=90 \
73
+ --config session_five_hour_budget_percent=15 \
74
+ --config absolute_five_hour_ceiling_percent=95 \
75
+ --config enforcement_enabled=true
76
+ ```
77
+
60
78
  `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.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ const COMMANDS = Object.freeze({
9
+ init: {
10
+ script: 'setup.js',
11
+ help: 'initialize settings and the statusLine bridge'
12
+ },
13
+ status: {
14
+ script: 'report.js',
15
+ help: 'show the current guard report'
16
+ },
17
+ doctor: {
18
+ script: 'verify.js',
19
+ help: 'verify the install without spending Claude quota'
20
+ }
21
+ });
22
+
23
+ function usage() {
24
+ return [
25
+ 'Usage: agent-guard <command> [options]',
26
+ '',
27
+ 'Commands:',
28
+ ...Object.entries(COMMANDS).map(([name, info]) => ` ${name.padEnd(8)} ${info.help}`),
29
+ '',
30
+ 'Examples:',
31
+ ' agent-guard init',
32
+ ' agent-guard init --defaults',
33
+ ' agent-guard status',
34
+ ' agent-guard doctor --offline'
35
+ ].join('\n');
36
+ }
37
+
38
+ function main() {
39
+ const [command, ...args] = process.argv.slice(2);
40
+
41
+ if (!command || command === '--help' || command === '-h') {
42
+ process.stdout.write(`${usage()}\n`);
43
+ return;
44
+ }
45
+
46
+ const target = COMMANDS[command];
47
+ if (!target) {
48
+ process.stderr.write(`Unknown command "${command}".\n\n${usage()}\n`);
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ const child = spawnSync(process.execPath, [path.join(BIN_DIR, target.script), ...args], {
54
+ stdio: 'inherit',
55
+ env: process.env,
56
+ cwd: process.cwd(),
57
+ windowsHide: true
58
+ });
59
+
60
+ if (child.error) {
61
+ process.stderr.write(`${child.error.message}\n`);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+
66
+ process.exitCode = child.status ?? 1;
67
+ }
68
+
69
+ main();
package/bin/setup.js CHANGED
@@ -1,17 +1,128 @@
1
1
  #!/usr/bin/env node
2
- import { getDataDir, getHomeDir, getPluginRoot, installStatusLineBridge } from '../lib/guard.js';
2
+ import { createInterface } from 'node:readline/promises';
3
+
4
+ import {
5
+ CONFIG_KEYS,
6
+ SETUP_CONFIG,
7
+ buildSetupConfig,
8
+ getDataDir,
9
+ getHomeDir,
10
+ getPluginRoot,
11
+ installStatusLineBridge
12
+ } from '../lib/guard.js';
13
+
14
+ const CONFIG_KEY_SET = new Set(CONFIG_KEYS);
15
+
16
+ function usage() {
17
+ return [
18
+ 'Usage: agent-guard init [--defaults] [--interactive] [--config key=value ...]',
19
+ '',
20
+ 'Config keys:',
21
+ ...CONFIG_KEYS.map((key) => ` ${key} (default ${SETUP_CONFIG[key]})`)
22
+ ].join('\n');
23
+ }
24
+
25
+ function parseConfigPair(pair) {
26
+ const index = pair.indexOf('=');
27
+ if (index <= 0) {
28
+ throw new Error(`Invalid --config value "${pair}". Expected key=value.`);
29
+ }
30
+
31
+ const key = pair.slice(0, index);
32
+ const value = pair.slice(index + 1);
33
+ if (!CONFIG_KEY_SET.has(key)) {
34
+ throw new Error(`Unknown config key "${key}". Valid keys: ${CONFIG_KEYS.join(', ')}`);
35
+ }
36
+ return [key, value];
37
+ }
38
+
39
+ function parseArgs(args) {
40
+ const options = {
41
+ interactive: false,
42
+ defaults: false,
43
+ overrides: {}
44
+ };
45
+
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const arg = args[index];
48
+ if (arg === '--help' || arg === '-h') {
49
+ process.stdout.write(`${usage()}\n`);
50
+ process.exit(0);
51
+ }
52
+ if (arg === '--interactive') {
53
+ options.interactive = true;
54
+ continue;
55
+ }
56
+ if (arg === '--defaults' || arg === '--yes' || arg === '-y') {
57
+ options.defaults = true;
58
+ continue;
59
+ }
60
+ if (arg === '--config') {
61
+ const pair = args[index + 1];
62
+ if (!pair) throw new Error('--config requires key=value');
63
+ const [key, value] = parseConfigPair(pair);
64
+ options.overrides[key] = value;
65
+ index += 1;
66
+ continue;
67
+ }
68
+ if (arg.startsWith('--config=')) {
69
+ const [key, value] = parseConfigPair(arg.slice('--config='.length));
70
+ options.overrides[key] = value;
71
+ continue;
72
+ }
73
+ throw new Error(`Unknown argument "${arg}".\n${usage()}`);
74
+ }
75
+
76
+ return options;
77
+ }
78
+
79
+ async function promptForConfig(defaults, { askMode = true } = {}) {
80
+ const rl = createInterface({
81
+ input: process.stdin,
82
+ output: process.stderr
83
+ });
84
+ const answers = {};
85
+
86
+ try {
87
+ if (askMode) {
88
+ const useDefaults = await rl.question('Use recommended defaults? [Y/n]: ');
89
+ if (!useDefaults.trim() || /^y(es)?$/i.test(useDefaults.trim())) {
90
+ return buildSetupConfig(defaults);
91
+ }
92
+ }
93
+
94
+ for (const key of CONFIG_KEYS) {
95
+ const answer = await rl.question(`${key} [${defaults[key]}]: `);
96
+ if (answer.trim()) {
97
+ answers[key] = answer.trim();
98
+ }
99
+ }
100
+ } finally {
101
+ rl.close();
102
+ }
103
+
104
+ return buildSetupConfig({ ...defaults, ...answers });
105
+ }
3
106
 
4
107
  async function main() {
108
+ const options = parseArgs(process.argv.slice(2));
109
+ const defaults = buildSetupConfig(options.overrides);
110
+ const setupConfig = options.interactive
111
+ ? await promptForConfig(defaults, { askMode: false })
112
+ : !options.defaults && Object.keys(options.overrides).length === 0 && process.stdin.isTTY
113
+ ? await promptForConfig(defaults)
114
+ : defaults;
5
115
  const result = await installStatusLineBridge({
6
116
  homeDir: getHomeDir(process.env),
7
117
  pluginRoot: getPluginRoot(process.env),
8
- pluginData: getDataDir(process.env)
118
+ pluginData: getDataDir(process.env),
119
+ setupConfig
9
120
  });
10
121
 
11
122
  process.stdout.write(
12
123
  [
13
- 'Subagent Budget Guard statusLine bridge installed.',
14
- 'Recommended plugin config applied:',
124
+ 'Agent Guard statusLine bridge installed.',
125
+ 'Plugin config applied:',
15
126
  ` max_concurrent_subagents=${result.pluginConfigOptions.max_concurrent_subagents}`,
16
127
  ` max_subagent_tokens_per_session=${result.pluginConfigOptions.max_subagent_tokens_per_session}`,
17
128
  ` subagent_token_warning_threshold_percent=${result.pluginConfigOptions.subagent_token_warning_threshold_percent}`,
package/lib/guard.js CHANGED
@@ -17,8 +17,9 @@ import { fileURLToPath } from 'node:url';
17
17
 
18
18
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
19
 
20
- export const PLUGIN_NAME = 'subagent-budget-guard';
21
- export const PLUGIN_ID = 'subagent-budget-guard@subagent-budget-tools';
20
+ export const PLUGIN_NAME = 'agent-guard';
21
+ export const PLUGIN_ID = 'agent-guard@subagent-budget-tools';
22
+ export const LEGACY_PLUGIN_ID = 'subagent-budget-guard@subagent-budget-tools';
22
23
 
23
24
  export const DEFAULT_CONFIG = Object.freeze({
24
25
  max_concurrent_subagents: 0,
@@ -87,7 +88,9 @@ function readSettingsOptions(env) {
87
88
  try {
88
89
  const text = readFileSync(settingsPath, 'utf8');
89
90
  const settings = JSON.parse(text.replace(/^\uFEFF/, ''));
90
- const options = settings?.pluginConfigs?.[PLUGIN_ID]?.options;
91
+ const options =
92
+ settings?.pluginConfigs?.[PLUGIN_ID]?.options ||
93
+ settings?.pluginConfigs?.[LEGACY_PLUGIN_ID]?.options;
91
94
  return isPlainObject(options) ? options : {};
92
95
  } catch (error) {
93
96
  if (error.code === 'ENOENT') return {};
@@ -134,6 +137,12 @@ export function loadConfig(env = process.env) {
134
137
  return normalizeConfig(config);
135
138
  }
136
139
 
140
+ export function buildSetupConfig(overrides = {}) {
141
+ const config = { ...SETUP_CONFIG };
142
+ applyConfigValues(config, (key) => overrides[key]);
143
+ return normalizeConfig(config);
144
+ }
145
+
137
146
  export function getHomeDir(env = process.env) {
138
147
  return env.USERPROFILE || env.HOME || os.homedir();
139
148
  }
@@ -754,7 +763,7 @@ export function formatReport(report) {
754
763
  const { state, config, summary } = report;
755
764
  const fiveHour = state.rateLimits.fiveHour;
756
765
  const lines = [
757
- `Subagent Budget Guard report for ${report.sessionId}`,
766
+ `Agent Guard report for ${report.sessionId}`,
758
767
  `Enforcement: ${config.enforcement_enabled ? 'enabled' : 'disabled'}`,
759
768
  `Subagents: allowed ${state.subagents.allowed}, denied ${state.subagents.denied}, active ${state.subagents.active}, lifecycle starts ${state.subagents.lifecycleStarted}, lifecycle stops ${state.subagents.lifecycleStopped}`,
760
769
  `Verified usage: ${summary.verifiedTokenLabel}, ${state.subagents.totalToolUseCount} subagent tool calls, ${state.subagents.totalDurationMs} ms`,
@@ -770,7 +779,7 @@ export function formatReport(report) {
770
779
  );
771
780
  } else {
772
781
  lines.push(
773
- '5-hour latest: unavailable. Run /subagent-budget-guard:setup so the statusLine bridge can capture rate_limits.five_hour.used_percentage.'
782
+ '5-hour latest: unavailable. Run /agent-guard:init so the statusLine bridge can capture rate_limits.five_hour.used_percentage.'
774
783
  );
775
784
  }
776
785
 
@@ -816,26 +825,33 @@ function applySetupPluginConfig(
816
825
  settings.pluginConfigs = {};
817
826
  }
818
827
 
828
+ const legacyEntry = isPlainObject(settings.pluginConfigs[LEGACY_PLUGIN_ID])
829
+ ? settings.pluginConfigs[LEGACY_PLUGIN_ID]
830
+ : {};
819
831
  const currentEntry = isPlainObject(settings.pluginConfigs[pluginId])
820
832
  ? settings.pluginConfigs[pluginId]
821
- : {};
833
+ : legacyEntry;
822
834
  const currentOptions = isPlainObject(currentEntry.options)
823
835
  ? currentEntry.options
824
836
  : {};
825
837
  const nextOptions = { ...currentOptions };
838
+ const normalizedSetupConfig = buildSetupConfig(setupConfig);
826
839
 
827
840
  for (const key of REMOVED_CONFIG_KEYS) {
828
841
  delete nextOptions[key];
829
842
  }
830
843
 
831
844
  for (const key of CONFIG_KEYS) {
832
- nextOptions[key] = setupConfig[key];
845
+ nextOptions[key] = normalizedSetupConfig[key];
833
846
  }
834
847
 
835
848
  settings.pluginConfigs[pluginId] = {
836
849
  ...currentEntry,
837
850
  options: nextOptions
838
851
  };
852
+ if (pluginId !== LEGACY_PLUGIN_ID) {
853
+ delete settings.pluginConfigs[LEGACY_PLUGIN_ID];
854
+ }
839
855
 
840
856
  return nextOptions;
841
857
  }
@@ -843,7 +859,8 @@ function applySetupPluginConfig(
843
859
  export async function installStatusLineBridge({
844
860
  homeDir = getHomeDir(),
845
861
  pluginRoot = getPluginRoot(),
846
- pluginData = getDataDir()
862
+ pluginData = getDataDir(),
863
+ setupConfig = SETUP_CONFIG
847
864
  } = {}) {
848
865
  await mkdir(pluginData, { recursive: true });
849
866
  const { settingsPath, settings } = await ensureSettings(homeDir);
@@ -861,7 +878,7 @@ export async function installStatusLineBridge({
861
878
  padding: existing?.padding ?? previousStatusLine?.padding ?? 0,
862
879
  refreshInterval: existing?.refreshInterval ?? 5
863
880
  };
864
- const pluginConfigOptions = applySetupPluginConfig(settings);
881
+ const pluginConfigOptions = applySetupPluginConfig(settings, { setupConfig });
865
882
 
866
883
  settings.statusLine = nextStatusLine;
867
884
  await writeJsonAtomic(settingsPath, settings);
package/lib/verifier.js CHANGED
@@ -64,6 +64,27 @@ function assert(condition, message) {
64
64
  if (!condition) throw new Error(message);
65
65
  }
66
66
 
67
+ async function withIsolatedPluginEnv(env, root, fn) {
68
+ const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-data-'));
69
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-home-'));
70
+ const checkEnv = {
71
+ PATH: env.PATH,
72
+ Path: env.Path,
73
+ SystemRoot: env.SystemRoot,
74
+ USERPROFILE: homeDir,
75
+ HOME: homeDir,
76
+ CLAUDE_PLUGIN_DATA: dataDir,
77
+ CLAUDE_PLUGIN_ROOT: root
78
+ };
79
+
80
+ try {
81
+ return await fn(checkEnv, { dataDir, homeDir });
82
+ } finally {
83
+ await rm(dataDir, { recursive: true, force: true });
84
+ await rm(homeDir, { recursive: true, force: true });
85
+ }
86
+ }
87
+
67
88
  export async function runOfflineVerification({
68
89
  repoRoot = process.cwd(),
69
90
  env = process.env
@@ -83,14 +104,14 @@ export async function runOfflineVerification({
83
104
  const marketplace = await readJson(marketplacePath);
84
105
  assert(marketplace.name === 'subagent-budget-tools', 'marketplace name mismatch');
85
106
  assert(Array.isArray(marketplace.plugins), 'marketplace.plugins must be an array');
86
- const entry = marketplace.plugins.find((plugin) => plugin.name === 'subagent-budget-guard');
87
- assert(entry, 'subagent-budget-guard entry missing');
107
+ const entry = marketplace.plugins.find((plugin) => plugin.name === 'agent-guard');
108
+ assert(entry, 'agent-guard entry missing');
88
109
  assert(entry.source?.source === 'npm', 'marketplace source must use npm');
89
110
  assert(
90
111
  entry.source?.package === '@rex_koh/subagent-budget-guard',
91
112
  'marketplace npm package mismatch'
92
113
  );
93
- assert(entry.source?.version === '0.1.7', 'marketplace npm version mismatch');
114
+ assert(entry.source?.version === '0.2.0', 'marketplace npm version mismatch');
94
115
  return marketplacePath;
95
116
  });
96
117
  } else {
@@ -108,7 +129,7 @@ export async function runOfflineVerification({
108
129
  await withCheck(result, 'plugin-manifest-no-install-config', async () => {
109
130
  const manifestPath = path.join(root, '.claude-plugin', 'plugin.json');
110
131
  const manifest = await readJson(manifestPath);
111
- assert(manifest.name === 'subagent-budget-guard', 'plugin name mismatch');
132
+ assert(manifest.name === 'agent-guard', 'plugin name mismatch');
112
133
  assert(
113
134
  manifest.hooks === undefined,
114
135
  'manifest.hooks must be omitted for default hooks/hooks.json to avoid duplicate loading'
@@ -147,12 +168,16 @@ export async function runOfflineVerification({
147
168
  await withCheck(result, 'script-paths', async () => {
148
169
  const scripts = [
149
170
  'bin/hook.js',
171
+ 'bin/agent-guard.js',
150
172
  'bin/statusline.js',
151
173
  'bin/setup.js',
152
174
  'bin/report.js',
153
175
  'bin/verify.js',
154
176
  'lib/guard.js',
155
177
  'lib/verifier.js',
178
+ 'skills/init/SKILL.md',
179
+ 'skills/status/SKILL.md',
180
+ 'skills/doctor/SKILL.md',
156
181
  'skills/setup/SKILL.md',
157
182
  'skills/report/SKILL.md',
158
183
  'skills/verify/SKILL.md'
@@ -164,13 +189,7 @@ export async function runOfflineVerification({
164
189
  });
165
190
 
166
191
  await withCheck(result, 'pretool-agent-denies-default', async () => {
167
- const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
168
- try {
169
- const checkEnv = {
170
- ...env,
171
- CLAUDE_PLUGIN_DATA: dataDir,
172
- CLAUDE_PLUGIN_ROOT: root
173
- };
192
+ return withIsolatedPluginEnv(env, root, async (checkEnv) => {
174
193
  const output = await handlePreToolUseAgent(
175
194
  {
176
195
  session_id: 'offline-pretool',
@@ -185,19 +204,11 @@ export async function runOfflineVerification({
185
204
  'Agent launch was not denied by default'
186
205
  );
187
206
  return output.stdout.hookSpecificOutput.permissionDecisionReason;
188
- } finally {
189
- await rm(dataDir, { recursive: true, force: true });
190
- }
207
+ });
191
208
  });
192
209
 
193
210
  await withCheck(result, 'posttool-agent-records-verified-tokens', async () => {
194
- const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
195
- try {
196
- const checkEnv = {
197
- ...env,
198
- CLAUDE_PLUGIN_DATA: dataDir,
199
- CLAUDE_PLUGIN_ROOT: root
200
- };
211
+ return withIsolatedPluginEnv(env, root, async (checkEnv) => {
201
212
  await handlePostToolUseAgent(
202
213
  {
203
214
  session_id: 'offline-posttool',
@@ -217,18 +228,13 @@ export async function runOfflineVerification({
217
228
  const report = await buildReport('offline-posttool', checkEnv);
218
229
  assert(report.state.subagents.verifiedTokens === 101, 'verified token count mismatch');
219
230
  return report.summary.verifiedTokenLabel;
220
- } finally {
221
- await rm(dataDir, { recursive: true, force: true });
222
- }
231
+ });
223
232
  });
224
233
 
225
234
  await withCheck(result, 'statusline-budget-blocks', async () => {
226
- const dataDir = await mkdtemp(path.join(os.tmpdir(), 'sbg-verify-'));
227
- try {
235
+ return withIsolatedPluginEnv(env, root, async (baseEnv) => {
228
236
  const checkEnv = {
229
- ...env,
230
- CLAUDE_PLUGIN_DATA: dataDir,
231
- CLAUDE_PLUGIN_ROOT: root,
237
+ ...baseEnv,
232
238
  CLAUDE_PLUGIN_OPTION_session_five_hour_budget_percent: '3'
233
239
  };
234
240
  await updateRateLimitFromStatusLine(
@@ -255,9 +261,7 @@ export async function runOfflineVerification({
255
261
  );
256
262
  assert(output.stdout?.decision === 'block', 'prompt was not blocked');
257
263
  return output.stdout.reason;
258
- } finally {
259
- await rm(dataDir, { recursive: true, force: true });
260
- }
264
+ });
261
265
  });
262
266
 
263
267
  await withCheck(result, 'statusline-setup-wraps-existing-command', async () => {
@@ -408,12 +412,12 @@ export async function runLiveVerification({
408
412
  const list = await runCommand('claude', ['plugin', 'list'], { cwd: repoRoot });
409
413
  assert(list.code === 0, list.stderr || list.stdout || 'claude plugin list failed');
410
414
  assert(
411
- list.stdout.includes('subagent-budget-guard'),
412
- 'subagent-budget-guard is not installed'
415
+ list.stdout.includes('agent-guard'),
416
+ 'agent-guard is not installed'
413
417
  );
414
418
  assert(
415
- !/subagent-budget-guard@subagent-budget-tools[\s\S]*failed to load/i.test(list.stdout),
416
- 'subagent-budget-guard is installed but failed to load'
419
+ !/agent-guard@subagent-budget-tools[\s\S]*failed to load/i.test(list.stdout),
420
+ 'agent-guard is installed but failed to load'
417
421
  );
418
422
  return 'claude plugin list returned output';
419
423
  });
@@ -427,7 +431,7 @@ export async function runLiveVerification({
427
431
  typeof settings.statusLine?.command === 'string' &&
428
432
  settings.statusLine.command.includes('statusline.js') &&
429
433
  settings.statusLine.command.includes('--data'),
430
- 'statusLine bridge is not installed; run /subagent-budget-guard:setup'
434
+ 'statusLine bridge is not installed; run /agent-guard:init'
431
435
  );
432
436
  return settings.statusLine.command;
433
437
  });
@@ -438,7 +442,7 @@ export async function runLiveVerification({
438
442
 
439
443
  export function formatVerificationResult(result) {
440
444
  const lines = [
441
- `Subagent Budget Guard ${result.mode} verification`,
445
+ `Agent Guard ${result.mode} verification`,
442
446
  result.ok ? 'PASS' : 'FAIL'
443
447
  ];
444
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex_koh/subagent-budget-guard",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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",
@@ -32,6 +32,8 @@
32
32
  "LICENSE"
33
33
  ],
34
34
  "bin": {
35
+ "agent-guard": "bin/agent-guard.js",
36
+ "ag": "bin/agent-guard.js",
35
37
  "subagent-budget-guard-report": "bin/report.js",
36
38
  "subagent-budget-guard-setup": "bin/setup.js",
37
39
  "subagent-budget-guard-verify": "bin/verify.js"
@@ -43,7 +45,7 @@
43
45
  "test": "node --test test/*.test.js",
44
46
  "verify:offline": "node bin/verify.js --offline",
45
47
  "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"
48
+ "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 bin/agent-guard.js && node --check lib/guard.js && node --check lib/verifier.js"
47
49
  },
48
50
  "engines": {
49
51
  "node": ">=20"
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Verify Agent Guard installation and offline behavior.
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ # Doctor Agent Guard
7
+
8
+ For default offline verification, run:
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --offline
12
+ ```
13
+
14
+ For live local installation checks, run:
15
+
16
+ ```bash
17
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --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.
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: Initialize Agent Guard settings and the statusLine bridge.
3
+ ---
4
+
5
+ # Init Agent Guard
6
+
7
+ Ask the user whether to use recommended defaults or custom values.
8
+
9
+ Recommended defaults:
10
+
11
+ ```text
12
+ max_concurrent_subagents=1
13
+ max_subagent_tokens_per_session=100000
14
+ subagent_token_warning_threshold_percent=95
15
+ session_five_hour_budget_percent=25
16
+ absolute_five_hour_ceiling_percent=95
17
+ enforcement_enabled=true
18
+ ```
19
+
20
+ If they choose defaults, run:
21
+
22
+ ```bash
23
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" init --defaults
24
+ ```
25
+
26
+ If they choose custom values, ask for each value. Accept a blank answer as the default, then run:
27
+
28
+ ```bash
29
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" init \
30
+ --config max_concurrent_subagents=<value> \
31
+ --config max_subagent_tokens_per_session=<value> \
32
+ --config subagent_token_warning_threshold_percent=<value> \
33
+ --config session_five_hour_budget_percent=<value> \
34
+ --config absolute_five_hour_ceiling_percent=<value> \
35
+ --config enforcement_enabled=<true-or-false>
36
+ ```
37
+
38
+ Then tell the user to fully exit and reopen Claude Code, interact once so the statusLine bridge receives fresh session JSON, and run:
39
+
40
+ ```bash
41
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --live
42
+ ```
@@ -1,18 +1,20 @@
1
1
  ---
2
- description: Show the current Subagent Budget Guard session report with subagent counts, verified token totals, and 5-hour budget state.
2
+ description: Compatibility alias for /agent-guard:status.
3
3
  disable-model-invocation: true
4
4
  ---
5
5
 
6
- # Report Subagent Budget Guard Usage
6
+ # Report Agent Guard Usage
7
+
8
+ Prefer `/agent-guard:status`.
7
9
 
8
10
  Run this command:
9
11
 
10
12
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/bin/report.js"
13
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" status
12
14
  ```
13
15
 
14
16
  If the user asks for machine-readable output, run:
15
17
 
16
18
  ```bash
17
- node "${CLAUDE_PLUGIN_ROOT}/bin/report.js" --json
19
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" status --json
18
20
  ```
@@ -1,17 +1,18 @@
1
1
  ---
2
- description: Install or refresh the Subagent Budget Guard statusLine bridge and apply the recommended plugin config.
3
- disable-model-invocation: true
2
+ description: Compatibility alias for /agent-guard:init.
4
3
  ---
5
4
 
6
- # Setup Subagent Budget Guard
5
+ # Setup Agent Guard
7
6
 
8
- Run this command:
7
+ Prefer `/agent-guard:init`.
8
+
9
+ Ask the user whether to use the recommended defaults or customize the values. If they choose defaults, run:
9
10
 
10
11
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/bin/setup.js"
12
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" init --defaults
12
13
  ```
13
14
 
14
- This applies the recommended config in Claude settings:
15
+ If they choose custom values, ask for each value below. The value in parentheses is the default; accept a blank answer as the default.
15
16
 
16
17
  ```text
17
18
  max_concurrent_subagents=1
@@ -22,10 +23,22 @@ absolute_five_hour_ceiling_percent=95
22
23
  enforcement_enabled=true
23
24
  ```
24
25
 
26
+ Then run setup with the chosen values:
27
+
28
+ ```bash
29
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" init \
30
+ --config max_concurrent_subagents=<value> \
31
+ --config max_subagent_tokens_per_session=<value> \
32
+ --config subagent_token_warning_threshold_percent=<value> \
33
+ --config session_five_hour_budget_percent=<value> \
34
+ --config absolute_five_hour_ceiling_percent=<value> \
35
+ --config enforcement_enabled=<true-or-false>
36
+ ```
37
+
25
38
  Then tell the user to fully exit and reopen Claude Code, interact once so the statusLine bridge receives fresh session JSON, and run:
26
39
 
27
40
  ```bash
28
- node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
41
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --live
29
42
  ```
30
43
 
31
44
  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,18 @@
1
+ ---
2
+ description: Show the current Agent Guard report.
3
+ disable-model-invocation: true
4
+ ---
5
+
6
+ # Status Agent Guard
7
+
8
+ Run:
9
+
10
+ ```bash
11
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" status
12
+ ```
13
+
14
+ If the user asks for machine-readable output, run:
15
+
16
+ ```bash
17
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" status --json
18
+ ```
@@ -1,20 +1,22 @@
1
1
  ---
2
- description: Verify the Subagent Budget Guard plugin without spending Claude quota, or run live local installation checks.
2
+ description: Compatibility alias for /agent-guard:doctor.
3
3
  disable-model-invocation: true
4
4
  ---
5
5
 
6
- # Verify Subagent Budget Guard
6
+ # Verify Agent Guard
7
+
8
+ Prefer `/agent-guard:doctor`.
7
9
 
8
10
  For default offline verification, run:
9
11
 
10
12
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --offline
13
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --offline
12
14
  ```
13
15
 
14
16
  For live local installation checks, run:
15
17
 
16
18
  ```bash
17
- node "${CLAUDE_PLUGIN_ROOT}/bin/verify.js" --live
19
+ node "${CLAUDE_PLUGIN_ROOT}/bin/agent-guard.js" doctor --live
18
20
  ```
19
21
 
20
22
  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.