claude-usage-limits 1.19.0 → 1.24.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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +220 -2
- package/bin/cli.js +16 -0
- package/commands/defer.md +47 -0
- package/commands/usage-mode.md +64 -0
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +196 -19
- package/skills/usage-limits/references/tactics.md +40 -9
- package/skills/usage-limits/scripts/agy-hook.js +175 -0
- package/skills/usage-limits/scripts/brief.js +406 -46
- package/skills/usage-limits/scripts/ceiling.js +191 -0
- package/skills/usage-limits/scripts/codex-lowpower.js +95 -4
- package/skills/usage-limits/scripts/codex.js +87 -6
- package/skills/usage-limits/scripts/defer.js +318 -0
- package/skills/usage-limits/scripts/drift.js +254 -0
- package/skills/usage-limits/scripts/feed.js +23 -1
- package/skills/usage-limits/scripts/host.js +23 -3
- package/skills/usage-limits/scripts/install-antigravity.js +215 -0
- package/skills/usage-limits/scripts/install-codex-hook.js +22 -2
- package/skills/usage-limits/scripts/lowpower.js +48 -0
- package/skills/usage-limits/scripts/mode.js +1637 -0
- package/skills/usage-limits/scripts/net.js +179 -0
- package/skills/usage-limits/scripts/pulse.js +254 -17
- package/skills/usage-limits/scripts/reading.js +12 -3
- package/skills/usage-limits/scripts/relay.js +266 -2
- package/skills/usage-limits/scripts/sessionend.js +8 -0
- package/skills/usage-limits/scripts/stop.js +145 -1
- package/skills/usage-limits/scripts/usage.js +244 -17
- package/skills/usage-limits/scripts/view.js +4 -0
- package/skills/usage-limits/scripts/voice.js +10 -1
- package/skills/usage-limits/scripts/wake.js +210 -30
|
@@ -275,6 +275,137 @@ The user can also ask any time with `/usage-limits:session`, or run
|
|
|
275
275
|
`--sessions` for the history of recent sessions on this machine.
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
## Budget modes
|
|
279
|
+
|
|
280
|
+
How hard this plugin leans, and what it costs to say it. Four modes, set by the
|
|
281
|
+
user and read by every hook:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
node scripts/mode.js # which one, where it came from, what it changes
|
|
285
|
+
node scripts/mode.js max # set it
|
|
286
|
+
node scripts/mode.js --list # all four, and the aliases
|
|
287
|
+
node scripts/mode.js --explain high # one mode's full record
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
| mode | one line |
|
|
291
|
+
| --- | --- |
|
|
292
|
+
| `max` | fewest tokens that can still finish the job: one terse line, readings every ten minutes, silent when nothing a decision depends on has moved |
|
|
293
|
+
| `high` | full capability, re-measured every two minutes mid-turn, and said only when the answer changes |
|
|
294
|
+
| `standard` | what the plugin does today, unchanged. The default |
|
|
295
|
+
| `off` | nothing injected at all; every hook returns before reading anything, including the end-of-reply tally |
|
|
296
|
+
|
|
297
|
+
Aliases: `ultra`, `ultra-efficient`, `maxtoken`, `maxefficient` map to `max`;
|
|
298
|
+
`smart`, `high-efficient` to `high`; `efficient`, `token-efficient`, `default`,
|
|
299
|
+
`on` to `standard`; `none`, `quiet`, `silent`, `ignore` to `off`.
|
|
300
|
+
|
|
301
|
+
**`normal` is not an alias for either.** To some people it means "the plugin
|
|
302
|
+
working as usual" (`standard`) and to others "the plugin stays out of the way"
|
|
303
|
+
(`off`) - opposite instructions, so a silent guess is wrong half the time. If
|
|
304
|
+
the user says "normal", ask which they meant and say what each one does. The
|
|
305
|
+
script answers the same way.
|
|
306
|
+
|
|
307
|
+
`auto` picks from how full the binding window is - standard under 50 per cent
|
|
308
|
+
used, high from 50 to 79, max at 80 or above - and always reports itself as
|
|
309
|
+
what it resolved to (`auto -> max`). It reads the snapshot already on disk
|
|
310
|
+
rather than scanning, because the mode has to be settled before anything
|
|
311
|
+
expensive runs, and with no readable window it stays on `standard` rather than
|
|
312
|
+
guessing. It never picks `off`: turning the plugin off is a decision a person
|
|
313
|
+
makes.
|
|
314
|
+
|
|
315
|
+
`off` means off, including at 100 per cent. That is deliberate. If the user
|
|
316
|
+
wants one line at the wall and nothing else, `node scripts/mode.js off --guard 95`
|
|
317
|
+
is it.
|
|
318
|
+
|
|
319
|
+
Two rules hold in every mode, and they are not negotiable:
|
|
320
|
+
|
|
321
|
+
- **A mode never lowers the quality of the work.** When things are tight you
|
|
322
|
+
change the ORDER of the work, never the amount or the quality. The savings
|
|
323
|
+
come from ceremony - speculative reads, re-reads, preamble, subagents nobody
|
|
324
|
+
needed, workflows that cost more context than they save.
|
|
325
|
+
- **The modes govern the agent plane only**, and never write `settings.json`.
|
|
326
|
+
|
|
327
|
+
### Two planes, and the conversation between them
|
|
328
|
+
|
|
329
|
+
The **user plane** is `settings.json` (`model`, `effortLevel`), the `/effort`
|
|
330
|
+
and `/model` pickers, and `lowpower.js`. It is the user saying what they want
|
|
331
|
+
for themselves. It is theirs.
|
|
332
|
+
|
|
333
|
+
The **agent plane** is the tier actually running this turn, and the tier of
|
|
334
|
+
everything this turn spawns. That is what costs money.
|
|
335
|
+
|
|
336
|
+
What is actually yours to move, stated exactly, because a claim beyond this
|
|
337
|
+
would be a lie the user cannot check:
|
|
338
|
+
|
|
339
|
+
- **The model on an `Agent` call is yours.** `model: "sonnet" | "opus" |
|
|
340
|
+
"haiku" | "fable"` on any Agent call, and it overrides both the agent
|
|
341
|
+
definition and the configured default. It is ignored for `subagent_type:
|
|
342
|
+
"fork"`, which always inherits the parent model.
|
|
343
|
+
- **Model and effort inside a `Workflow` script are yours**, per `agent()`
|
|
344
|
+
call: `opts.model` and `opts.effort` (`low` | `medium` | `high` | `xhigh` |
|
|
345
|
+
`max`). The host's own advice is to omit `opts.model` unless you are
|
|
346
|
+
confident, and to use `opts.effort: 'low'` for mechanical stages - so lean on
|
|
347
|
+
effort rather than model when sizing what you spawn.
|
|
348
|
+
- **Your own model and effort are NOT yours to change mid-session.** Nothing a
|
|
349
|
+
hook can emit changes them: there is no such field anywhere in the hook
|
|
350
|
+
output contract, and `PreModelSwitch` can only veto a switch someone else
|
|
351
|
+
started. `/model` and `/effort` are typed by a person and
|
|
352
|
+
`CLAUDE_CODE_EFFORT_LEVEL` is read at launch. So say the exact command in one
|
|
353
|
+
line and let the user run it. Never imply you changed it yourself.
|
|
354
|
+
- `xhigh` and `max` are refused outright when thinking is disabled, so never
|
|
355
|
+
point at either without that being true.
|
|
356
|
+
|
|
357
|
+
The two planes talk to each other, in both directions, through the
|
|
358
|
+
conversation. The whole rule in one line:
|
|
359
|
+
|
|
360
|
+
**You may RECOMMEND a user-plane change. You may MAKE one when asked. You may
|
|
361
|
+
never make one unasked.**
|
|
362
|
+
|
|
363
|
+
Recommending, when you do it:
|
|
364
|
+
|
|
365
|
+
- **Evidence or silence.** Cite a measurement - what this account has actually
|
|
366
|
+
measured, from `usage.js --recommend`, the fit line in the budget briefing,
|
|
367
|
+
or `scripts/drift.js`. "Recommended" with no number is nagging.
|
|
368
|
+
- **At most one per session**, and none at all in `off`. The plugin enforces
|
|
369
|
+
this; do not route around it.
|
|
370
|
+
- **A declined recommendation is not raised again.** When the user says no,
|
|
371
|
+
record it - `node scripts/mode.js --decline` - and it is never volunteered
|
|
372
|
+
again, in this session or any later one. `--advice` shows what is pending.
|
|
373
|
+
- **Name the exact command, the plane it changes, and when it takes effect.**
|
|
374
|
+
|
|
375
|
+
When the user asks you to change something, or says **"change it back"**: read
|
|
376
|
+
`node scripts/mode.js --history` first, say out loud what you are reverting and
|
|
377
|
+
to what, then do it. Never infer a target that is not in the log. `node
|
|
378
|
+
scripts/mode.js undo` reverses the last change the plugin owns and names it
|
|
379
|
+
first; for a change to the user's own settings it names the command and leaves
|
|
380
|
+
the file alone, because that plane is not the plugin's to write.
|
|
381
|
+
|
|
382
|
+
The surprise to head off, every time: **a user-plane change does not move the
|
|
383
|
+
session already running.** If the user says "turn low power on" and you do it,
|
|
384
|
+
nothing about this session changes - `lowpower.js` says so itself ("Applies to
|
|
385
|
+
new sessions. For the session you are in, run /effort X"). Relay that in your
|
|
386
|
+
own words whenever you make one, and the same in reverse: putting the baseline
|
|
387
|
+
back does not restore this session's tier either.
|
|
388
|
+
|
|
389
|
+
### Bounds the user can set
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
node scripts/mode.js --floor sonnet/medium # never point below this, even in max
|
|
393
|
+
node scripts/mode.js --ceiling opus/xhigh # nor above it, even on the hard part
|
|
394
|
+
node scripts/mode.js --pin # do not self-switch at all: report only
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Under `--pin` the plugin reports the gap between the baseline and what is
|
|
398
|
+
running and suggests nothing. Respect it in your own words too: report, do not
|
|
399
|
+
switch.
|
|
400
|
+
|
|
401
|
+
### Stepping back up
|
|
402
|
+
|
|
403
|
+
The efficient modes are two-directional, and that matters more than it sounds.
|
|
404
|
+
Having dropped to a cheap tier for a mechanical stretch, say so when the work
|
|
405
|
+
turns hard again and point back at the baseline. A one-way ratchet down is how
|
|
406
|
+
a long session quietly degrades, which is the thing the quality rule forbids.
|
|
407
|
+
|
|
408
|
+
|
|
278
409
|
## 4. Low power
|
|
279
410
|
|
|
280
411
|
Two halves, and the second one is the half that actually binds.
|
|
@@ -404,41 +535,48 @@ waste at 10 per cent used exactly as much as at 80; the only difference is that
|
|
|
404
535
|
at 80 somebody notices.
|
|
405
536
|
|
|
406
537
|
It cannot judge how hard your work is. You can. So when you see it, look at what
|
|
407
|
-
is actually in front of you and
|
|
408
|
-
|
|
538
|
+
is actually in front of you and reach for one of three levers, saying which in
|
|
539
|
+
one line. Two of them are yours to pull; the first is not.
|
|
409
540
|
|
|
410
541
|
1. **Drop the effort** for that stretch - a rename, a docs pass, running tests,
|
|
411
|
-
applying a fix you have already worked out.
|
|
412
|
-
|
|
413
|
-
|
|
542
|
+
applying a fix you have already worked out. `/effort` and `settings.json` are
|
|
543
|
+
the user's own baseline, so this one you **offer**: name the command, say why
|
|
544
|
+
the stretch does not need the tier, and leave the pulling of it to them.
|
|
545
|
+
2. **Hand the stretch to a cheaper model.** Your own model is the user's setting
|
|
546
|
+
too, so say that in one line rather than switching - but the model on
|
|
547
|
+
anything you SPAWN is entirely yours: an Agent call takes a model, and a
|
|
548
|
+
Workflow's `agent()` takes a model and an effort. Size those to the stage.
|
|
414
549
|
3. **Do less of it at this setting** - a fan-out multiplies the setting across
|
|
415
|
-
every agent, so six agents at ultra is six ultra turns, not one.
|
|
550
|
+
every agent, so six agents at ultra is six ultra turns, not one. Yours.
|
|
416
551
|
|
|
417
552
|
Put it back when the work gets hard again. The question is asked once per
|
|
418
553
|
setting per session, not every prompt, and never at all unless there is a real
|
|
419
554
|
measured comparison behind it - a ratio from a price list would have you drop
|
|
420
555
|
effort on a hunch and call it evidence.
|
|
421
556
|
|
|
422
|
-
**This is
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
557
|
+
**This is worth raising early, not only at the wall.** Say it whenever the
|
|
558
|
+
current setting is dearer than the work in front of you needs, long before any
|
|
559
|
+
window is tight: a mechanical rename, a docs pass or a mass find-and-replace
|
|
560
|
+
does not need the model and effort a hard design decision does. An agent that
|
|
561
|
+
only ever reads this as a wall notice runs every trivial turn at the top setting
|
|
562
|
+
and then wonders where the window went.
|
|
563
|
+
|
|
564
|
+
**And "offer it" is one line, not a stop.** Naming the lever and carrying on is
|
|
565
|
+
the whole action. Waiting for an answer before doing the work would cost more
|
|
566
|
+
than the tier ever did.
|
|
430
567
|
|
|
431
568
|
The budget line does this arithmetic for you. Once the binding window is half
|
|
432
569
|
gone it says which lever applies, and at the wall it says outright that you are
|
|
433
570
|
not out of budget and must not stop as though you were. When it does, the
|
|
434
|
-
sequence is:
|
|
435
|
-
the whole request at full quality.
|
|
571
|
+
sequence is: say in one line which command frees it and why, and carry on with
|
|
572
|
+
the whole request at full quality. A per-model weekly is not the account's
|
|
573
|
+
budget, and treating it as one is the error.
|
|
436
574
|
|
|
437
575
|
This is written down because it was got wrong. A session ended with the Fable
|
|
438
576
|
weekly at 89 per cent and the line saying the budget was nearly gone - while the
|
|
439
577
|
5-hour sat at 46 and every other model on the account was untouched. One command
|
|
440
|
-
would have carried it on
|
|
441
|
-
reason that sounded like one.
|
|
578
|
+
would have carried it on, and naming that command costs a line. Stopping there
|
|
579
|
+
was not caution; it was quitting with a reason that sounded like one.
|
|
442
580
|
|
|
443
581
|
Only when the switch is genuinely unavailable - no other window has room, or the
|
|
444
582
|
user has ruled it out - does the checkpoint below apply.
|
|
@@ -482,6 +620,32 @@ which files are mid-change, what must be verified before anything is built on
|
|
|
482
620
|
it. It is delivered as a prompt, so an instruction beats a summary. If nothing
|
|
483
621
|
is written, the relay falls back to the outstanding todo list, which is worse.
|
|
484
622
|
|
|
623
|
+
**It arms at the end of a reply, not in the middle of one.** Crossing the
|
|
624
|
+
threshold no longer schedules anything by itself: the relay waits for the reply
|
|
625
|
+
to finish, so what it carries is work that reached a boundary rather than a
|
|
626
|
+
snapshot of an interruption. Past the backstop (95 per cent by default) it stops
|
|
627
|
+
waiting and arms anyway, because a completion that never comes - the limit cut
|
|
628
|
+
the reply off mid-sentence - is a relay that was never armed at all. That is
|
|
629
|
+
what "waiting for this reply to finish before arming" means when the budget line
|
|
630
|
+
says it, and it is not an error. `relay armon threshold` restores the old
|
|
631
|
+
behaviour.
|
|
632
|
+
|
|
633
|
+
**Being offline is not failing.** Before it launches anything the wake checks it
|
|
634
|
+
can actually reach the API, and a machine that cannot is held, not spent: it
|
|
635
|
+
retries on a backoff for hours rather than burning the relay on a launch that
|
|
636
|
+
could not work. This exists because it happened - a relay woke on time, said
|
|
637
|
+
`SSL certificate hostname mismatch` 1.5 seconds later, recorded a failure and
|
|
638
|
+
deleted itself, and a night's work was lost to a router being off. A failed
|
|
639
|
+
launch is also classified now: the network and a busy service buy another
|
|
640
|
+
window, an invalid key or a missing CLI does not, because retrying those would
|
|
641
|
+
waste the window and saying "retrying" would be a lie.
|
|
642
|
+
|
|
643
|
+
Run `relay doctor` before trusting it with anything. It checks the things that
|
|
644
|
+
fail silently hours later - a CLI that moved, a permission mode nobody set, a
|
|
645
|
+
machine whose power plan forbids wake timers, a scheduled task this account
|
|
646
|
+
cannot register, no continuation written, no network - and says which would
|
|
647
|
+
bite. Every resumed run's full output is kept: `relay log --run`.
|
|
648
|
+
|
|
485
649
|
The relay is off unless the user turned it on, and it only arms while there is
|
|
486
650
|
an unfinished todo list or an approved plan to carry. Do not turn it on for
|
|
487
651
|
them, and do not promise behaviour it does not have:
|
|
@@ -490,6 +654,8 @@ them, and do not promise behaviour it does not have:
|
|
|
490
654
|
input to a shell on purpose; the relay uses it only to tell whether somebody
|
|
491
655
|
is at the keyboard, and to show a banner.
|
|
492
656
|
- In `notify` mode — the default — it raises a notification and starts nothing.
|
|
657
|
+
- The resumed run opens in a window you can see. `relay show off` hides it; the
|
|
658
|
+
output is kept either way.
|
|
493
659
|
- In `resume` mode it runs the CLI itself. A headless resume does **not**
|
|
494
660
|
inherit the session's permission mode, so unless one was set the resumed run
|
|
495
661
|
will sit waiting for an approval nobody is there to give.
|
|
@@ -540,6 +706,14 @@ of it.
|
|
|
540
706
|
|
|
541
707
|
## Running under Codex
|
|
542
708
|
|
|
709
|
+
One difference that matters for the budget modes. Every entry in Codex's
|
|
710
|
+
`hooks.json` carries an `additionalContextLimit` - a per-hook cap on how much
|
|
711
|
+
context a hook may inject, which Claude Code has no equivalent of. A long
|
|
712
|
+
briefing can therefore be truncated there without a word. If a line looks cut
|
|
713
|
+
off under Codex, that is why: `node scripts/mode.js max` keeps it to one line,
|
|
714
|
+
and `high` is the verbose one.
|
|
715
|
+
|
|
716
|
+
|
|
543
717
|
Everything above works the same. The numbers come from a different place and
|
|
544
718
|
one thing about how they arrive is different, and both are worth knowing.
|
|
545
719
|
|
|
@@ -654,6 +828,7 @@ stop.
|
|
|
654
828
|
| `scripts/host.js` | Works out which agent this is running inside, so one host's percentages are never reported against the other's turns. |
|
|
655
829
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
656
830
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
831
|
+
| `scripts/mode.js` | The budget mode: no arguments to report it, `max`/`high`/`standard`/`off` to set it, `auto`, `off --guard 95`, `--list`, `--explain <name>`, `--floor`/`--ceiling`/`--pin`, `--baseline`, `--advice`/`--no-advice`, `--history`, `undo`, `--ledger`. Reads settings.json and never writes it. |
|
|
657
832
|
| `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. Claude Code only. |
|
|
658
833
|
| `scripts/recommend.js` | The chooser behind `usage.js --recommend`: posture, then the effort and model commands for each lever. Not meant to be called by hand. |
|
|
659
834
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
@@ -661,8 +836,10 @@ stop.
|
|
|
661
836
|
| `scripts/statusline.js` | `status`, `on`, `off`. Puts the bars under the prompt and restores what was there. |
|
|
662
837
|
| `scripts/feed.js` | The status line command Claude Code runs. Not meant to be called by hand. |
|
|
663
838
|
| `scripts/live.js` | The usage reading itself, taken the way Claude Code takes it for `/usage`, kept in `usage-limits-live.json` where `collect()` prefers it when newer than the cache. |
|
|
839
|
+
| `scripts/drift.js` | How wrong the reading was: each correction written down against the one it replaced. Run it with no arguments for the median and worst gap measured so far, `--json` for the fields. Answer "how far behind does this plugin actually run" from here rather than from memory. |
|
|
664
840
|
| `scripts/view.js`, `scripts/bars.js`, `scripts/activity.js` | The display model, the drawing in Claude's colours, and the working/idle marks the hooks leave for the panel. Not meant to be called by hand. |
|
|
665
|
-
| `scripts/relay.js` | The relay: `status`, `on`/`off`, `at N`, `grace N`, `mode notify\|resume`, `permission MODE`, `thinking off\|resume\|always`, `note "<text>"`, `cancel`, `log`. |
|
|
841
|
+
| `scripts/relay.js` | The relay: `status`, `on`/`off`, `at N`, `grace N`, `mode notify\|resume`, `permission MODE`, `thinking off\|resume\|always`, `armon threshold\|completion`, `backstop N`, `show on\|off`, `onfailure rearm\|stop`, `rearms N`, `offline N`, `doctor`, `note "<text>"`, `cancel`, `log [--run]`. |
|
|
842
|
+
| `scripts/net.js` | Can this machine reach the API, and was a failed run the network's fault. Three probes, TLS-interception detection, and the backoff the offline retries use. |
|
|
666
843
|
| `scripts/wake.js` | What the scheduler runs after the reset: re-checks the meter, then notifies or resumes. Never called by hand. |
|
|
667
844
|
| `scripts/voice.js` | The local writing profile: `show`, `card`, `set "<instruction>"`, `clear`, `off`/`on`, `forget`. |
|
|
668
845
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
@@ -87,15 +87,46 @@ reason, if none of them are in use.
|
|
|
87
87
|
|
|
88
88
|
Cache reads are ten times cheaper than fresh input, and the cache matches on
|
|
89
89
|
an exact prefix. Any byte that changes early invalidates everything after it.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- switching models
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
the
|
|
90
|
+
What actually invalidates it mid-session:
|
|
91
|
+
|
|
92
|
+
- **switching models** - each model has its own cache, so the next request
|
|
93
|
+
re-reads the whole conversation even though the content is identical
|
|
94
|
+
- **changing the effort level** - on most models each effort level has its own
|
|
95
|
+
cache too. This one matters here because dropping effort is the saving this
|
|
96
|
+
plugin recommends most often. It is still worth doing, but it is not free
|
|
97
|
+
mid-session: on a large context the one-off rebuild can cost more than a few
|
|
98
|
+
cheaper turns save. Choose effort at the START of a session where you can
|
|
99
|
+
- **turning on fast mode** - it adds a request header that is part of the cache key
|
|
100
|
+
- **connecting or disconnecting an MCP server, but only when its tools sit in
|
|
101
|
+
the prefix.** With tool search - the default on supported models - a server
|
|
102
|
+
connecting, disconnecting or changing its tool list only appends, and the
|
|
103
|
+
cached prefix survives
|
|
104
|
+
- **enabling or disabling a plugin that provides MCP servers**, by the same
|
|
105
|
+
rule. A plugin ships skills, commands, agents and hooks by appending them,
|
|
106
|
+
and those never invalidate anything
|
|
107
|
+
- **adding or removing a bare tool-name deny rule** (`Bash`, `WebFetch`), which
|
|
108
|
+
takes the tool out of the system prompt. Scoped rules like `Bash(rm *)` do not
|
|
109
|
+
- **compacting**, by design, and **upgrading Claude Code**
|
|
110
|
+
|
|
111
|
+
What does NOT invalidate it, despite being widely believed to:
|
|
112
|
+
|
|
113
|
+
- **editing `CLAUDE.md` mid-session.** It is read once at session start and held
|
|
114
|
+
in memory. The edit does not invalidate the cache - and it also does not
|
|
115
|
+
apply, until `/clear`, `/compact` or a restart
|
|
116
|
+
- editing files in the repository, changing permission mode, changing output
|
|
117
|
+
style, invoking a skill or command, and `/recap`
|
|
118
|
+
|
|
119
|
+
None of the invalidating ones are forbidden. Just do them at a session boundary
|
|
120
|
+
instead of in the middle of a long run.
|
|
121
|
+
|
|
122
|
+
**`/rewind` rather than `/compact`** when abandoning a path: it truncates back to
|
|
123
|
+
a prefix that is already cached, where compaction builds a new one and pays a
|
|
124
|
+
summarisation call to do it.
|
|
125
|
+
|
|
126
|
+
**Cache scope is one machine and one directory.** Parallel sessions in the same
|
|
127
|
+
directory read each other cache; different directories do not - and that
|
|
128
|
+
includes two worktrees of the same repository, which is a real and unobvious
|
|
129
|
+
cost of isolating agents that way.
|
|
99
130
|
|
|
100
131
|
### 6. Batch tool calls
|
|
101
132
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The Antigravity entry point.
|
|
5
|
+
//
|
|
6
|
+
// Antigravity (and the `agy` CLI behind it) has a lifecycle-hook system that is
|
|
7
|
+
// close to Claude Code's in spirit and different from it in every detail that
|
|
8
|
+
// matters to a script:
|
|
9
|
+
//
|
|
10
|
+
// - Events are PreInvocation, PostInvocation, PreToolUse, PostToolUse and
|
|
11
|
+
// Stop. PreInvocation is the one that corresponds to UserPromptSubmit.
|
|
12
|
+
// - Every payload is protojson, so the keys are camelCase: conversationId,
|
|
13
|
+
// workspacePaths, transcriptPath, modelName, stepIdx, invocationNum.
|
|
14
|
+
// - Output is JSON on stdout, and each event has its own shape. Plain text
|
|
15
|
+
// on stdout is not context; it is a parse failure, and a parse failure is
|
|
16
|
+
// silent. That is the whole reason this file exists rather than pointing
|
|
17
|
+
// Antigravity at brief.js and hoping.
|
|
18
|
+
// - PreToolUse takes a decision of allow, deny, ask or force_ask.
|
|
19
|
+
//
|
|
20
|
+
// The contract is documented on the machine itself, in the built-in
|
|
21
|
+
// agy-customizations skill at
|
|
22
|
+
// ~/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/hooks.md,
|
|
23
|
+
// which is where these shapes were read from rather than guessed.
|
|
24
|
+
//
|
|
25
|
+
// node agy-hook.js --event PreInvocation
|
|
26
|
+
// node agy-hook.js --event PreToolUse
|
|
27
|
+
//
|
|
28
|
+
// Nothing here ever exits non-zero. Antigravity runs hooks synchronously and
|
|
29
|
+
// they block the agent loop, so a hook that fails is a hook that stops the
|
|
30
|
+
// user's work, and no budget figure is worth that.
|
|
31
|
+
|
|
32
|
+
const usage = require('./usage.js');
|
|
33
|
+
const host = require('./host.js');
|
|
34
|
+
const mode = require('./mode.js');
|
|
35
|
+
const ceiling = require('./ceiling.js');
|
|
36
|
+
const brief = require('./brief.js');
|
|
37
|
+
|
|
38
|
+
const EVENTS = ['PreInvocation', 'PostInvocation', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
39
|
+
|
|
40
|
+
function eventFrom(argv, input) {
|
|
41
|
+
const args = argv || [];
|
|
42
|
+
const at = args.indexOf('--event');
|
|
43
|
+
if (at !== -1 && EVENTS.includes(args[at + 1])) return args[at + 1];
|
|
44
|
+
// Antigravity does not name the event in the payload, so the shape is the
|
|
45
|
+
// only other evidence. toolCall is only ever present on the two tool events,
|
|
46
|
+
// and only PreToolUse can act on one.
|
|
47
|
+
if (input && input.toolCall) return 'PreToolUse';
|
|
48
|
+
if (input && input.terminationReason !== undefined) return 'Stop';
|
|
49
|
+
if (input && input.invocationNum !== undefined) return 'PreInvocation';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readInput() {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (process.stdin.isTTY) return resolve(null);
|
|
56
|
+
let raw = '';
|
|
57
|
+
let settled = false;
|
|
58
|
+
const done = () => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
try {
|
|
62
|
+
resolve(raw ? JSON.parse(raw) : null);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const timer = setTimeout(done, 500);
|
|
68
|
+
if (timer.unref) timer.unref();
|
|
69
|
+
process.stdin.setEncoding('utf8');
|
|
70
|
+
process.stdin.on('data', (chunk) => {
|
|
71
|
+
raw += chunk;
|
|
72
|
+
});
|
|
73
|
+
process.stdin.on('end', done);
|
|
74
|
+
process.stdin.on('error', done);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The tool name, as Antigravity spells it.
|
|
79
|
+
//
|
|
80
|
+
// Tool names there are the step type lowercased with the CORTEX_STEP_TYPE_
|
|
81
|
+
// prefix removed, so they are snake_case: run_command, view_file, browser_*.
|
|
82
|
+
// ceiling.js matches both spellings, so nothing has to be translated here.
|
|
83
|
+
function toolNameOf(input) {
|
|
84
|
+
if (!input || !input.toolCall) return '';
|
|
85
|
+
return String(input.toolCall.name || '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The cheapest percentage worth enforcing against.
|
|
89
|
+
//
|
|
90
|
+
// Antigravity does not publish remaining quota anywhere this can read - see
|
|
91
|
+
// collectGemini in usage.js, which says so rather than inventing a number - so
|
|
92
|
+
// on a Gemini host this is usually null and the ceiling never fires. It is
|
|
93
|
+
// still wired, because the same hook file runs when USAGE_LIMITS_HOST names a
|
|
94
|
+
// host that DOES have a meter, and because a ceiling that silently does
|
|
95
|
+
// nothing on the day Antigravity starts publishing one would be worse.
|
|
96
|
+
function percentNow(now) {
|
|
97
|
+
let worst = null;
|
|
98
|
+
try {
|
|
99
|
+
const snapshot = usage.collect(now);
|
|
100
|
+
const utilization = snapshot && snapshot.utilization;
|
|
101
|
+
if (utilization && typeof utilization === 'object') {
|
|
102
|
+
for (const key of Object.keys(utilization)) {
|
|
103
|
+
const window = utilization[key];
|
|
104
|
+
if (!window || typeof window !== 'object') continue;
|
|
105
|
+
const value = Number(window.utilization);
|
|
106
|
+
if (Number.isFinite(value) && (worst === null || value > worst)) worst = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// No reading is a reason not to enforce, never a reason to throw.
|
|
111
|
+
}
|
|
112
|
+
return worst;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function run(now, input, argv) {
|
|
116
|
+
const event = eventFrom(argv, input);
|
|
117
|
+
if (!event) return {};
|
|
118
|
+
|
|
119
|
+
usage.setHost(host.GEMINI);
|
|
120
|
+
const sessionId = input && input.conversationId ? String(input.conversationId) : null;
|
|
121
|
+
const budget = mode.forSession({ sessionId });
|
|
122
|
+
if (budget.policy.briefStyle === 'none') return {};
|
|
123
|
+
|
|
124
|
+
if (event === 'PreToolUse') {
|
|
125
|
+
const tool = toolNameOf(input);
|
|
126
|
+
if (!ceiling.isMultiplier(tool)) return {};
|
|
127
|
+
const at = ceiling.assess({ percent: percentNow(now), state: budget.state, env: process.env });
|
|
128
|
+
const call = ceiling.verdict(at, tool);
|
|
129
|
+
if (call.decision !== 'deny') return {};
|
|
130
|
+
return { decision: 'deny', reason: call.reason };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event === 'PreInvocation') {
|
|
134
|
+
// The one place a budget line can reach the model. Antigravity takes it as
|
|
135
|
+
// an injected step rather than as stdout text; an ephemeralMessage is a
|
|
136
|
+
// transient system message, which is exactly what a per-turn figure is.
|
|
137
|
+
let text = '';
|
|
138
|
+
try {
|
|
139
|
+
text = await brief.run(now, input);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
text = '';
|
|
142
|
+
}
|
|
143
|
+
const warning = ceiling.warning(
|
|
144
|
+
ceiling.assess({ percent: percentNow(now), state: budget.state, env: process.env })
|
|
145
|
+
);
|
|
146
|
+
const message = [text, warning].filter(Boolean).join(' ');
|
|
147
|
+
return message ? { injectSteps: [{ ephemeralMessage: message }] } : { injectSteps: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PostToolUse, PostInvocation and Stop all want an object and none of them
|
|
151
|
+
// wants anything from this plugin. Deliberately NOT returning
|
|
152
|
+
// terminationBehavior on PostInvocation, and NOT returning decision
|
|
153
|
+
// "continue" on Stop: both would keep the loop running, which is the
|
|
154
|
+
// opposite of what a budget plugin should ever do to a user's quota.
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (require.main === module) {
|
|
159
|
+
readInput()
|
|
160
|
+
.then((input) => run(Date.now(), input, process.argv.slice(2)))
|
|
161
|
+
.then(
|
|
162
|
+
(result) => {
|
|
163
|
+
process.stdout.write(JSON.stringify(result || {}) + '\n');
|
|
164
|
+
process.exit(0);
|
|
165
|
+
},
|
|
166
|
+
() => {
|
|
167
|
+
// Hooks block the agent loop here, so a failure has to be silent and
|
|
168
|
+
// well formed rather than loud.
|
|
169
|
+
process.stdout.write('{}\n');
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { EVENTS, eventFrom, toolNameOf, percentNow, run };
|