@ethlete/agent-rules 0.1.0-next.1 → 0.1.0-next.11

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +296 -21
  3. package/content/git-hooks/post-checkout.sh +10 -0
  4. package/content/git-hooks/pre-push.sh +5 -0
  5. package/content/hooks/context-warning.py +284 -69
  6. package/content/output-styles/ste-clarity.md +131 -0
  7. package/content/rules/comments.md +50 -20
  8. package/content/skills/angular-patterns/SKILL.md +1 -1
  9. package/content/skills/api-source/SKILL.md +118 -0
  10. package/content/skills/figma-export/SKILL.md +193 -0
  11. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  12. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  13. package/content/skills/figma-export/measure-template.mjs +87 -0
  14. package/content/skills/git-commit/SKILL.md +6 -7
  15. package/content/skills/git-flow/SKILL.md +87 -0
  16. package/content/skills/handoff/SKILL.md +4 -0
  17. package/content/skills/query/SKILL.md +23 -13
  18. package/content/skills/rxjs-signals/SKILL.md +1 -1
  19. package/content/skills/sdk-docs/SKILL.md +10 -2
  20. package/content/skills/sdk-local-build/SKILL.md +115 -0
  21. package/content/skills/sdk-source/SKILL.md +133 -0
  22. package/content/skills/styleguide/STYLEGUIDE.md +2 -2
  23. package/content/skills/theming/SKILL.md +14 -7
  24. package/content/skills/timetrack/SKILL.md +66 -0
  25. package/package.json +12 -1
  26. package/src/index.js +23 -10
  27. package/src/index.js.map +1 -1
  28. package/src/lib/commitlint.d.ts +10 -0
  29. package/src/lib/commitlint.js +51 -0
  30. package/src/lib/commitlint.js.map +1 -0
  31. package/src/lib/config.d.ts +35 -6
  32. package/src/lib/config.js +26 -3
  33. package/src/lib/config.js.map +1 -1
  34. package/src/lib/git-flow/build.d.ts +35 -0
  35. package/src/lib/git-flow/build.js +24 -0
  36. package/src/lib/git-flow/build.js.map +1 -0
  37. package/src/lib/git-flow/config.d.ts +59 -0
  38. package/src/lib/git-flow/config.js +50 -0
  39. package/src/lib/git-flow/config.js.map +1 -0
  40. package/src/lib/git-flow/index.d.ts +6 -0
  41. package/src/lib/git-flow/index.js +10 -0
  42. package/src/lib/git-flow/index.js.map +1 -0
  43. package/src/lib/git-flow/parse.d.ts +49 -0
  44. package/src/lib/git-flow/parse.js +274 -0
  45. package/src/lib/git-flow/parse.js.map +1 -0
  46. package/src/lib/git-flow/rename.d.ts +24 -0
  47. package/src/lib/git-flow/rename.js +70 -0
  48. package/src/lib/git-flow/rename.js.map +1 -0
  49. package/src/lib/git-flow/start.d.ts +49 -0
  50. package/src/lib/git-flow/start.js +57 -0
  51. package/src/lib/git-flow/start.js.map +1 -0
  52. package/src/lib/git-flow/validate.d.ts +34 -0
  53. package/src/lib/git-flow/validate.js +72 -0
  54. package/src/lib/git-flow/validate.js.map +1 -0
  55. package/src/lib/git-flow-command.d.ts +4 -0
  56. package/src/lib/git-flow-command.js +157 -0
  57. package/src/lib/git-flow-command.js.map +1 -0
  58. package/src/lib/git-flow-repair.d.ts +17 -0
  59. package/src/lib/git-flow-repair.js +146 -0
  60. package/src/lib/git-flow-repair.js.map +1 -0
  61. package/src/lib/git-flow-start.d.ts +20 -0
  62. package/src/lib/git-flow-start.js +132 -0
  63. package/src/lib/git-flow-start.js.map +1 -0
  64. package/src/lib/git.d.ts +27 -0
  65. package/src/lib/git.js +49 -0
  66. package/src/lib/git.js.map +1 -0
  67. package/src/lib/gitlab.d.ts +35 -0
  68. package/src/lib/gitlab.js +98 -0
  69. package/src/lib/gitlab.js.map +1 -0
  70. package/src/lib/index.d.ts +1 -0
  71. package/src/lib/index.js +1 -0
  72. package/src/lib/index.js.map +1 -1
  73. package/src/lib/output-style-command.d.ts +3 -0
  74. package/src/lib/output-style-command.js +69 -0
  75. package/src/lib/output-style-command.js.map +1 -0
  76. package/src/lib/output-style.d.ts +38 -0
  77. package/src/lib/output-style.js +126 -0
  78. package/src/lib/output-style.js.map +1 -0
  79. package/src/lib/owned-paths.js +19 -1
  80. package/src/lib/owned-paths.js.map +1 -1
  81. package/src/lib/plan.d.ts +0 -1
  82. package/src/lib/plan.js +83 -9
  83. package/src/lib/plan.js.map +1 -1
  84. package/src/lib/prompt.d.ts +8 -0
  85. package/src/lib/prompt.js +27 -0
  86. package/src/lib/prompt.js.map +1 -0
  87. package/src/lib/render.d.ts +20 -2
  88. package/src/lib/render.js +31 -8
  89. package/src/lib/render.js.map +1 -1
  90. package/src/lib/sync.d.ts +0 -1
  91. package/src/lib/sync.js +2 -2
  92. package/src/lib/sync.js.map +1 -1
  93. package/src/lib/targets/claude-hooks.d.ts +1 -23
  94. package/src/lib/targets/claude-hooks.js +15 -84
  95. package/src/lib/targets/claude-hooks.js.map +1 -1
  96. package/src/lib/targets/claude.js +1 -1
  97. package/src/lib/targets/claude.js.map +1 -1
  98. package/src/lib/targets/codex-hooks.d.ts +12 -0
  99. package/src/lib/targets/codex-hooks.js +33 -0
  100. package/src/lib/targets/codex-hooks.js.map +1 -0
  101. package/src/lib/targets/codex.js +1 -1
  102. package/src/lib/targets/codex.js.map +1 -1
  103. package/src/lib/targets/copilot.js +1 -1
  104. package/src/lib/targets/copilot.js.map +1 -1
  105. package/src/lib/targets/cursor.js +1 -1
  106. package/src/lib/targets/cursor.js.map +1 -1
  107. package/src/lib/targets/git-hooks.d.ts +24 -0
  108. package/src/lib/targets/git-hooks.js +70 -0
  109. package/src/lib/targets/git-hooks.js.map +1 -0
  110. package/src/lib/targets/hooks-shared.d.ts +37 -0
  111. package/src/lib/targets/hooks-shared.js +95 -0
  112. package/src/lib/targets/hooks-shared.js.map +1 -0
  113. package/src/lib/targets/shared.d.ts +0 -1
  114. package/src/lib/targets/shared.js +1 -1
  115. package/src/lib/targets/shared.js.map +1 -1
  116. package/src/lib/timetrack-command.d.ts +11 -0
  117. package/src/lib/timetrack-command.js +199 -0
  118. package/src/lib/timetrack-command.js.map +1 -0
  119. package/src/lib/timetrack.d.ts +86 -0
  120. package/src/lib/timetrack.js +112 -0
  121. package/src/lib/timetrack.js.map +1 -0
