@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 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
- Claude Code hooks run commands on the developer's machine, so none are emitted by
119
- default - opt in per hook in the config:
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
- `sync` writes the script to `.claude/hooks/ethlete/` and registers it in
128
- `.claude/settings.json` (your own entries are left untouched); removing the name from
129
- `hooks` unregisters and deletes it again.
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 Claude) when the session
134
- context crosses 70% / 85% of the token budget, recommending `/handoff`. The budget is
135
- capped at the 200k long-context pricing boundary: on 1M-window models every request
136
- past 200k input tokens bills the whole context at a premium rate, so the warnings
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 last main-chain assistant message in the session transcript, and emits a
6
- warning (visible to both the user and Claude) when it crosses a threshold.
7
- Recommends the /handoff skill so work can continue in a fresh session.
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
- The thresholds are fractions of a token budget, and the budget is capped at the
10
- 200k long-context pricing boundary: on models with a larger window, every
11
- request past that point bills the entire context at the premium rate, which
12
- costs far more than handing off into a fresh session ever would.
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 /compact).
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
- def disabled_locally():
58
- """True when the local config disables this hook (or all hooks) on this machine."""
59
- root = os.environ.get("CLAUDE_PROJECT_DIR")
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 False
163
+ return {}
62
164
  try:
63
165
  with open(os.path.join(root, LOCAL_CONFIG_FILE), encoding="utf-8") as f:
64
- disabled = json.load(f).get("disableHooks")
166
+ config = json.load(f)
65
167
  except (OSError, ValueError, AttributeError):
66
- return False
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 context_state(transcript_path):
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
- def messages(tier, tokens, budget, priced):
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
- if tier == 2 and priced:
119
- return (
120
- f"🔴 Context is at {k} tokens — about to cross the {budget_k} long-context "
121
- f"pricing boundary, after which every request bills the whole context at the "
122
- f"premium rate. Run /handoff now and continue in a fresh session.",
123
- f"[context-warning hook] The context is at {k} tokens — {pct}% of the "
124
- f"{budget_k} long-context pricing boundary. Past it every request is billed at "
125
- "the premium rate. Finish only the immediate step, then recommend the user run "
126
- "/handoff to save state and start a fresh session. Do not start new sub-tasks.",
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
- return (
130
- f"🔴 Context is at {k} tokens ({pct}% of the {budget_k} window) — "
131
- f"auto-compact is imminent. Run /handoff now and continue in a fresh session.",
132
- f"[context-warning hook] The context window is at {k} tokens — {pct}% of "
133
- f"this model's {budget_k} window (critical, ≥{int(CRITICAL_FRACTION * 100)}%). "
134
- "Finish only the immediate step, then recommend the user run /handoff to "
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
- if priced:
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"🟡 Context is at {k} tokens, approaching the {budget_k} long-context "
140
- f"pricing boundary. At the next natural stopping point, consider /handoff "
141
- f"to continue in a fresh session.",
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"🟡 Context is at {k} tokens ({pct}% of the {budget_k} window). At the next "
150
- f"natural stopping point, consider /handoff to continue in a fresh session.",
151
- f"[context-warning hook] The context window is at {k} tokens — {pct}% of "
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
- if disabled_locally():
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 = context_state(transcript_path)
168
- window = window_for(model)
169
- budget = min(window, PREMIUM_BOUNDARY)
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"claude-context-warning-{session_id}"
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(tier, tokens, budget, budget < window)
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. Part of the Ethlete styleguide (judgment beyond what lint enforces).
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']