@ethlete/agent-rules 0.1.0-next.0 → 0.1.0-next.10
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 +102 -0
- package/README.md +363 -16
- package/content/git-hooks/post-checkout.sh +10 -0
- package/content/git-hooks/pre-push.sh +5 -0
- package/content/hooks/context-warning.py +430 -0
- package/content/output-styles/ste-clarity.md +131 -0
- package/content/rules/comments.md +50 -20
- package/content/skills/angular-patterns/SKILL.md +1 -1
- package/content/skills/api-source/SKILL.md +118 -0
- 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/git-commit/SKILL.md +6 -7
- package/content/skills/git-flow/SKILL.md +87 -0
- package/content/skills/handoff/SKILL.md +4 -0
- package/content/skills/query/SKILL.md +23 -13
- package/content/skills/rxjs-signals/SKILL.md +1 -1
- package/content/skills/sdk-docs/SKILL.md +10 -2
- package/content/skills/sdk-local-build/SKILL.md +115 -0
- package/content/skills/sdk-source/SKILL.md +133 -0
- package/content/skills/styleguide/STYLEGUIDE.md +2 -2
- package/content/skills/theming/SKILL.md +1 -1
- package/content/skills/timetrack/SKILL.md +66 -0
- package/package.json +12 -1
- package/src/index.js +35 -14
- package/src/index.js.map +1 -1
- package/src/lib/commitlint.d.ts +10 -0
- package/src/lib/commitlint.js +51 -0
- package/src/lib/commitlint.js.map +1 -0
- package/src/lib/config.d.ts +64 -2
- package/src/lib/config.js +54 -9
- package/src/lib/config.js.map +1 -1
- package/src/lib/git-flow/build.d.ts +35 -0
- package/src/lib/git-flow/build.js +24 -0
- package/src/lib/git-flow/build.js.map +1 -0
- package/src/lib/git-flow/config.d.ts +59 -0
- package/src/lib/git-flow/config.js +50 -0
- package/src/lib/git-flow/config.js.map +1 -0
- package/src/lib/git-flow/index.d.ts +6 -0
- package/src/lib/git-flow/index.js +10 -0
- package/src/lib/git-flow/index.js.map +1 -0
- package/src/lib/git-flow/parse.d.ts +49 -0
- package/src/lib/git-flow/parse.js +274 -0
- package/src/lib/git-flow/parse.js.map +1 -0
- package/src/lib/git-flow/rename.d.ts +24 -0
- package/src/lib/git-flow/rename.js +70 -0
- package/src/lib/git-flow/rename.js.map +1 -0
- package/src/lib/git-flow/start.d.ts +49 -0
- package/src/lib/git-flow/start.js +57 -0
- package/src/lib/git-flow/start.js.map +1 -0
- package/src/lib/git-flow/validate.d.ts +34 -0
- package/src/lib/git-flow/validate.js +72 -0
- package/src/lib/git-flow/validate.js.map +1 -0
- package/src/lib/git-flow-command.d.ts +4 -0
- package/src/lib/git-flow-command.js +157 -0
- package/src/lib/git-flow-command.js.map +1 -0
- package/src/lib/git-flow-repair.d.ts +17 -0
- package/src/lib/git-flow-repair.js +146 -0
- package/src/lib/git-flow-repair.js.map +1 -0
- package/src/lib/git-flow-start.d.ts +20 -0
- package/src/lib/git-flow-start.js +132 -0
- package/src/lib/git-flow-start.js.map +1 -0
- package/src/lib/git.d.ts +27 -0
- package/src/lib/git.js +49 -0
- package/src/lib/git.js.map +1 -0
- package/src/lib/gitlab.d.ts +35 -0
- package/src/lib/gitlab.js +98 -0
- package/src/lib/gitlab.js.map +1 -0
- package/src/lib/index.d.ts +2 -0
- package/src/lib/index.js +2 -0
- package/src/lib/index.js.map +1 -1
- package/src/lib/migrate.d.ts +6 -0
- package/src/lib/migrate.js +155 -0
- package/src/lib/migrate.js.map +1 -0
- package/src/lib/output-style-command.d.ts +3 -0
- package/src/lib/output-style-command.js +69 -0
- package/src/lib/output-style-command.js.map +1 -0
- package/src/lib/output-style.d.ts +38 -0
- package/src/lib/output-style.js +126 -0
- package/src/lib/output-style.js.map +1 -0
- package/src/lib/owned-paths.js +22 -1
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/plan.d.ts +4 -1
- package/src/lib/plan.js +141 -8
- package/src/lib/plan.js.map +1 -1
- package/src/lib/prompt.d.ts +8 -0
- package/src/lib/prompt.js +27 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/render.d.ts +20 -2
- package/src/lib/render.js +31 -8
- package/src/lib/render.js.map +1 -1
- package/src/lib/sync.d.ts +0 -1
- package/src/lib/sync.js +8 -2
- package/src/lib/sync.js.map +1 -1
- package/src/lib/targets/agents-skills.d.ts +8 -0
- package/src/lib/targets/agents-skills.js +18 -0
- package/src/lib/targets/agents-skills.js.map +1 -0
- package/src/lib/targets/claude-hooks.d.ts +11 -0
- package/src/lib/targets/claude-hooks.js +28 -0
- package/src/lib/targets/claude-hooks.js.map +1 -0
- package/src/lib/targets/claude.d.ts +3 -1
- package/src/lib/targets/claude.js +14 -21
- package/src/lib/targets/claude.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/codex.d.ts +5 -3
- package/src/lib/targets/codex.js +9 -8
- package/src/lib/targets/codex.js.map +1 -1
- package/src/lib/targets/copilot.d.ts +3 -3
- package/src/lib/targets/copilot.js +6 -29
- package/src/lib/targets/copilot.js.map +1 -1
- package/src/lib/targets/cursor.d.ts +3 -3
- package/src/lib/targets/cursor.js +7 -12
- package/src/lib/targets/cursor.js.map +1 -1
- package/src/lib/targets/git-hooks.d.ts +24 -0
- package/src/lib/targets/git-hooks.js +70 -0
- package/src/lib/targets/git-hooks.js.map +1 -0
- package/src/lib/targets/hooks-shared.d.ts +37 -0
- package/src/lib/targets/hooks-shared.js +95 -0
- package/src/lib/targets/hooks-shared.js.map +1 -0
- package/src/lib/targets/shared.d.ts +22 -13
- package/src/lib/targets/shared.js +32 -15
- package/src/lib/targets/shared.js.map +1 -1
- package/src/lib/timetrack-command.d.ts +11 -0
- package/src/lib/timetrack-command.js +199 -0
- package/src/lib/timetrack-command.js.map +1 -0
- package/src/lib/timetrack.d.ts +86 -0
- package/src/lib/timetrack.js +112 -0
- package/src/lib/timetrack.js.map +1 -0
- package/src/lib/targets/neutral.d.ts +0 -8
- package/src/lib/targets/neutral.js +0 -29
- package/src/lib/targets/neutral.js.map +0 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit hook: warn when the context window is getting large.
|
|
3
|
+
|
|
4
|
+
Reads the hook input JSON from stdin, estimates the current context size from
|
|
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
|
+
|
|
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 a slash command and /clear; Codex has
|
|
25
|
+
neither and reads the skill from disk. The skill's own name differs per repo
|
|
26
|
+
(`ethlete-handoff` where the generator installed it, `handoff` where the repo
|
|
27
|
+
ships its own copy), so it is resolved at runtime — see handoff_skill().
|
|
28
|
+
|
|
29
|
+
At the critical tier, if the session's permission_mode is "auto", the
|
|
30
|
+
instruction escalates from "recommend" to "just do it": auto mode already
|
|
31
|
+
means the user wants the agent acting without stopping to ask, and clearing +
|
|
32
|
+
resuming can't be triggered programmatically (no hook or tool can submit
|
|
33
|
+
input to a running session), so the best available automation is to save the
|
|
34
|
+
handoff file itself immediately rather than waiting for a natural stopping
|
|
35
|
+
point. Codex's permission_mode value set is undocumented, so its profile lists
|
|
36
|
+
no auto modes and the escalation stays off there.
|
|
37
|
+
|
|
38
|
+
Warns once per tier per session (state kept in a temp file); re-arms itself
|
|
39
|
+
if the context shrinks again (e.g. after a compaction).
|
|
40
|
+
|
|
41
|
+
Can be disabled per machine via a gitignored ethlete-agents.config.local.json
|
|
42
|
+
at the repo root: {"disableHooks": true} or {"disableHooks": ["context-warning"]}.
|
|
43
|
+
To keep the tiered warnings but drop just the auto-mode auto-save escalation,
|
|
44
|
+
use {"disableAutoHandoffSave": true} instead - the critical tier then falls
|
|
45
|
+
back to recommending a handoff, same as non-auto mode.
|
|
46
|
+
|
|
47
|
+
Fail-safe: any error exits 0 with no output — the hook must never block a prompt.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import os
|
|
52
|
+
import sys
|
|
53
|
+
import tempfile
|
|
54
|
+
|
|
55
|
+
HOOK_NAME = "context-warning"
|
|
56
|
+
LOCAL_CONFIG_FILE = "ethlete-agents.config.local.json"
|
|
57
|
+
|
|
58
|
+
# Warn / critical fire at these fractions of the token budget.
|
|
59
|
+
WARN_FRACTION = 0.70
|
|
60
|
+
CRITICAL_FRACTION = 0.85
|
|
61
|
+
|
|
62
|
+
# On models with a window larger than this, tokens beyond it bill the whole
|
|
63
|
+
# context at the long-context premium rate — so the budget never exceeds it.
|
|
64
|
+
PREMIUM_BOUNDARY = 200_000
|
|
65
|
+
|
|
66
|
+
# Context window (tokens) per model, matched by substring against the model id
|
|
67
|
+
# from the transcript — first match wins. Edit these as model windows change;
|
|
68
|
+
# anything unmatched falls back to DEFAULT_WINDOW.
|
|
69
|
+
CONTEXT_WINDOWS = (
|
|
70
|
+
("opus-5", 1_000_000),
|
|
71
|
+
("opus-4-8", 1_000_000),
|
|
72
|
+
("sonnet-4-5", 1_000_000),
|
|
73
|
+
("sonnet-5", 1_000_000),
|
|
74
|
+
("fable-5", 1_000_000),
|
|
75
|
+
# Generic fallbacks — keep these last: the first substring match wins, so a bare
|
|
76
|
+
# "opus"/"sonnet" entry placed above would shadow every versioned entry below it.
|
|
77
|
+
("opus", 200_000),
|
|
78
|
+
("sonnet", 200_000),
|
|
79
|
+
("haiku", 200_000),
|
|
80
|
+
)
|
|
81
|
+
DEFAULT_WINDOW = 200_000
|
|
82
|
+
|
|
83
|
+
AGENT_PROFILES = {
|
|
84
|
+
"claude": {
|
|
85
|
+
"premium_boundary": PREMIUM_BOUNDARY,
|
|
86
|
+
# Claude's transcript reports no window, so it is resolved from the model id.
|
|
87
|
+
"default_window": None,
|
|
88
|
+
"auto_modes": ("auto",),
|
|
89
|
+
"emits_system_message": True,
|
|
90
|
+
"act_now": "Run /{handoff} now and continue in a fresh session.",
|
|
91
|
+
"act_later": "consider /{handoff} to continue in a fresh session",
|
|
92
|
+
"recommend": "recommend the user run /{handoff} to save state and start a fresh session",
|
|
93
|
+
"suggest": "suggest the user run /{handoff} to save state and start a fresh session",
|
|
94
|
+
"save_now": (
|
|
95
|
+
"run the handoff skill's save mode right now (finish only an in-flight atomic "
|
|
96
|
+
"edit first, nothing new). Then tell the user exactly which handoff file was "
|
|
97
|
+
"written and that they should run /clear, then '/{handoff} resume <slug>', to "
|
|
98
|
+
"continue — clearing and resuming can't be done programmatically, so this is "
|
|
99
|
+
"the one step still on them."
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
"codex": {
|
|
103
|
+
"premium_boundary": None,
|
|
104
|
+
# Only reached if a rollout omits model_context_window; the CONTEXT_WINDOWS table
|
|
105
|
+
# holds Claude model ids and would never match a Codex one.
|
|
106
|
+
"default_window": 272_000,
|
|
107
|
+
# Codex's permission_mode values are undocumented; until one is confirmed to mean
|
|
108
|
+
# "never ask", no value enables the auto-save escalation.
|
|
109
|
+
"auto_modes": (),
|
|
110
|
+
# Only hookSpecificOutput.additionalContext is documented for Codex, so the
|
|
111
|
+
# user-facing line is folded into the model-facing text instead.
|
|
112
|
+
"emits_system_message": False,
|
|
113
|
+
"act_now": "Save a handoff now and continue in a fresh session.",
|
|
114
|
+
"act_later": "consider saving a handoff and continuing in a fresh session",
|
|
115
|
+
"recommend": (
|
|
116
|
+
"tell the user you are near the context limit, then follow "
|
|
117
|
+
".agents/skills/{handoff}/SKILL.md to save a handoff and have them start a "
|
|
118
|
+
"fresh session"
|
|
119
|
+
),
|
|
120
|
+
"suggest": (
|
|
121
|
+
"suggest saving a handoff via .agents/skills/{handoff}/SKILL.md and "
|
|
122
|
+
"continuing in a fresh session"
|
|
123
|
+
),
|
|
124
|
+
"save_now": (
|
|
125
|
+
"follow .agents/skills/{handoff}/SKILL.md and save a handoff right now "
|
|
126
|
+
"(finish only an in-flight atomic edit first, nothing new). Then tell the user "
|
|
127
|
+
"exactly which handoff file was written and that they should start a fresh "
|
|
128
|
+
"codex session and resume from it."
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def handoff_skill(root):
|
|
135
|
+
"""Name of the handoff skill installed in this repo.
|
|
136
|
+
|
|
137
|
+
The generator prefixes every skill it writes, so the command is /ethlete-handoff in a
|
|
138
|
+
consumer repo — but a repo that excludes the generated copy and ships its own (the SDK
|
|
139
|
+
itself) has a plain /handoff instead. Naming the wrong one sends the user to a slash
|
|
140
|
+
command that does not exist, so it is read off disk rather than assumed.
|
|
141
|
+
"""
|
|
142
|
+
for name in ("ethlete-handoff", "handoff"):
|
|
143
|
+
for skills_dir in (".claude/skills", ".agents/skills"):
|
|
144
|
+
if os.path.isdir(os.path.join(root or ".", skills_dir, name)):
|
|
145
|
+
return name
|
|
146
|
+
return "ethlete-handoff"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_profile(profile, skill):
|
|
150
|
+
"""Fills the installed handoff skill's name into a profile's message fragments."""
|
|
151
|
+
return {
|
|
152
|
+
key: value.replace("{handoff}", skill) if isinstance(value, str) else value
|
|
153
|
+
for key, value in profile.items()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def agent_name(argv):
|
|
158
|
+
"""The --agent value, defaulting to claude — older registrations pass no flag."""
|
|
159
|
+
for index, arg in enumerate(argv):
|
|
160
|
+
if arg == "--agent" and index + 1 < len(argv):
|
|
161
|
+
return argv[index + 1]
|
|
162
|
+
if arg.startswith("--agent="):
|
|
163
|
+
return arg.split("=", 1)[1]
|
|
164
|
+
return "claude"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def repo_root(data):
|
|
168
|
+
"""Repo root: the env var the agent sets, else derived from this script's own location.
|
|
169
|
+
|
|
170
|
+
The script always lives at <root>/.<agent>/hooks/ethlete/context-warning.py, so walking
|
|
171
|
+
four levels up works for any agent that has no project-dir variable of its own.
|
|
172
|
+
"""
|
|
173
|
+
for variable in ("CLAUDE_PROJECT_DIR", "CODEX_PROJECT_DIR"):
|
|
174
|
+
value = os.environ.get(variable)
|
|
175
|
+
if value:
|
|
176
|
+
return value
|
|
177
|
+
here = os.path.abspath(__file__)
|
|
178
|
+
derived = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(here))))
|
|
179
|
+
if os.path.isfile(os.path.join(derived, LOCAL_CONFIG_FILE)):
|
|
180
|
+
return derived
|
|
181
|
+
cwd = data.get("cwd")
|
|
182
|
+
return cwd if isinstance(cwd, str) and cwd else derived
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def load_local_config(root):
|
|
186
|
+
"""Parsed ethlete-agents.config.local.json at the repo root, or {} if missing/unreadable."""
|
|
187
|
+
if not root:
|
|
188
|
+
return {}
|
|
189
|
+
try:
|
|
190
|
+
with open(os.path.join(root, LOCAL_CONFIG_FILE), encoding="utf-8") as f:
|
|
191
|
+
config = json.load(f)
|
|
192
|
+
except (OSError, ValueError, AttributeError):
|
|
193
|
+
return {}
|
|
194
|
+
return config if isinstance(config, dict) else {}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def disabled_locally(config):
|
|
198
|
+
"""True when the local config disables this hook (or all hooks) on this machine."""
|
|
199
|
+
disabled = config.get("disableHooks")
|
|
200
|
+
return disabled is True or (isinstance(disabled, list) and HOOK_NAME in disabled)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def auto_handoff_save_disabled(config):
|
|
204
|
+
"""True when the local config opts out of the auto-mode critical-tier auto-save."""
|
|
205
|
+
return config.get("disableAutoHandoffSave") is True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def window_for(model):
|
|
209
|
+
"""Context window for a model id, by first substring match; DEFAULT_WINDOW otherwise."""
|
|
210
|
+
if model:
|
|
211
|
+
for needle, window in CONTEXT_WINDOWS:
|
|
212
|
+
if needle in model:
|
|
213
|
+
return window
|
|
214
|
+
return DEFAULT_WINDOW
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def claude_context_state(transcript_path):
|
|
218
|
+
"""(tokens, model, window) from the last main-chain assistant message.
|
|
219
|
+
|
|
220
|
+
tokens ≈ its total input tokens (fresh + cache read + cache creation). Claude reports no
|
|
221
|
+
window in the transcript, so it is resolved from the model id by the caller.
|
|
222
|
+
"""
|
|
223
|
+
last_usage = None
|
|
224
|
+
last_model = None
|
|
225
|
+
with open(transcript_path, encoding="utf-8") as f:
|
|
226
|
+
for line in f:
|
|
227
|
+
try:
|
|
228
|
+
obj = json.loads(line)
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
continue
|
|
231
|
+
if obj.get("type") != "assistant" or obj.get("isSidechain"):
|
|
232
|
+
continue
|
|
233
|
+
message = obj.get("message") or {}
|
|
234
|
+
usage = message.get("usage")
|
|
235
|
+
if usage:
|
|
236
|
+
last_usage = usage
|
|
237
|
+
last_model = message.get("model") or last_model
|
|
238
|
+
if not last_usage:
|
|
239
|
+
return 0, last_model, None
|
|
240
|
+
tokens = (
|
|
241
|
+
last_usage.get("input_tokens", 0)
|
|
242
|
+
+ last_usage.get("cache_read_input_tokens", 0)
|
|
243
|
+
+ last_usage.get("cache_creation_input_tokens", 0)
|
|
244
|
+
)
|
|
245
|
+
return tokens, last_model, None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def find_token_usage_info(node):
|
|
249
|
+
"""The deepest TokenUsageInfo-shaped dict in a parsed rollout line, or None.
|
|
250
|
+
|
|
251
|
+
Codex documents its rollout format as unstable, so this searches for the shape
|
|
252
|
+
(a dict carrying `last_token_usage`) rather than walking a fixed key path.
|
|
253
|
+
"""
|
|
254
|
+
if isinstance(node, dict):
|
|
255
|
+
if isinstance(node.get("last_token_usage"), dict):
|
|
256
|
+
return node
|
|
257
|
+
for value in node.values():
|
|
258
|
+
found = find_token_usage_info(value)
|
|
259
|
+
if found:
|
|
260
|
+
return found
|
|
261
|
+
elif isinstance(node, list):
|
|
262
|
+
for value in node:
|
|
263
|
+
found = find_token_usage_info(value)
|
|
264
|
+
if found:
|
|
265
|
+
return found
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def codex_context_state(transcript_path):
|
|
270
|
+
"""(tokens, model, window) from the last token_count event in a Codex rollout.
|
|
271
|
+
|
|
272
|
+
tokens is the last request's `total_tokens` — Codex's own `tokens_in_context_window()`
|
|
273
|
+
is exactly that field, and `last_token_usage` is replaced per request while
|
|
274
|
+
`total_token_usage` accumulates across the whole session.
|
|
275
|
+
"""
|
|
276
|
+
last_info = None
|
|
277
|
+
last_model = None
|
|
278
|
+
with open(transcript_path, encoding="utf-8") as f:
|
|
279
|
+
for line in f:
|
|
280
|
+
try:
|
|
281
|
+
obj = json.loads(line)
|
|
282
|
+
except json.JSONDecodeError:
|
|
283
|
+
continue
|
|
284
|
+
model = (obj.get("payload") or {}).get("model") if isinstance(obj.get("payload"), dict) else None
|
|
285
|
+
if isinstance(model, str) and model:
|
|
286
|
+
last_model = model
|
|
287
|
+
info = find_token_usage_info(obj)
|
|
288
|
+
if info:
|
|
289
|
+
last_info = info
|
|
290
|
+
if not last_info:
|
|
291
|
+
return 0, last_model, None
|
|
292
|
+
usage = last_info.get("last_token_usage") or {}
|
|
293
|
+
window = last_info.get("model_context_window")
|
|
294
|
+
return usage.get("total_tokens", 0), last_model, window if isinstance(window, int) else None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
CONTEXT_READERS = {"claude": claude_context_state, "codex": codex_context_state}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def messages(profile, tier, tokens, budget, priced, auto_mode):
|
|
301
|
+
"""(systemMessage, additionalContext) for a tier.
|
|
302
|
+
|
|
303
|
+
priced: the budget is the pricing boundary, not the window — the reason to
|
|
304
|
+
hand off is cost, not an imminent auto-compact.
|
|
305
|
+
auto_mode: at the critical tier, escalates from "recommend a handoff" to
|
|
306
|
+
"save it now" — see the module docstring for why.
|
|
307
|
+
"""
|
|
308
|
+
k = f"~{tokens // 1000}k"
|
|
309
|
+
pct = round(tokens / budget * 100)
|
|
310
|
+
budget_k = f"{budget // 1000}k"
|
|
311
|
+
|
|
312
|
+
if priced:
|
|
313
|
+
approach = "about to cross" if tier == 2 else "approaching"
|
|
314
|
+
headline = f"Context is at {k} tokens — {approach} the {budget_k} long-context pricing boundary"
|
|
315
|
+
detail = (
|
|
316
|
+
f"The context is at {k} tokens — {pct}% of the {budget_k} long-context "
|
|
317
|
+
f"pricing boundary."
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
headline = f"Context is at {k} tokens ({pct}% of the {budget_k} window)"
|
|
321
|
+
detail = (
|
|
322
|
+
f"The context window is at {k} tokens — {pct}% of this model's "
|
|
323
|
+
f"{budget_k} window"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if tier == 2:
|
|
327
|
+
threshold = f" (critical, ≥{int(CRITICAL_FRACTION * 100)}%)"
|
|
328
|
+
detail = detail if priced else f"{detail}{threshold}."
|
|
329
|
+
pressure = (
|
|
330
|
+
"Past it every request is billed at the premium rate. "
|
|
331
|
+
if priced
|
|
332
|
+
else ""
|
|
333
|
+
)
|
|
334
|
+
tail = "" if priced else " — auto-compact is imminent"
|
|
335
|
+
if auto_mode:
|
|
336
|
+
return (
|
|
337
|
+
f"🔴 {headline}{tail}. Auto mode is active: saving a handoff now.",
|
|
338
|
+
f"[context-warning hook] {detail} {pressure}Auto mode is active, so don't "
|
|
339
|
+
f"just recommend a handoff — {profile['save_now']}",
|
|
340
|
+
)
|
|
341
|
+
return (
|
|
342
|
+
f"🔴 {headline}{tail}. {profile['act_now']}",
|
|
343
|
+
f"[context-warning hook] {detail} {pressure}Finish only the immediate step, "
|
|
344
|
+
f"then {profile['recommend']}. Do not start new sub-tasks.",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
threshold = f" (≥{int(WARN_FRACTION * 100)}%)"
|
|
348
|
+
detail = detail if priced else f"{detail}{threshold}."
|
|
349
|
+
pressure = "Past it every request is billed at the premium rate. " if priced else ""
|
|
350
|
+
return (
|
|
351
|
+
f"🟡 {headline}. At the next natural stopping point, {profile['act_later']}.",
|
|
352
|
+
f"[context-warning hook] {detail} {pressure}When the current task reaches a "
|
|
353
|
+
f"natural stopping point, {profile['suggest']}. Keep working normally until then.",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def main():
|
|
358
|
+
agent = agent_name(sys.argv[1:])
|
|
359
|
+
profile = AGENT_PROFILES.get(agent)
|
|
360
|
+
if not profile:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
data = json.load(sys.stdin)
|
|
364
|
+
root = repo_root(data)
|
|
365
|
+
local_config = load_local_config(root)
|
|
366
|
+
if disabled_locally(local_config):
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
profile = resolve_profile(profile, handoff_skill(root))
|
|
370
|
+
|
|
371
|
+
transcript_path = data.get("transcript_path")
|
|
372
|
+
session_id = data.get("session_id", "unknown")
|
|
373
|
+
auto_mode = data.get("permission_mode") in profile["auto_modes"] and not auto_handoff_save_disabled(local_config)
|
|
374
|
+
if not transcript_path or not os.path.isfile(transcript_path):
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
tokens, model, reported_window = CONTEXT_READERS[agent](transcript_path)
|
|
378
|
+
window = reported_window or profile["default_window"] or window_for(model)
|
|
379
|
+
boundary = profile["premium_boundary"]
|
|
380
|
+
budget = min(window, boundary) if boundary else window
|
|
381
|
+
warn_tokens = int(budget * WARN_FRACTION)
|
|
382
|
+
critical_tokens = int(budget * CRITICAL_FRACTION)
|
|
383
|
+
tier = 2 if tokens >= critical_tokens else 1 if tokens >= warn_tokens else 0
|
|
384
|
+
|
|
385
|
+
state_file = os.path.join(
|
|
386
|
+
tempfile.gettempdir(), f"{agent}-context-warning-{session_id}"
|
|
387
|
+
)
|
|
388
|
+
prev_tier = 0
|
|
389
|
+
try:
|
|
390
|
+
with open(state_file, encoding="utf-8") as f:
|
|
391
|
+
prev_tier = int(f.read().strip() or 0)
|
|
392
|
+
except (OSError, ValueError):
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
if tier != prev_tier:
|
|
396
|
+
try:
|
|
397
|
+
with open(state_file, "w", encoding="utf-8") as f:
|
|
398
|
+
f.write(str(tier))
|
|
399
|
+
except OSError:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
if tier <= prev_tier:
|
|
403
|
+
return # already warned at this tier (or context shrank — state re-armed above)
|
|
404
|
+
|
|
405
|
+
system_message, additional_context = messages(
|
|
406
|
+
profile, tier, tokens, budget, budget < window, auto_mode
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if not profile["emits_system_message"]:
|
|
410
|
+
additional_context = f"{additional_context}\n\nTell the user: {system_message}"
|
|
411
|
+
|
|
412
|
+
payload = {
|
|
413
|
+
"hookSpecificOutput": {
|
|
414
|
+
"hookEventName": "UserPromptSubmit",
|
|
415
|
+
"additionalContext": additional_context,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if profile["emits_system_message"]:
|
|
419
|
+
payload["systemMessage"] = system_message
|
|
420
|
+
payload["suppressOutput"] = True
|
|
421
|
+
|
|
422
|
+
print(json.dumps(payload))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
try:
|
|
427
|
+
main()
|
|
428
|
+
except Exception:
|
|
429
|
+
pass
|
|
430
|
+
sys.exit(0)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ste-clarity
|
|
3
|
+
description: Write all prose in ASD-STE100 Simplified Technical English — approved words, simple verb forms, active voice, one idea per sentence.
|
|
4
|
+
keep-coding-instructions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ASD-STE100 output style
|
|
8
|
+
|
|
9
|
+
Write every sentence you show the user in Simplified Technical English, as specified
|
|
10
|
+
by ASD-STE100. Clarity comes first. Never sacrifice technical accuracy for brevity.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
Apply STE to your own prose: answers, explanations, plans, summaries, commit
|
|
15
|
+
messages, and pull request text.
|
|
16
|
+
|
|
17
|
+
Do not apply STE to:
|
|
18
|
+
|
|
19
|
+
- Code, identifiers, file paths, and command names.
|
|
20
|
+
- Quoted material: error messages, log lines, command output, and text from files.
|
|
21
|
+
Copy these exactly. Never rewrite a quote to make it simpler.
|
|
22
|
+
- Text the user asks you to write in a different style.
|
|
23
|
+
|
|
24
|
+
Markdown is still available. Use code blocks, and use `file.ts:42` references.
|
|
25
|
+
|
|
26
|
+
## Words
|
|
27
|
+
|
|
28
|
+
1. Use the most common, shortest word that has one clear meaning.
|
|
29
|
+
2. Use one word for one meaning. Do not use synonyms for the same thing. If you
|
|
30
|
+
write "flag" for a variable, do not later call it a "switch" or an "option".
|
|
31
|
+
3. Use one meaning for one word. Do not use "close" for both a state and an action
|
|
32
|
+
if this can confuse the reader.
|
|
33
|
+
4. Technical names are permitted. Use the exact name from the code or the domain.
|
|
34
|
+
5. Do not use slang, idioms, or figures of speech. "The build blew up" is wrong.
|
|
35
|
+
"The build failed" is correct.
|
|
36
|
+
6. Do not use "etc." Complete the list, or write "for example" and give two items.
|
|
37
|
+
7. Do not use a noun as a verb. Write "make a request", not "request it", when
|
|
38
|
+
"request" is a name in the code.
|
|
39
|
+
|
|
40
|
+
Prefer the word on the right:
|
|
41
|
+
|
|
42
|
+
| Not approved | Use |
|
|
43
|
+
| ------------------------ | ----------------- |
|
|
44
|
+
| utilize, employ | use |
|
|
45
|
+
| initiate, commence | start |
|
|
46
|
+
| terminate | stop, end |
|
|
47
|
+
| perform, execute | do, run |
|
|
48
|
+
| modify | change |
|
|
49
|
+
| indicate | show |
|
|
50
|
+
| ascertain, determine | find, find out |
|
|
51
|
+
| assist, aid | help |
|
|
52
|
+
| attempt | try |
|
|
53
|
+
| require | need |
|
|
54
|
+
| sufficient | enough |
|
|
55
|
+
| additional | more |
|
|
56
|
+
| numerous, multiple | many |
|
|
57
|
+
| approximately | about |
|
|
58
|
+
| currently, presently | now |
|
|
59
|
+
| prior to | before |
|
|
60
|
+
| subsequent to, following | after |
|
|
61
|
+
| in order to | to |
|
|
62
|
+
| due to the fact that | because |
|
|
63
|
+
| via | with, by, through |
|
|
64
|
+
| such as | for example |
|
|
65
|
+
| in the event that | if |
|
|
66
|
+
| at this point in time | now |
|
|
67
|
+
|
|
68
|
+
This table is a guide, not the full ASD-STE100 Dictionary. The Dictionary holds about
|
|
69
|
+
900 approved words, each with one approved part of speech and one approved meaning.
|
|
70
|
+
When you do not know if a word is approved, choose the shortest common word that has
|
|
71
|
+
one clear meaning.
|
|
72
|
+
|
|
73
|
+
## Verbs
|
|
74
|
+
|
|
75
|
+
1. Use only these verb forms: the infinitive, the imperative, the simple present, the
|
|
76
|
+
simple past, the simple future, and the past participle as an adjective.
|
|
77
|
+
2. Do not use complex tenses. Write "I changed the config", not "I have changed the
|
|
78
|
+
config".
|
|
79
|
+
3. Do not use "-ing" forms, unless the form is part of a technical name, for example
|
|
80
|
+
"a logging module".
|
|
81
|
+
4. Use the active voice. Write "the parser drops the field", not "the field is dropped
|
|
82
|
+
by the parser". Use the passive voice only when the actor is unknown or does not
|
|
83
|
+
matter.
|
|
84
|
+
5. Do not leave out a verb to make a sentence shorter.
|
|
85
|
+
|
|
86
|
+
## Noun phrases
|
|
87
|
+
|
|
88
|
+
1. Do not build a cluster of more than three nouns. Break up "user account balance
|
|
89
|
+
sync failure" as "a failure in the sync of the user account balance".
|
|
90
|
+
2. Add articles ("a", "the") where they help the reader.
|
|
91
|
+
|
|
92
|
+
## Sentences
|
|
93
|
+
|
|
94
|
+
1. Instructions: 20 words maximum.
|
|
95
|
+
2. Descriptions: 25 words maximum.
|
|
96
|
+
3. One idea per sentence. One instruction per sentence.
|
|
97
|
+
4. A paragraph holds 6 sentences maximum, and covers one topic.
|
|
98
|
+
5. Put a condition at the start of the sentence, before the instruction. Write "If the
|
|
99
|
+
sync fails, restart the server".
|
|
100
|
+
6. Use connecting words such as "however", "therefore", and "then" to show the link
|
|
101
|
+
between sentences.
|
|
102
|
+
7. Use a vertical list when the content has more than three parallel parts. Do not use
|
|
103
|
+
a list to give structure to a single idea.
|
|
104
|
+
|
|
105
|
+
## Procedures
|
|
106
|
+
|
|
107
|
+
1. Use the imperative for each step. Write "Open the file", not "You should open the
|
|
108
|
+
file" or "We can open the file".
|
|
109
|
+
2. Number the steps in the order the user must do them.
|
|
110
|
+
3. Give the result after the action, if the result is not obvious.
|
|
111
|
+
|
|
112
|
+
## Warnings
|
|
113
|
+
|
|
114
|
+
1. Put a warning or a caution before the step it applies to, never after.
|
|
115
|
+
2. Start the warning with a clear command. Write "Do not run this on the production
|
|
116
|
+
database. The command deletes all rows."
|
|
117
|
+
3. State the consequence in a separate, simple sentence.
|
|
118
|
+
|
|
119
|
+
## Punctuation
|
|
120
|
+
|
|
121
|
+
1. Do not use a slash to mean "and" or "or". Write "the date or the payee".
|
|
122
|
+
2. Do not use parentheses for information the reader needs. Put it in its own sentence.
|
|
123
|
+
3. Do not use a dash to join two thoughts. Use two sentences.
|
|
124
|
+
4. Use a hyphen only to make a compound word clear.
|
|
125
|
+
|
|
126
|
+
## When STE and accuracy conflict
|
|
127
|
+
|
|
128
|
+
Accuracy wins. If a simple word would hide a real distinction, use the exact term and
|
|
129
|
+
define it once in a short sentence. Never simplify a claim about what the code does.
|
|
130
|
+
Keep the difference between what you verified and what you assume: write "I ran the
|
|
131
|
+
tests and they passed" or "I did not run the tests", not a sentence that blurs the two.
|
|
@@ -1,31 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: comments
|
|
3
|
-
description:
|
|
3
|
+
description: Comments are an allowlist of four cases. Everything else gets deleted before the change is done.
|
|
4
4
|
kind: rule
|
|
5
5
|
scope: both
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## Comments:
|
|
8
|
+
## Comments: almost none, and never for the reviewer of your change
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
**Write no comment unless it fits one of the four cases below.** This is an allowlist, not a
|
|
11
|
+
set of tips. Anything outside it gets **deleted** before you call the change done — not
|
|
12
|
+
softened, not shortened. Code that needs prose to be understood needs a better name, a
|
|
13
|
+
smaller function, or a type; fix that instead of narrating it.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
### The only comments allowed
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
once where the pattern is defined (the helper's JSDoc, the lint rule's message, the guide)
|
|
23
|
-
and let every use site stay silent.
|
|
24
|
-
- **Restating the code.** `// increment the counter` above `counter++`.
|
|
17
|
+
1. **An ordering or timing constraint** a reasonable edit would break. Say what breaks.
|
|
18
|
+
2. **An invariant the types cannot express**, that a caller or a future edit could violate.
|
|
19
|
+
3. **A workaround**, naming its concrete cause (browser bug, upstream issue, framework
|
|
20
|
+
limitation) and linking it where a link exists, so the next reader can tell when it may go.
|
|
21
|
+
4. **Public API JSDoc** — what it does and how to call it, on something a lib actually
|
|
22
|
+
exports. One or two sentences. Not internals, not history, not why it is shaped that way.
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
use it — not why it is shaped that way).
|
|
24
|
+
Nothing else qualifies. Not "this is subtle", not "worth noting", not a heading over a group
|
|
25
|
+
of members, not a summary of the function underneath it.
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
### The test each one still has to pass
|
|
28
|
+
|
|
29
|
+
Delete it unless **both** are true:
|
|
30
|
+
|
|
31
|
+
- a competent reader who never sees your diff would be **surprised** without it, and
|
|
32
|
+
- a future edit could **break something** that this sentence is the only warning about.
|
|
33
|
+
|
|
34
|
+
Unsure counts as no. A missing comment costs a minute of reading; a stale one misleads for
|
|
35
|
+
years.
|
|
36
|
+
|
|
37
|
+
### Always delete
|
|
38
|
+
|
|
39
|
+
- **Restating the code** — `// increment the counter` over `counter++`; a JSDoc on `size` that
|
|
40
|
+
says nothing beyond "the size of the button".
|
|
41
|
+
- **Section headers and dividers** — `// --- Inputs ---`, `// Helpers`, `// Public API`.
|
|
42
|
+
- **Rationale for a mechanical choice** — `Record<Size, X>` with literal keys, a `@__PURE__`
|
|
43
|
+
annotation, a factory instead of a literal, a helper moved into its own file. The type, the
|
|
44
|
+
annotation and the import already say what happens.
|
|
45
|
+
- **Migration narration** — "moved here from X", "used to be a tuple", "so Y no longer pulls
|
|
46
|
+
Z", "renamed for clarity". Git knows; the next reader does not care.
|
|
47
|
+
- **The same explanation at every call site.** Explain a pattern once where it is defined (the
|
|
48
|
+
helper's JSDoc, the lint rule's message, the guide) and let every use site stay silent.
|
|
49
|
+
- **Commented-out code.**
|
|
50
|
+
- **`TODO`/`FIXME` without an issue link.** Fix it now or leave nothing.
|
|
51
|
+
- **Hedging and meta** — "note that", "for clarity", "just in case", "this is cleaner", "we
|
|
52
|
+
could also…".
|
|
53
|
+
|
|
54
|
+
### Before you call the change done
|
|
55
|
+
|
|
56
|
+
Re-read every comment in your diff and cut the ones that are not one of the four. Then fix or
|
|
57
|
+
delete any existing comment your change made wrong — one describing behaviour that no longer
|
|
58
|
+
exists is worse than none.
|
|
59
|
+
|
|
60
|
+
Two signals you have already over-commented: you wrote the word "because", or the diff adds
|
|
61
|
+
more than a handful of comments. Both mean go back and cut.
|
|
@@ -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']
|