@@ -2,20 +2,47 @@
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 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.
13
37
 
14
38
  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).
39
+ if the context shrinks again (e.g. after a compaction).
16
40
 
17
41
  Can be disabled per machine via a gitignored ethlete-agents.config.local.json
18
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.
19
46
 
20
47
  Fail-safe: any error exits 0 with no output — the hook must never block a prompt.
21
48
  """
@@ -53,20 +80,131 @@ CONTEXT_WINDOWS = (
53
80
  )
54
81
  DEFAULT_WINDOW = 200_000
55
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
+ }
56
132
 
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")
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."""
60
187
  if not root:
61
- return False
188
+ return {}
62
189
  try:
63
190
  with open(os.path.join(root, LOCAL_CONFIG_FILE), encoding="utf-8") as f:
64
- disabled = json.load(f).get("disableHooks")
191
+ config = json.load(f)
65
192
  except (OSError, ValueError, AttributeError):
66
- return False
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")
67
200
  return disabled is True or (isinstance(disabled, list) and HOOK_NAME in disabled)
68
201
 
69
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
+
70
208
  def window_for(model):
71
209
  """Context window for a model id, by first substring match; DEFAULT_WINDOW otherwise."""
72
210
  if model:
@@ -76,10 +214,11 @@ def window_for(model):
76
214
  return DEFAULT_WINDOW
77
215
 
78
216
 
