@ethlete/agent-rules 0.1.0-next.3 → 0.1.0-next.5
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/CHANGELOG.md +26 -0
- package/README.md +27 -10
- package/content/hooks/context-warning.py +256 -69
- package/content/skills/angular-patterns/SKILL.md +1 -1
- package/content/skills/figma-export/SKILL.md +193 -0
- package/content/skills/figma-export/dump-figma-layers.py +83 -0
- package/content/skills/figma-export/dump-figma-svg.py +235 -0
- package/content/skills/figma-export/measure-template.mjs +87 -0
- package/content/skills/query/SKILL.md +6 -0
- package/content/skills/rxjs-signals/SKILL.md +1 -1
- package/content/skills/theming/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/lib/config.d.ts +5 -1
- package/src/lib/config.js +1 -1
- package/src/lib/config.js.map +1 -1
- package/src/lib/owned-paths.js +19 -1
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/plan.js +14 -4
- package/src/lib/plan.js.map +1 -1
- package/src/lib/targets/claude-hooks.d.ts +1 -23
- package/src/lib/targets/claude-hooks.js +15 -84
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/codex-hooks.d.ts +12 -0
- package/src/lib/targets/codex-hooks.js +33 -0
- package/src/lib/targets/codex-hooks.js.map +1 -0
- package/src/lib/targets/hooks-shared.d.ts +38 -0
- package/src/lib/targets/hooks-shared.js +95 -0
- package/src/lib/targets/hooks-shared.js.map +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @ethlete/agent-rules
|
|
2
2
|
|
|
3
|
+
## 0.1.0-next.5
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#3048](https://github.com/ethlete-io/ethdk/pull/3048) [`6e19999`](https://github.com/ethlete-io/ethdk/commit/6e199997f51b88aaa1860a56a6b96be057ba1205) Thanks [@github-actions](https://github.com/apps/github-actions)! - The `context-warning` hook now runs under Codex as well as Claude Code, registered in
|
|
8
|
+
`.codex/hooks.json` whenever the `codex` target is on.
|
|
9
|
+
|
|
10
|
+
- [#3049](https://github.com/ethlete-io/ethdk/pull/3049) [`a606dda`](https://github.com/ethlete-io/ethdk/commit/a606dda0695ac8cc4370816bf3ff1c0814436091) Thanks [@TomTomB](https://github.com/TomTomB)! - Add the `figma-export` skill, guiding agents through reconciling a component against a Figma "copy as CSS" export.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- [#3048](https://github.com/ethlete-io/ethdk/pull/3048) [`fc56189`](https://github.com/ethlete-io/ethdk/commit/fc56189450a45f2b5819d40945a71205a6d67ba0) Thanks [@github-actions](https://github.com/apps/github-actions)! - The `query` skill now says to prefer `withArgs` over passing `args` to `execute()`, and when the imperative form is still right.
|
|
15
|
+
|
|
16
|
+
- [#3048](https://github.com/ethlete-io/ethdk/pull/3048) [`a16cc69`](https://github.com/ethlete-io/ethdk/commit/a16cc698a3cd3f14d0419d8f9710929fb41b4711) Thanks [@github-actions](https://github.com/apps/github-actions)! - Figma export skill: read an SVG frame as well as the CSS dump, with a `dump-figma-svg.py`
|
|
17
|
+
that prints its box tree and measures the auto-layout gaps.
|
|
18
|
+
|
|
19
|
+
## 0.1.0-next.4
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [#3045](https://github.com/ethlete-io/ethdk/pull/3045) [`fcd14f0`](https://github.com/ethlete-io/ethdk/commit/fcd14f09b4b09f81b5bde9f128a0fac4b0e2245c) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `disableAutoHandoffSave` to `ethlete-agents.config.local.json`, to opt out of the context-warning hook's auto-mode auto-save at the critical tier while keeping its normal warnings.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#3045](https://github.com/ethlete-io/ethdk/pull/3045) [`eb1b0a9`](https://github.com/ethlete-io/ethdk/commit/eb1b0a9cfb4e868b13a2b8eb6fd13221a6e0a654) Thanks [@github-actions](https://github.com/apps/github-actions)! - `context-warning` hook: in auto mode, the critical-tier warning now saves a `/handoff` immediately instead of just recommending it.
|
|
28
|
+
|
|
3
29
|
## 0.1.0-next.3
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -115,8 +115,8 @@ a repo without `@ethlete/query` never sees the query guide.
|
|
|
115
115
|
|
|
116
116
|
## Hooks (opt-in)
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
Hooks run commands on the developer's machine, so none are emitted by default - opt in
|
|
119
|
+
per hook in the config:
|
|
120
120
|
|
|
121
121
|
```json
|
|
122
122
|
{
|
|
@@ -124,17 +124,31 @@ default - opt in per hook in the config:
|
|
|
124
124
|
}
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
They are emitted for whichever of the `claude` and `codex` targets is enabled:
|
|
128
|
+
|
|
129
|
+
| Target | Script | Registered in |
|
|
130
|
+
| -------- | ------------------------ | ----------------------- |
|
|
131
|
+
| `claude` | `.claude/hooks/ethlete/` | `.claude/settings.json` |
|
|
132
|
+
| `codex` | `.codex/hooks/ethlete/` | `.codex/hooks.json` |
|
|
133
|
+
|
|
134
|
+
Your own entries in those files are left untouched; removing the name from `hooks`
|
|
135
|
+
unregisters and deletes the script again. Codex only loads project-local hooks once the
|
|
136
|
+
`.codex/` layer is trusted, and honours `[features] hooks = false`.
|
|
130
137
|
|
|
131
138
|
Available hooks:
|
|
132
139
|
|
|
133
|
-
- **`context-warning`** - warns once per tier (and instructs
|
|
134
|
-
context crosses 70% / 85% of the token budget, recommending
|
|
135
|
-
capped at the 200k long-context pricing boundary: on 1M-window models every
|
|
136
|
-
past 200k input tokens bills the whole context at a premium rate, so the
|
|
137
|
-
fire at ~140k/~170k instead of deep into the expensive range.
|
|
140
|
+
- **`context-warning`** - warns once per tier (and instructs the agent) when the session
|
|
141
|
+
context crosses 70% / 85% of the token budget, recommending a handoff. Under Claude the
|
|
142
|
+
budget is capped at the 200k long-context pricing boundary: on 1M-window models every
|
|
143
|
+
request past 200k input tokens bills the whole context at a premium rate, so the
|
|
144
|
+
warnings fire at ~140k/~170k instead of deep into the expensive range. Codex has no
|
|
145
|
+
such boundary, so its budget is the model's own reported context window and the
|
|
146
|
+
warnings are pure occupancy.
|
|
147
|
+
|
|
148
|
+
Two things are Claude-only: the separate user-facing line (Codex documents only
|
|
149
|
+
`additionalContext`, so there the warning is folded into the text the model is told to
|
|
150
|
+
relay), and the auto-mode escalation that writes the handoff file unprompted - Codex's
|
|
151
|
+
`permission_mode` values are undocumented, so no value enables it.
|
|
138
152
|
|
|
139
153
|
Hooks can be turned off per machine - see the local config below.
|
|
140
154
|
|
|
@@ -153,6 +167,9 @@ differ per developer, without touching any committed file:
|
|
|
153
167
|
- **`disableHooks`** - `true` disables every generated hook; an array
|
|
154
168
|
(`["context-warning"]`) just the named ones. The hook scripts read the file at
|
|
155
169
|
runtime, so toggling takes effect on the next prompt - no `sync` needed.
|
|
170
|
+
- **`disableAutoHandoffSave`** - keeps the `context-warning` hook's tiered warnings but
|
|
171
|
+
drops the auto-mode escalation: at the critical tier it recommends `/handoff` instead
|
|
172
|
+
of saving the handoff file itself.
|
|
156
173
|
- **`sdkSourcePath`** - a local `ethlete-sdk` checkout. The `sdk-source` and
|
|
157
174
|
`sdk-local-build` skills read it when the agent needs the SDK's own sources, or has to
|
|
158
175
|
build the SDK and install it here through a `file:` dependency. A relative path is
|
|
@@ -2,20 +2,45 @@
|
|
|
2
2
|
"""UserPromptSubmit hook: warn when the context window is getting large.
|
|
3
3
|
|
|
4
4
|
Reads the hook input JSON from stdin, estimates the current context size from
|
|
5
|
-
the
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
the session transcript, and emits a warning (visible to both the user and the
|
|
6
|
+
agent) when it crosses a threshold. Recommends the handoff skill so work can
|
|
7
|
+
continue in a fresh session.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Runs under both Claude Code and Codex, selected by `--agent` on the command
|
|
10
|
+
line rather than sniffed from the payload — the generator writes the flag, so
|
|
11
|
+
the two never have to be told apart at runtime. The two differ in three ways
|
|
12
|
+
that matter here, all captured in AGENT_PROFILES:
|
|
13
|
+
|
|
14
|
+
* Transcript format. Claude writes one JSON object per message with
|
|
15
|
+
`message.usage`; Codex writes a rollout stream whose `token_count` events
|
|
16
|
+
carry a TokenUsageInfo. Codex's rollout format is explicitly not a stable
|
|
17
|
+
interface, so the parser searches each line for the usage object instead of
|
|
18
|
+
walking a fixed path.
|
|
19
|
+
* Context budget. Claude's budget is capped at the 200k long-context pricing
|
|
20
|
+
boundary: on models with a larger window, every request past that point
|
|
21
|
+
bills the entire context at the premium rate, which costs far more than
|
|
22
|
+
handing off into a fresh session ever would. Codex has no such boundary, so
|
|
23
|
+
its budget is just the window.
|
|
24
|
+
* How a handoff is invoked. Claude has /handoff and /clear; Codex has neither
|
|
25
|
+
and reads the skill from disk.
|
|
26
|
+
|
|
27
|
+
At the critical tier, if the session's permission_mode is "auto", the
|
|
28
|
+
instruction escalates from "recommend" to "just do it": auto mode already
|
|
29
|
+
means the user wants the agent acting without stopping to ask, and clearing +
|
|
30
|
+
resuming can't be triggered programmatically (no hook or tool can submit
|
|
31
|
+
input to a running session), so the best available automation is to save the
|
|
32
|
+
handoff file itself immediately rather than waiting for a natural stopping
|
|
33
|
+
point. Codex's permission_mode value set is undocumented, so its profile lists
|
|
34
|
+
no auto modes and the escalation stays off there.
|
|
13
35
|
|
|
14
36
|
Warns once per tier per session (state kept in a temp file); re-arms itself
|
|
15
|
-
if the context shrinks again (e.g. after
|
|
37
|
+
if the context shrinks again (e.g. after a compaction).
|
|
16
38
|
|
|
17
39
|
Can be disabled per machine via a gitignored ethlete-agents.config.local.json
|
|
18
40
|
at the repo root: {"disableHooks": true} or {"disableHooks": ["context-warning"]}.
|
|
41
|
+
To keep the tiered warnings but drop just the auto-mode auto-save escalation,
|
|
42
|
+
use {"disableAutoHandoffSave": true} instead - the critical tier then falls
|
|
43
|
+
back to recommending a handoff, same as non-auto mode.
|
|
19
44
|
|
|
20
45
|
Fail-safe: any error exits 0 with no output — the hook must never block a prompt.
|
|
21
46
|
"""
|
|
@@ -53,20 +78,108 @@ CONTEXT_WINDOWS = (
|
|
|
53
78
|
)
|
|
54
79
|
DEFAULT_WINDOW = 200_000
|
|
55
80
|
|
|
81
|
+
AGENT_PROFILES = {
|
|
82
|
+
"claude": {
|
|
83
|
+
"premium_boundary": PREMIUM_BOUNDARY,
|
|
84
|
+
# Claude's transcript reports no window, so it is resolved from the model id.
|
|
85
|
+
"default_window": None,
|
|
86
|
+
"auto_modes": ("auto",),
|
|
87
|
+
"emits_system_message": True,
|
|
88
|
+
"act_now": "Run /handoff now and continue in a fresh session.",
|
|
89
|
+
"act_later": "consider /handoff to continue in a fresh session",
|
|
90
|
+
"recommend": "recommend the user run /handoff to save state and start a fresh session",
|
|
91
|
+
"suggest": "suggest the user run /handoff to save state and start a fresh session",
|
|
92
|
+
"save_now": (
|
|
93
|
+
"run the handoff skill's save mode right now (finish only an in-flight atomic "
|
|
94
|
+
"edit first, nothing new). Then tell the user exactly which handoff file was "
|
|
95
|
+
"written and that they should run /clear, then '/handoff resume <slug>', to "
|
|
96
|
+
"continue — clearing and resuming can't be done programmatically, so this is "
|
|
97
|
+
"the one step still on them."
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
"codex": {
|
|
101
|
+
"premium_boundary": None,
|
|
102
|
+
# Only reached if a rollout omits model_context_window; the CONTEXT_WINDOWS table
|
|
103
|
+
# holds Claude model ids and would never match a Codex one.
|
|
104
|
+
"default_window": 272_000,
|
|
105
|
+
# Codex's permission_mode values are undocumented; until one is confirmed to mean
|
|
106
|
+
# "never ask", no value enables the auto-save escalation.
|
|
107
|
+
"auto_modes": (),
|
|
108
|
+
# Only hookSpecificOutput.additionalContext is documented for Codex, so the
|
|
109
|
+
# user-facing line is folded into the model-facing text instead.
|
|
110
|
+
"emits_system_message": False,
|
|
111
|
+
"act_now": "Save a handoff now and continue in a fresh session.",
|
|
112
|
+
"act_later": "consider saving a handoff and continuing in a fresh session",
|
|
113
|
+
"recommend": (
|
|
114
|
+
"tell the user you are near the context limit, then follow "
|
|
115
|
+
".agents/skills/ethlete-handoff/SKILL.md to save a handoff and have them start a "
|
|
116
|
+
"fresh session"
|
|
117
|
+
),
|
|
118
|
+
"suggest": (
|
|
119
|
+
"suggest saving a handoff via .agents/skills/ethlete-handoff/SKILL.md and "
|
|
120
|
+
"continuing in a fresh session"
|
|
121
|
+
),
|
|
122
|
+
"save_now": (
|
|
123
|
+
"follow .agents/skills/ethlete-handoff/SKILL.md and save a handoff right now "
|
|
124
|
+
"(finish only an in-flight atomic edit first, nothing new). Then tell the user "
|
|
125
|
+
"exactly which handoff file was written and that they should start a fresh "
|
|
126
|
+
"codex session and resume from it."
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
56
130
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
131
|
+
|
|
132
|
+
def agent_name(argv):
|
|
133
|
+
"""The --agent value, defaulting to claude — older registrations pass no flag."""
|
|
134
|
+
for index, arg in enumerate(argv):
|
|
135
|
+
if arg == "--agent" and index + 1 < len(argv):
|
|
136
|
+
return argv[index + 1]
|
|
137
|
+
if arg.startswith("--agent="):
|
|
138
|
+
return arg.split("=", 1)[1]
|
|
139
|
+
return "claude"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def repo_root(data):
|
|
143
|
+
"""Repo root: the env var the agent sets, else derived from this script's own location.
|
|
144
|
+
|
|
145
|
+
The script always lives at <root>/.<agent>/hooks/ethlete/context-warning.py, so walking
|
|
146
|
+
four levels up works for any agent that has no project-dir variable of its own.
|
|
147
|
+
"""
|
|
148
|
+
for variable in ("CLAUDE_PROJECT_DIR", "CODEX_PROJECT_DIR"):
|
|
149
|
+
value = os.environ.get(variable)
|
|
150
|
+
if value:
|
|
151
|
+
return value
|
|
152
|
+
here = os.path.abspath(__file__)
|
|
153
|
+
derived = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(here))))
|
|
154
|
+
if os.path.isfile(os.path.join(derived, LOCAL_CONFIG_FILE)):
|
|
155
|
+
return derived
|
|
156
|
+
cwd = data.get("cwd")
|
|
157
|
+
return cwd if isinstance(cwd, str) and cwd else derived
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def load_local_config(root):
|
|
161
|
+
"""Parsed ethlete-agents.config.local.json at the repo root, or {} if missing/unreadable."""
|
|
60
162
|
if not root:
|
|
61
|
-
return
|
|
163
|
+
return {}
|
|
62
164
|
try:
|
|
63
165
|
with open(os.path.join(root, LOCAL_CONFIG_FILE), encoding="utf-8") as f:
|
|
64
|
-
|
|
166
|
+
config = json.load(f)
|
|
65
167
|
except (OSError, ValueError, AttributeError):
|
|
66
|
-
return
|
|
168
|
+
return {}
|
|
169
|
+
return config if isinstance(config, dict) else {}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def disabled_locally(config):
|
|
173
|
+
"""True when the local config disables this hook (or all hooks) on this machine."""
|
|
174
|
+
disabled = config.get("disableHooks")
|
|
67
175
|
return disabled is True or (isinstance(disabled, list) and HOOK_NAME in disabled)
|
|
68
176
|
|
|
69
177
|
|
|
178
|
+
def auto_handoff_save_disabled(config):
|
|
179
|
+
"""True when the local config opts out of the auto-mode critical-tier auto-save."""
|
|
180
|
+
return config.get("disableAutoHandoffSave") is True
|
|
181
|
+
|
|
182
|
+
|
|
70
183
|
def window_for(model):
|
|
71
184
|
"""Context window for a model id, by first substring match; DEFAULT_WINDOW otherwise."""
|
|
72
185
|
if model:
|
|
@@ -76,10 +189,11 @@ def window_for(model):
|
|
|
76
189
|
return DEFAULT_WINDOW
|
|
77
190
|
|
|
78
191
|
|
|
79
|
-
def
|
|
80
|
-
"""(tokens, model) from the last main-chain assistant message.
|
|
192
|
+
def claude_context_state(transcript_path):
|
|
193
|
+
"""(tokens, model, window) from the last main-chain assistant message.
|
|
81
194
|
|
|
82
|
-
tokens ≈ its total input tokens (fresh + cache read + cache creation).
|
|
195
|
+
tokens ≈ its total input tokens (fresh + cache read + cache creation). Claude reports no
|
|
196
|
+
window in the transcript, so it is resolved from the model id by the caller.
|
|
83
197
|
"""
|
|
84
198
|
last_usage = None
|
|
85
199
|
last_model = None
|
|
@@ -97,82 +211,151 @@ def context_state(transcript_path):
|
|
|
97
211
|
last_usage = usage
|
|
98
212
|
last_model = message.get("model") or last_model
|
|
99
213
|
if not last_usage:
|
|
100
|
-
return 0, last_model
|
|
214
|
+
return 0, last_model, None
|
|
101
215
|
tokens = (
|
|
102
216
|
last_usage.get("input_tokens", 0)
|
|
103
217
|
+ last_usage.get("cache_read_input_tokens", 0)
|
|
104
218
|
+ last_usage.get("cache_creation_input_tokens", 0)
|
|
105
219
|
)
|
|
106
|
-
return tokens, last_model
|
|
220
|
+
return tokens, last_model, None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def find_token_usage_info(node):
|
|
224
|
+
"""The deepest TokenUsageInfo-shaped dict in a parsed rollout line, or None.
|
|
225
|
+
|
|
226
|
+
Codex documents its rollout format as unstable, so this searches for the shape
|
|
227
|
+
(a dict carrying `last_token_usage`) rather than walking a fixed key path.
|
|
228
|
+
"""
|
|
229
|
+
if isinstance(node, dict):
|
|
230
|
+
if isinstance(node.get("last_token_usage"), dict):
|
|
231
|
+
return node
|
|
232
|
+
for value in node.values():
|
|
233
|
+
found = find_token_usage_info(value)
|
|
234
|
+
if found:
|
|
235
|
+
return found
|
|
236
|
+
elif isinstance(node, list):
|
|
237
|
+
for value in node:
|
|
238
|
+
found = find_token_usage_info(value)
|
|
239
|
+
if found:
|
|
240
|
+
return found
|
|
241
|
+
return None
|
|
242
|
+
|
|
107
243
|
|
|
244
|
+
def codex_context_state(transcript_path):
|
|
245
|
+
"""(tokens, model, window) from the last token_count event in a Codex rollout.
|
|
108
246
|
|
|
109
|
-
|
|
247
|
+
tokens is the last request's `total_tokens` — Codex's own `tokens_in_context_window()`
|
|
248
|
+
is exactly that field, and `last_token_usage` is replaced per request while
|
|
249
|
+
`total_token_usage` accumulates across the whole session.
|
|
250
|
+
"""
|
|
251
|
+
last_info = None
|
|
252
|
+
last_model = None
|
|
253
|
+
with open(transcript_path, encoding="utf-8") as f:
|
|
254
|
+
for line in f:
|
|
255
|
+
try:
|
|
256
|
+
obj = json.loads(line)
|
|
257
|
+
except json.JSONDecodeError:
|
|
258
|
+
continue
|
|
259
|
+
model = (obj.get("payload") or {}).get("model") if isinstance(obj.get("payload"), dict) else None
|
|
260
|
+
if isinstance(model, str) and model:
|
|
261
|
+
last_model = model
|
|
262
|
+
info = find_token_usage_info(obj)
|
|
263
|
+
if info:
|
|
264
|
+
last_info = info
|
|
265
|
+
if not last_info:
|
|
266
|
+
return 0, last_model, None
|
|
267
|
+
usage = last_info.get("last_token_usage") or {}
|
|
268
|
+
window = last_info.get("model_context_window")
|
|
269
|
+
return usage.get("total_tokens", 0), last_model, window if isinstance(window, int) else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
CONTEXT_READERS = {"claude": claude_context_state, "codex": codex_context_state}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def messages(profile, tier, tokens, budget, priced, auto_mode):
|
|
110
276
|
"""(systemMessage, additionalContext) for a tier.
|
|
111
277
|
|
|
112
278
|
priced: the budget is the pricing boundary, not the window — the reason to
|
|
113
279
|
hand off is cost, not an imminent auto-compact.
|
|
280
|
+
auto_mode: at the critical tier, escalates from "recommend a handoff" to
|
|
281
|
+
"save it now" — see the module docstring for why.
|
|
114
282
|
"""
|
|
115
283
|
k = f"~{tokens // 1000}k"
|
|
116
284
|
pct = round(tokens / budget * 100)
|
|
117
285
|
budget_k = f"{budget // 1000}k"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
f"
|
|
124
|
-
f"
|
|
125
|
-
|
|
126
|
-
|
|
286
|
+
|
|
287
|
+
if priced:
|
|
288
|
+
approach = "about to cross" if tier == 2 else "approaching"
|
|
289
|
+
headline = f"Context is at {k} tokens — {approach} the {budget_k} long-context pricing boundary"
|
|
290
|
+
detail = (
|
|
291
|
+
f"The context is at {k} tokens — {pct}% of the {budget_k} long-context "
|
|
292
|
+
f"pricing boundary."
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
headline = f"Context is at {k} tokens ({pct}% of the {budget_k} window)"
|
|
296
|
+
detail = (
|
|
297
|
+
f"The context window is at {k} tokens — {pct}% of this model's "
|
|
298
|
+
f"{budget_k} window"
|
|
127
299
|
)
|
|
300
|
+
|
|
128
301
|
if tier == 2:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"save state and start a fresh session. Do not start new sub-tasks.",
|
|
302
|
+
threshold = f" (critical, ≥{int(CRITICAL_FRACTION * 100)}%)"
|
|
303
|
+
detail = detail if priced else f"{detail}{threshold}."
|
|
304
|
+
pressure = (
|
|
305
|
+
"Past it every request is billed at the premium rate. "
|
|
306
|
+
if priced
|
|
307
|
+
else ""
|
|
136
308
|
)
|
|
137
|
-
|
|
309
|
+
tail = "" if priced else " — auto-compact is imminent"
|
|
310
|
+
if auto_mode:
|
|
311
|
+
return (
|
|
312
|
+
f"🔴 {headline}{tail}. Auto mode is active: saving a handoff now.",
|
|
313
|
+
f"[context-warning hook] {detail} {pressure}Auto mode is active, so don't "
|
|
314
|
+
f"just recommend a handoff — {profile['save_now']}",
|
|
315
|
+
)
|
|
138
316
|
return (
|
|
139
|
-
f"
|
|
140
|
-
f"
|
|
141
|
-
f"
|
|
142
|
-
f"[context-warning hook] The context is at {k} tokens — {pct}% of the "
|
|
143
|
-
f"{budget_k} long-context pricing boundary, past which every request is "
|
|
144
|
-
"billed at the premium rate. When the current task reaches a natural "
|
|
145
|
-
"stopping point, suggest the user run /handoff to save state and start a "
|
|
146
|
-
"fresh session. Keep working normally until then.",
|
|
317
|
+
f"🔴 {headline}{tail}. {profile['act_now']}",
|
|
318
|
+
f"[context-warning hook] {detail} {pressure}Finish only the immediate step, "
|
|
319
|
+
f"then {profile['recommend']}. Do not start new sub-tasks.",
|
|
147
320
|
)
|
|
321
|
+
|
|
322
|
+
threshold = f" (≥{int(WARN_FRACTION * 100)}%)"
|
|
323
|
+
detail = detail if priced else f"{detail}{threshold}."
|
|
324
|
+
pressure = "Past it every request is billed at the premium rate. " if priced else ""
|
|
148
325
|
return (
|
|
149
|
-
f"🟡
|
|
150
|
-
f"
|
|
151
|
-
f"
|
|
152
|
-
f"this model's {budget_k} window (≥{int(WARN_FRACTION * 100)}%). When the "
|
|
153
|
-
"current task reaches a natural stopping point, suggest the user run "
|
|
154
|
-
"/handoff to save state and start a fresh session. Keep working normally until then.",
|
|
326
|
+
f"🟡 {headline}. At the next natural stopping point, {profile['act_later']}.",
|
|
327
|
+
f"[context-warning hook] {detail} {pressure}When the current task reaches a "
|
|
328
|
+
f"natural stopping point, {profile['suggest']}. Keep working normally until then.",
|
|
155
329
|
)
|
|
156
330
|
|
|
157
331
|
|
|
158
332
|
def main():
|
|
159
|
-
|
|
333
|
+
agent = agent_name(sys.argv[1:])
|
|
334
|
+
profile = AGENT_PROFILES.get(agent)
|
|
335
|
+
if not profile:
|
|
160
336
|
return
|
|
337
|
+
|
|
161
338
|
data = json.load(sys.stdin)
|
|
339
|
+
local_config = load_local_config(repo_root(data))
|
|
340
|
+
if disabled_locally(local_config):
|
|
341
|
+
return
|
|
342
|
+
|
|
162
343
|
transcript_path = data.get("transcript_path")
|
|
163
344
|
session_id = data.get("session_id", "unknown")
|
|
345
|
+
auto_mode = data.get("permission_mode") in profile["auto_modes"] and not auto_handoff_save_disabled(local_config)
|
|
164
346
|
if not transcript_path or not os.path.isfile(transcript_path):
|
|
165
347
|
return
|
|
166
348
|
|
|
167
|
-
tokens, model =
|
|
168
|
-
window = window_for(model)
|
|
169
|
-
|
|
349
|
+
tokens, model, reported_window = CONTEXT_READERS[agent](transcript_path)
|
|
350
|
+
window = reported_window or profile["default_window"] or window_for(model)
|
|
351
|
+
boundary = profile["premium_boundary"]
|
|
352
|
+
budget = min(window, boundary) if boundary else window
|
|
170
353
|
warn_tokens = int(budget * WARN_FRACTION)
|
|
171
354
|
critical_tokens = int(budget * CRITICAL_FRACTION)
|
|
172
355
|
tier = 2 if tokens >= critical_tokens else 1 if tokens >= warn_tokens else 0
|
|
173
356
|
|
|
174
357
|
state_file = os.path.join(
|
|
175
|
-
tempfile.gettempdir(), f"
|
|
358
|
+
tempfile.gettempdir(), f"{agent}-context-warning-{session_id}"
|
|
176
359
|
)
|
|
177
360
|
prev_tier = 0
|
|
178
361
|
try:
|
|
@@ -191,21 +374,25 @@ def main():
|
|
|
191
374
|
if tier <= prev_tier:
|
|
192
375
|
return # already warned at this tier (or context shrank — state re-armed above)
|
|
193
376
|
|
|
194
|
-
system_message, additional_context = messages(
|
|
195
|
-
|
|
196
|
-
print(
|
|
197
|
-
json.dumps(
|
|
198
|
-
{
|
|
199
|
-
"systemMessage": system_message,
|
|
200
|
-
"suppressOutput": True,
|
|
201
|
-
"hookSpecificOutput": {
|
|
202
|
-
"hookEventName": "UserPromptSubmit",
|
|
203
|
-
"additionalContext": additional_context,
|
|
204
|
-
},
|
|
205
|
-
}
|
|
206
|
-
)
|
|
377
|
+
system_message, additional_context = messages(
|
|
378
|
+
profile, tier, tokens, budget, budget < window, auto_mode
|
|
207
379
|
)
|
|
208
380
|
|
|
381
|
+
if not profile["emits_system_message"]:
|
|
382
|
+
additional_context = f"{additional_context}\n\nTell the user: {system_message}"
|
|
383
|
+
|
|
384
|
+
payload = {
|
|
385
|
+
"hookSpecificOutput": {
|
|
386
|
+
"hookEventName": "UserPromptSubmit",
|
|
387
|
+
"additionalContext": additional_context,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if profile["emits_system_message"]:
|
|
391
|
+
payload["systemMessage"] = system_message
|
|
392
|
+
payload["suppressOutput"] = True
|
|
393
|
+
|
|
394
|
+
print(json.dumps(payload))
|
|
395
|
+
|
|
209
396
|
|
|
210
397
|
if __name__ == "__main__":
|
|
211
398
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: angular-patterns
|
|
3
|
-
description: How to build Angular pieces the Ethlete way - templates, lifecycle, and when to reach for a component/directive/service/pipe vs a plain function. Read when writing or restructuring a component, directive, service, or pipe, wiring up lifecycle, or binding values in a template.
|
|
3
|
+
description: How to build Angular pieces the Ethlete way - templates, lifecycle, and when to reach for a component/directive/service/pipe vs a plain function. Read when writing or restructuring a component, directive, service, or pipe, wiring up lifecycle, or binding values in a template.
|
|
4
4
|
kind: skill
|
|
5
5
|
scope: both
|
|
6
6
|
requires: ['@ethlete/core']
|