79
- def context_state(transcript_path):
80
- """(tokens, model) from the last main-chain assistant message.
217
+ def claude_context_state(transcript_path):
218
+ """(tokens, model, window) from the last main-chain assistant message.
81
219
 
82
- tokens ≈ its total input tokens (fresh + cache read + cache creation).
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.
83
222
  """
84
223
  last_usage = None
85
224
  last_model = None
@@ -97,82 +236,154 @@ def context_state(transcript_path):
97
236
  last_usage = usage
98
237
  last_model = message.get("model") or last_model
99
238
  if not last_usage:
100
- return 0, last_model
239
+ return 0, last_model, None
101
240
  tokens = (
102
241
  last_usage.get("input_tokens", 0)
103
242
  + last_usage.get("cache_read_input_tokens", 0)
104
243
  + last_usage.get("cache_creation_input_tokens", 0)
105
244
  )
106
- return tokens, last_model
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}
107
298
 
108
299
 
109
- def messages(tier, tokens, budget, priced):
300
+ def messages(profile, tier, tokens, budget, priced, auto_mode):
110
301
  """(systemMessage, additionalContext) for a tier.
111
302
 
112
303
  priced: the budget is the pricing boundary, not the window — the reason to
113
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.
114
307
  """
115
308
  k = f"~{tokens // 1000}k"
116
309
  pct = round(tokens / budget * 100)
117
310
  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.",
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."
127
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
+
128
326
  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.",
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 ""
136
333
  )
137
- if priced:
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
+ )
138
341
  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.",
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.",
147
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 ""
148
350
  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.",
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.",
155
354
  )
156
355
 
157
356
 
158
357
  def main():
159
- if disabled_locally():
358
+ agent = agent_name(sys.argv[1:])
359
+ profile = AGENT_PROFILES.get(agent)
360
+ if not profile:
160
361
  return
362
+
161
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
+
162
371
  transcript_path = data.get("transcript_path")
163
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)
164
374
  if not transcript_path or not os.path.isfile(transcript_path):
165
375
  return
166
376
 
167
- tokens, model = context_state(transcript_path)
168
- window = window_for(model)
169
- budget = min(window, PREMIUM_BOUNDARY)
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
170
381
  warn_tokens = int(budget * WARN_FRACTION)
171
382
  critical_tokens = int(budget * CRITICAL_FRACTION)
172
383
  tier = 2 if tokens >= critical_tokens else 1 if tokens >= warn_tokens else 0
173
384
 
174
385
  state_file = os.path.join(
175
- tempfile.gettempdir(), f"claude-context-warning-{session_id}"
386
+ tempfile.gettempdir(), f"{agent}-context-warning-{session_id}"
176
387
  )
177
388
  prev_tier = 0
178
389
  try:
@@ -191,21 +402,25 @@ def main():
191
402
  if tier <= prev_tier:
192
403
  return # already warned at this tier (or context shrank — state re-armed above)
193
404
 
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
- )
405
+ system_message, additional_context = messages(
406
+ profile, tier, tokens, budget, budget < window, auto_mode
207
407
  )
208
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
+
209
424
 
210
425
  if __name__ == "__main__":
211
426
  try:
@@ -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: Write comments for the next reader of the file, not for the reviewer of your change.
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: write for the next reader of this file, not for the reviewer of your change
8
+ ## Comments: almost none, and never for the reviewer of your change
9
9
 
10
- A comment earns its place by telling someone **using or editing this code** something the
11
- code cannot. Explaining _why the change was made_ is not that — it belongs in the commit
12
- message, the changeset, or the docs.
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
- Do **not** leave behind:
15
+ ### The only comments allowed
15
16
 
16
- - **Rationale for a mechanical choice.** `Record<Size, X>` with literal keys, a `@__PURE__`
17
- annotation, a factory instead of a literal, a helper moved to another file — the type,
18
- the annotation and the import already say what happens.
19
- - **Migration narration.** "moved here from X", "used to be a tuple", "so Y no longer pulls Z".
20
- Git knows. A reader six months from now does not care.
21
- - **The same explanation repeated per call site.** If a pattern needs explaining, explain it
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
- Do keep: non-obvious behaviour and ordering constraints, a real invariant a future edit could
27
- break, a workaround with the reason it exists, and public API JSDoc (what it does and how to
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
- When you catch yourself writing "because", check whether the sentence is aimed at the reviewer
31
- of your diff. If it is, cut it.
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.