@ethlete/agent-rules 0.1.0-next.0 → 0.1.0-next.2

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 (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +97 -15
  3. package/content/hooks/context-warning.py +215 -0
  4. package/content/skills/sdk-docs/SKILL.md +1 -0
  5. package/content/skills/sdk-local-build/SKILL.md +113 -0
  6. package/content/skills/sdk-source/SKILL.md +108 -0
  7. package/content/skills/styleguide/STYLEGUIDE.md +1 -1
  8. package/package.json +1 -1
  9. package/src/index.js +12 -4
  10. package/src/index.js.map +1 -1
  11. package/src/lib/config.d.ts +39 -2
  12. package/src/lib/config.js +30 -8
  13. package/src/lib/config.js.map +1 -1
  14. package/src/lib/index.d.ts +1 -0
  15. package/src/lib/index.js +1 -0
  16. package/src/lib/index.js.map +1 -1
  17. package/src/lib/migrate.d.ts +6 -0
  18. package/src/lib/migrate.js +155 -0
  19. package/src/lib/migrate.js.map +1 -0
  20. package/src/lib/owned-paths.js +4 -1
  21. package/src/lib/owned-paths.js.map +1 -1
  22. package/src/lib/plan.d.ts +4 -0
  23. package/src/lib/plan.js +85 -6
  24. package/src/lib/plan.js.map +1 -1
  25. package/src/lib/sync.js +8 -2
  26. package/src/lib/sync.js.map +1 -1
  27. package/src/lib/targets/agents-skills.d.ts +8 -0
  28. package/src/lib/targets/agents-skills.js +18 -0
  29. package/src/lib/targets/agents-skills.js.map +1 -0
  30. package/src/lib/targets/claude-hooks.d.ts +33 -0
  31. package/src/lib/targets/claude-hooks.js +97 -0
  32. package/src/lib/targets/claude-hooks.js.map +1 -0
  33. package/src/lib/targets/claude.d.ts +3 -1
  34. package/src/lib/targets/claude.js +13 -20
  35. package/src/lib/targets/claude.js.map +1 -1
  36. package/src/lib/targets/codex.d.ts +5 -3
  37. package/src/lib/targets/codex.js +8 -7
  38. package/src/lib/targets/codex.js.map +1 -1
  39. package/src/lib/targets/copilot.d.ts +3 -3
  40. package/src/lib/targets/copilot.js +5 -28
  41. package/src/lib/targets/copilot.js.map +1 -1
  42. package/src/lib/targets/cursor.d.ts +3 -3
  43. package/src/lib/targets/cursor.js +6 -11
  44. package/src/lib/targets/cursor.js.map +1 -1
  45. package/src/lib/targets/shared.d.ts +22 -12
  46. package/src/lib/targets/shared.js +32 -15
  47. package/src/lib/targets/shared.js.map +1 -1
  48. package/src/lib/targets/neutral.d.ts +0 -8
  49. package/src/lib/targets/neutral.js +0 -29
  50. package/src/lib/targets/neutral.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @ethlete/agent-rules
2
2
 
3
+ ## 0.1.0-next.2
4
+
5
+ ### Minor Changes
6
+
7
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`a311f80`](https://github.com/ethlete-io/ethdk/commit/a311f80455bc9cc9a925fe8f72ac945a1b057315) Thanks [@github-actions](https://github.com/apps/github-actions)! - New `sdk-source` and `sdk-local-build` skills let an agent read the SDK's own sources and test an unreleased build via `file:`, from the checkout named by the local config's new `sdkSourcePath`.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#3043](https://github.com/ethlete-io/ethdk/pull/3043) [`9627646`](https://github.com/ethlete-io/ethdk/commit/96276462e1c2ecde5394b8b1eafcebbb9f56a973) Thanks [@github-actions](https://github.com/apps/github-actions)! - Styleguide: changeset notes are now capped at one to two sentences, with mechanism and API inventories
12
+ explicitly sent to the docs instead.
13
+
14
+ ## 0.1.0-next.1
15
+
16
+ ### Minor Changes
17
+
18
+ - [#3042](https://github.com/ethlete-io/ethdk/pull/3042) [`28a58eb`](https://github.com/ethlete-io/ethdk/commit/28a58ebc56420b7e067d8c56108b39601d7b367e) Thanks [@github-actions](https://github.com/apps/github-actions)! - - Skills now compile to the cross-tool `.agents/skills/ethlete-*/SKILL.md` format, discovered natively by Codex, Cursor and Copilot; the `.agents/ethlete/` pointer tree is pruned on sync.
19
+ - New `ethlete-agents migrate` converts a repo to the `AGENTS.md`-canonical layout: `CLAUDE.md` becomes an `@AGENTS.md` import and hand-written skills move to `.agents/skills` with symlinks.
20
+ - New opt-in `hooks` config: `context-warning` warns (and instructs Claude) before the context crosses the 200k long-context pricing boundary, recommending `/handoff`.
21
+ - A gitignored `ethlete-agents.config.local.json` (`"disableHooks": true` or a list of hook names) disables generated hooks per machine at runtime — committed files never change, and `sync`/`check` warn about unsupported keys or unknown hook names in it.
22
+
3
23
  ## 0.1.0-next.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -23,17 +23,44 @@ npx ethlete-agents check # exits non-zero when the generated files are stale
23
23
 
24
24
  ## What gets written
25
25
 
26
- | | Claude Code | Cursor | Copilot | Codex |
27
- | ------------------- | ----------------------------------- | --------------------------------------------------- | ------------------------------------------------------ | ------------------------------ |
28
- | Always-loaded rules | `.claude/rules/ethlete/*.md` | `.cursor/rules/ethlete-*.mdc` (`alwaysApply: true`) | inlined into `.github/copilot-instructions.md` | inlined into `AGENTS.md` |
29
- | On-demand guides | `.claude/skills/ethlete-*/SKILL.md` | `.cursor/rules/ethlete-*.mdc` | `.github/instructions/*.instructions.md`, or a pointer | a pointer table in `AGENTS.md` |
30
-
31
- `AGENTS.md` supports neither frontmatter nor includes, so anything a target cannot
32
- express on-demand falls back to plain markdown under `.agents/ethlete/` plus a pointer
33
- from the always-loaded file. Marker-block files (`AGENTS.md`,
34
- `.github/copilot-instructions.md`) are only rewritten between
35
- `<!-- ethlete:agent-rules:start -->` and `:end` - everything you wrote around them
36
- survives.
26
+ Skills are compiled once into `.agents/skills/ethlete-*/SKILL.md` - the cross-tool
27
+ [Agent Skills](https://agentskills.io) format that Codex, Cursor, Copilot and VS Code
28
+ discover natively (each skill's body loads on demand from its `description`
29
+ frontmatter). Claude Code only scans `.claude/skills/`, so the claude target writes its
30
+ own copies there. Rules are always-loaded and go into each tool's native mechanism:
31
+
32
+ | | Claude Code | Cursor | Copilot | Codex |
33
+ | ------------------- | ----------------------------------- | --------------------------------------------------- | ---------------------------------------------- | ----------------------------------- |
34
+ | Always-loaded rules | `.claude/rules/ethlete/*.md` | `.cursor/rules/ethlete-*.mdc` (`alwaysApply: true`) | inlined into `.github/copilot-instructions.md` | inlined into `AGENTS.md` |
35
+ | On-demand skills | `.claude/skills/ethlete-*/SKILL.md` | `.agents/skills/ethlete-*/SKILL.md` | `.agents/skills/ethlete-*/SKILL.md` | `.agents/skills/ethlete-*/SKILL.md` |
36
+
37
+ Marker-block files (`AGENTS.md`, `.github/copilot-instructions.md`) are only rewritten
38
+ between `<!-- ethlete:agent-rules:start -->` and `:end` - everything you wrote around
39
+ them survives.
40
+
41
+ ## Migrating a repo to the AGENTS.md layout
42
+
43
+ `AGENTS.md` is the cross-tool standard, and Claude Code officially supports reading it
44
+ through a one-line `CLAUDE.md` import. To restructure a whole repo around that:
45
+
46
+ ```bash
47
+ npx ethlete-agents migrate --dry-run # prints the plan
48
+ npx ethlete-agents migrate
49
+ ```
50
+
51
+ - `CLAUDE.md` content moves to the top of `AGENTS.md`; `CLAUDE.md` becomes `@AGENTS.md`.
52
+ - Hand-written `.claude/skills/<name>` directories move to `.agents/skills/<name>`, with
53
+ a symlink left behind so Claude Code still finds them. (Symlinks need Developer Mode
54
+ on Windows checkouts.)
55
+ - A short layout note is added to `AGENTS.md` so agents know the symlinked skills are
56
+ the same files, not duplicates - skipped if your `AGENTS.md` already explains it.
57
+ - The config gains the `codex` target and `claudeMdImportsAgentsMd: true`, which stops
58
+ the claude target from writing `.claude/rules/ethlete/` - the rules already reach
59
+ Claude through the `AGENTS.md` marker block, and a second copy would load twice.
60
+ - A `sync` runs, which also prunes output from older layouts (`.agents/ethlete/`,
61
+ `.github/instructions/ethlete-*`).
62
+
63
+ The command is idempotent - every step detects the migrated state and skips itself.
37
64
 
38
65
  Every generated file carries a `DO NOT EDIT` banner. Files that disappear from the
39
66
  package are pruned on the next `sync`; nothing outside an `ethlete` directory or an
@@ -47,7 +74,6 @@ Prettier rewrites them and `check` then reports drift on every run:
47
74
  /.claude
48
75
  /.agents
49
76
  /.cursor/rules/ethlete-*
50
- /.github/instructions/ethlete-*
51
77
  ```
52
78
 
53
79
  ## Configuration
@@ -69,8 +95,9 @@ Prettier rewrites them and `check` then reports drift on every run:
69
95
  }
70
96
  ```
71
97
 
72
- - **`targets`** - `"auto"` (default) emits for every agent whose directory already
73
- exists, or list a subset of `claude`, `codex`, `cursor`, `copilot`.
98
+ - **`targets`** - `"auto"` (default) always emits `codex` (`AGENTS.md` plus
99
+ `.agents/skills/` is the cross-tool baseline) and adds `claude`, `cursor` or `copilot`
100
+ when their directory exists; or list an explicit subset.
74
101
  - **`profile`** - `"consumer"` (default) emits `scope: consumer` and `scope: both`
75
102
  content. `"sdk"` emits only `both`; the SDK repo uses it so its own hand-written,
76
103
  authoring-side guides are not overwritten by the consumer-side versions.
@@ -78,10 +105,65 @@ Prettier rewrites them and `check` then reports drift on every run:
78
105
  `content/defaults.json`; a guide whose variable has no default and no value is
79
106
  skipped with a warning rather than emitted with a dangling placeholder.
80
107
  - **`exclude`** - content names to skip entirely.
108
+ - **`claudeMdImportsAgentsMd`** - set (usually by `migrate`) when `CLAUDE.md` is an
109
+ `@AGENTS.md` import or symlink; the claude target then skips `.claude/rules/ethlete/`
110
+ so the rules don't load twice. `sync` warns when the flag is set but the import is
111
+ missing.
81
112
 
82
113
  Content that declares `requires` is only emitted when those packages are installed, so
83
114
  a repo without `@ethlete/query` never sees the query guide.
84
115
 
116
+ ## Hooks (opt-in)
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:
120
+
121
+ ```json
122
+ {
123
+ "hooks": ["context-warning"]
124
+ }
125
+ ```
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.
130
+
131
+ Available hooks:
132
+
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.
138
+
139
+ Hooks can be turned off per machine - see the local config below.
140
+
141
+ ## Per-machine local config
142
+
143
+ A gitignored `ethlete-agents.config.local.json` at the repo root holds the values that
144
+ differ per developer, without touching any committed file:
145
+
146
+ ```json
147
+ {
148
+ "disableHooks": true,
149
+ "sdkSourcePath": "/absolute/path/to/ethlete-sdk"
150
+ }
151
+ ```
152
+
153
+ - **`disableHooks`** - `true` disables every generated hook; an array
154
+ (`["context-warning"]`) just the named ones. The hook scripts read the file at
155
+ runtime, so toggling takes effect on the next prompt - no `sync` needed.
156
+ - **`sdkSourcePath`** - a local `ethlete-sdk` checkout. The `sdk-source` and
157
+ `sdk-local-build` skills read it when the agent needs the SDK's own sources, or has to
158
+ build the SDK and install it here through a `file:` dependency. A relative path is
159
+ resolved from the repo root.
160
+
161
+ Everything in this file is read at runtime, never by `sync`: the generated files stay
162
+ identical on every machine and in CI, which is what lets `check` diff them. That is also
163
+ why the file takes nothing beyond these keys - `sync`/`check` warn about unknown keys,
164
+ and about an `sdkSourcePath` that is missing or is not an SDK checkout. Add the filename
165
+ to your repo's `.gitignore`.
166
+
85
167
  ## Authoring content
86
168
 
87
169
  `content/rules/<name>.md` for short, always-loaded rules; `content/skills/<name>/SKILL.md`
@@ -94,7 +176,7 @@ description: Read before writing any color, background or border CSS.
94
176
  kind: skill # rule | skill
95
177
  scope: consumer # consumer | sdk | both
96
178
  requires: ['@ethlete/core'] # optional
97
- paths: ['**/*.css'] # optional; becomes Claude `paths`, Cursor `globs`, Copilot `applyTo`
179
+ paths: ['**/*.css'] # optional; becomes Claude `paths` and Cursor `globs` on rules
98
180
  vars: [docsBaseUrl] # optional
99
181
  ---
100
182
  ```
@@ -0,0 +1,215 @@
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 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.
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.
13
+
14
+ 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).
16
+
17
+ Can be disabled per machine via a gitignored ethlete-agents.config.local.json
18
+ at the repo root: {"disableHooks": true} or {"disableHooks": ["context-warning"]}.
19
+
20
+ Fail-safe: any error exits 0 with no output — the hook must never block a prompt.
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+ import tempfile
27
+
28
+ HOOK_NAME = "context-warning"
29
+ LOCAL_CONFIG_FILE = "ethlete-agents.config.local.json"
30
+
31
+ # Warn / critical fire at these fractions of the token budget.
32
+ WARN_FRACTION = 0.70
33
+ CRITICAL_FRACTION = 0.85
34
+
35
+ # On models with a window larger than this, tokens beyond it bill the whole
36
+ # context at the long-context premium rate — so the budget never exceeds it.
37
+ PREMIUM_BOUNDARY = 200_000
38
+
39
+ # Context window (tokens) per model, matched by substring against the model id
40
+ # from the transcript — first match wins. Edit these as model windows change;
41
+ # anything unmatched falls back to DEFAULT_WINDOW.
42
+ CONTEXT_WINDOWS = (
43
+ ("opus-5", 1_000_000),
44
+ ("opus-4-8", 1_000_000),
45
+ ("sonnet-4-5", 1_000_000),
46
+ ("sonnet-5", 1_000_000),
47
+ ("fable-5", 1_000_000),
48
+ # Generic fallbacks — keep these last: the first substring match wins, so a bare
49
+ # "opus"/"sonnet" entry placed above would shadow every versioned entry below it.
50
+ ("opus", 200_000),
51
+ ("sonnet", 200_000),
52
+ ("haiku", 200_000),
53
+ )
54
+ DEFAULT_WINDOW = 200_000
55
+
56
+
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")
60
+ if not root:
61
+ return False
62
+ try:
63
+ with open(os.path.join(root, LOCAL_CONFIG_FILE), encoding="utf-8") as f:
64
+ disabled = json.load(f).get("disableHooks")
65
+ except (OSError, ValueError, AttributeError):
66
+ return False
67
+ return disabled is True or (isinstance(disabled, list) and HOOK_NAME in disabled)
68
+
69
+
70
+ def window_for(model):
71
+ """Context window for a model id, by first substring match; DEFAULT_WINDOW otherwise."""
72
+ if model:
73
+ for needle, window in CONTEXT_WINDOWS:
74
+ if needle in model:
75
+ return window
76
+ return DEFAULT_WINDOW
77
+
78
+
79
+ def context_state(transcript_path):
80
+ """(tokens, model) from the last main-chain assistant message.
81
+
82
+ tokens ≈ its total input tokens (fresh + cache read + cache creation).
83
+ """
84
+ last_usage = None
85
+ last_model = None
86
+ with open(transcript_path, encoding="utf-8") as f:
87
+ for line in f:
88
+ try:
89
+ obj = json.loads(line)
90
+ except json.JSONDecodeError:
91
+ continue
92
+ if obj.get("type") != "assistant" or obj.get("isSidechain"):
93
+ continue
94
+ message = obj.get("message") or {}
95
+ usage = message.get("usage")
96
+ if usage:
97
+ last_usage = usage
98
+ last_model = message.get("model") or last_model
99
+ if not last_usage:
100
+ return 0, last_model
101
+ tokens = (
102
+ last_usage.get("input_tokens", 0)
103
+ + last_usage.get("cache_read_input_tokens", 0)
104
+ + last_usage.get("cache_creation_input_tokens", 0)
105
+ )
106
+ return tokens, last_model
107
+
108
+
109
+ def messages(tier, tokens, budget, priced):
110
+ """(systemMessage, additionalContext) for a tier.
111
+
112
+ priced: the budget is the pricing boundary, not the window — the reason to
113
+ hand off is cost, not an imminent auto-compact.
114
+ """
115
+ k = f"~{tokens // 1000}k"
116
+ pct = round(tokens / budget * 100)
117
+ 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.",
127
+ )
128
+ 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.",
136
+ )
137
+ if priced:
138
+ 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.",
147
+ )
148
+ 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.",
155
+ )
156
+
157
+
158
+ def main():
159
+ if disabled_locally():
160
+ return
161
+ data = json.load(sys.stdin)
162
+ transcript_path = data.get("transcript_path")
163
+ session_id = data.get("session_id", "unknown")
164
+ if not transcript_path or not os.path.isfile(transcript_path):
165
+ return
166
+
167
+ tokens, model = context_state(transcript_path)
168
+ window = window_for(model)
169
+ budget = min(window, PREMIUM_BOUNDARY)
170
+ warn_tokens = int(budget * WARN_FRACTION)
171
+ critical_tokens = int(budget * CRITICAL_FRACTION)
172
+ tier = 2 if tokens >= critical_tokens else 1 if tokens >= warn_tokens else 0
173
+
174
+ state_file = os.path.join(
175
+ tempfile.gettempdir(), f"claude-context-warning-{session_id}"
176
+ )
177
+ prev_tier = 0
178
+ try:
179
+ with open(state_file, encoding="utf-8") as f:
180
+ prev_tier = int(f.read().strip() or 0)
181
+ except (OSError, ValueError):
182
+ pass
183
+
184
+ if tier != prev_tier:
185
+ try:
186
+ with open(state_file, "w", encoding="utf-8") as f:
187
+ f.write(str(tier))
188
+ except OSError:
189
+ pass
190
+
191
+ if tier <= prev_tier:
192
+ return # already warned at this tier (or context shrank — state re-armed above)
193
+
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
+ )
207
+ )
208
+
209
+
210
+ if __name__ == "__main__":
211
+ try:
212
+ main()
213
+ except Exception:
214
+ pass
215
+ sys.exit(0)
@@ -68,3 +68,4 @@ So the table guide is `{%docsBaseUrl%}/components/table`, the menu guide
68
68
 
69
69
  - Data fetching has its own guide: {%skill:query%}
70
70
  - Theming tokens and how to register themes: {%skill:theming%}
71
+ - When the docs cannot answer it, read the SDK source: {%skill:sdk-source%}
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: sdk-local-build
3
+ description: Build the @ethlete SDK from a local ethlete-sdk checkout and install it into this repo through a `file:` dependency, so an unreleased SDK change can be tested against this app before it is published - and reverted cleanly afterwards. Read whenever an SDK-side fix needs verifying here, or when package.json already points at a local build.
4
+ kind: skill
5
+ scope: consumer
6
+ ---
7
+
8
+ # Testing an unreleased SDK build in this repo
9
+
10
+ Swapping an `@ethlete/*` dependency for a locally built one lets you verify an SDK
11
+ change against this app before it ships. It is a **temporary, local-only** state: the
12
+ `file:` specifier and the lockfile entry it produces must never be committed.
13
+
14
+ Prefer a published prerelease when one exists - installing `@ethlete/components@next`
15
+ is faster and reproducible for the whole team. Use a local build when the change is not
16
+ published yet, or when you are iterating on it.
17
+
18
+ ## 1. Prerequisites
19
+
20
+ - The checkout path comes from `sdkSourcePath` in `ethlete-agents.config.local.json`;
21
+ {%skill:sdk-source%} covers resolving it and what to check before trusting it.
22
+ - The checkout has its dependencies installed (`yarn install` in the checkout root - the
23
+ SDK repo is a Yarn 4 workspace).
24
+ - Note which branch it is on. Building `main` when your app runs `-next` prereleases
25
+ swaps in a completely different API surface.
26
+
27
+ ## 2. Build the libraries you changed
28
+
29
+ From the **checkout root**, one build per `@ethlete/*` package whose source you touched:
30
+
31
+ ```bash
32
+ npx nx build components # writes dist/libs/components
33
+ npx nx build query # writes dist/libs/query
34
+ ```
35
+
36
+ Each build also builds the libs it depends on (`types` → `core` → `query` →
37
+ `components`), so a single command is enough to produce a consistent set. Only the
38
+ packages you actually changed need to be installed here; leave the rest on their
39
+ published versions.
40
+
41
+ If a build stalls trying to reach Nx Cloud, re-run it with `NX_NO_CLOUD=true`.
42
+
43
+ ## 3. Point this repo at the build
44
+
45
+ Edit the version specifiers in `package.json` (the one declaring the dependency - in a
46
+ workspace that is the workspace package, not necessarily the root):
47
+
48
+ ```json
49
+ {
50
+ "dependencies": {
51
+ "@ethlete/components": "file:../ethlete-sdk/dist/libs/components"
52
+ }
53
+ }
54
+ ```
55
+
56
+ The path is resolved relative to that `package.json`; an absolute path works too. Then
57
+ install:
58
+
59
+ ```bash
60
+ yarn install
61
+ ```
62
+
63
+ Expect peer-dependency warnings - the built package pins peers to the SDK's own Angular
64
+ version. They are warnings, not failures; a real version conflict shows up as a build
65
+ error, and means the checkout is on the wrong branch.
66
+
67
+ ## 4. Confirm the local build is really what got installed
68
+
69
+ `yarn install` is the only thing that copies the build into `node_modules`, so verifying
70
+ is not optional - a stale package looks exactly like a change that did not work:
71
+
72
+ ```bash
73
+ grep -m1 '"version"' node_modules/@ethlete/components/package.json
74
+ rg -n "<a symbol from your change>" node_modules/@ethlete/components/fesm2022/
75
+ ```
76
+
77
+ Then **restart the dev server**. Bundlers pre-bundle dependencies and will keep serving
78
+ the old copy; if the change still does not show up, delete `.angular/cache` (and
79
+ `node_modules/.vite` if present) and start it again.
80
+
81
+ ## 5. Iterating
82
+
83
+ Every SDK edit needs the full loop - there is no watch mode across the boundary:
84
+
85
+ 1. rebuild in the checkout (`npx nx build <lib>`)
86
+ 2. `yarn install` here
87
+ 3. restart the dev server
88
+
89
+ Yarn 4 re-copies a `file:` dependency whenever its contents change, so step 2 does pick
90
+ up the rebuild. It also rewrites that package's `resolution` hash in `yarn.lock` on
91
+ every rebuild - which is one more reason the lockfile must not be committed in this
92
+ state. (With npm, `file:` symlinks instead of copying, so a rebuild is picked up without
93
+ reinstalling; the restart in step 3 is still required.)
94
+
95
+ ## 6. Clean up when you are done
96
+
97
+ Leaving a `file:` dependency behind breaks every other checkout and CI, because the path
98
+ does not exist there. Restore it as part of the same task, not later:
99
+
100
+ ```bash
101
+ git checkout package.json # or hand-restore the original version specifier
102
+ yarn install
103
+ git status --short # package.json and yarn.lock must both be clean
104
+ ```
105
+
106
+ Never commit a `file:` specifier or the lockfile it produced. If the verified fix is
107
+ still unreleased, say what has to be published (which lib, which version) instead of
108
+ shipping a local path.
109
+
110
+ ## Related
111
+
112
+ - Finding and reading the checkout: {%skill:sdk-source%}
113
+ - What the published packages document: {%skill:sdk-docs%}
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: sdk-source
3
+ description: How to read the @ethlete SDK's own source from a local ethlete-sdk checkout when the docs and the installed types are not enough. Read when you need an implementation detail, the exact behaviour behind a bug, or an API the docs do not cover - and never edit that checkout as part of work in this repo.
4
+ kind: skill
5
+ scope: consumer
6
+ vars: [docsBaseUrl]
7
+ ---
8
+
9
+ # Reading the @ethlete SDK source
10
+
11
+ The SDK is developed in a separate repository (`ethlete-sdk`). Most questions are
12
+ answered faster and more reliably by the documentation - read {%skill:sdk-docs%}
13
+ first. Reach for the source when the docs genuinely cannot answer the question:
14
+
15
+ - a behaviour looks like an SDK bug and you need to see what the code actually does
16
+ - you need an implementation detail the guides omit (event order, internal defaults,
17
+ which host directive writes which attribute)
18
+ - the installed version is ahead of - or behind - the published docs and you have to
19
+ confirm what the code in _this_ version does
20
+ - you are about to report or fix something in the SDK itself
21
+
22
+ ## 1. Resolve the checkout
23
+
24
+ The path is per machine, so it lives in the gitignored `ethlete-agents.config.local.json`
25
+ at the repo root:
26
+
27
+ ```json
28
+ {
29
+ "sdkSourcePath": "/absolute/path/to/ethlete-sdk"
30
+ }
31
+ ```
32
+
33
+ Read that file before searching anywhere. A relative path is resolved from the repo root.
34
+
35
+ If the file or the key is missing, **do not guess a path** and do not clone the
36
+ repository. Fall back to the installed package - `node_modules/@ethlete/<lib>/types/`
37
+ holds the full `.d.ts` surface of exactly the version this repo runs - and to
38
+ {%docsBaseUrl%}. Then tell the user a local checkout would help, and offer the snippet
39
+ above (the file is gitignored, so adding it changes nothing for anyone else).
40
+
41
+ ## 2. Check the checkout matches what is installed
42
+
43
+ A checkout sits on whatever branch the developer left it on, so it can be months of
44
+ work ahead of the installed package - or behind it. Compare before you trust it:
45
+
46
+ ```bash
47
+ grep '"@ethlete/' package.json # what this repo runs
48
+ grep -m1 '"version"' <sdkSourcePath>/libs/<lib>/package.json # what the checkout is at
49
+ git -C <sdkSourcePath> status -sb # branch, and whether it is dirty
50
+ ```
51
+
52
+ Rules when they differ:
53
+
54
+ - **The installed package wins** for anything about how this repo behaves today. The
55
+ `.d.ts` in `node_modules` is the truth about the API you are calling.
56
+ - Source that is ahead describes an **unreleased** API. Never write consumer code
57
+ against it, and never assume it is available - say what release it needs.
58
+ - A dirty checkout may contain someone's work in progress. Say so rather than quoting
59
+ it as SDK behaviour.
60
+
61
+ ## 3. Where things live
62
+
63
+ Paths are relative to the checkout root:
64
+
65
+ | Path | What is in it |
66
+ | ----------------------------------- | --------------------------------------------------------------------------- |
67
+ | `libs/components/src/lib/<domain>/` | The active UI library, one folder per domain (`button`, `menu`, `table`, …) |
68
+ | `libs/core/src/lib/` | Framework primitives: directives, signal utils, overlay runtime, theming |
69
+ | `libs/query/src/lib/` | Data fetching: `http`, `gql`, `ws`, auth, query-form |
70
+ | `libs/types/src/lib/` | Shared types |
71
+ | `libs/cdk/` | The predecessor UI toolkit, maintenance mode - only for code still on it |
72
+ | `libs/eslint-plugin/src/` | The lint rules, including the message text explaining each one |
73
+ | `apps/docs/` | The markdown behind {%docsBaseUrl%} |
74
+ | `apps/playground/` | The Storybook app - stories also live next to each component |
75
+
76
+ Inside a component domain: `<name>.component.ts` with its `.css` next to it,
77
+ `<name>.imports.ts` (the imports array to spread into a consumer component),
78
+ `headless/` for the unstyled directives, `stories/` for the Storybook stories, and
79
+ `index.ts` as the barrel. The lib's public surface is `libs/<lib>/src/index.ts` -
80
+ anything not re-exported from there is internal, whatever it looks like.
81
+
82
+ ## 4. Search it, don't read it whole
83
+
84
+ ```bash
85
+ rg -n "etButton" <sdkSourcePath>/libs/components/src --glob '!*.spec.ts' # a selector
86
+ rg -n "export const OVERLAY" <sdkSourcePath>/libs/core/src # an export
87
+ rg -n "menu" <sdkSourcePath>/apps/docs/components # the guide source
88
+ ```
89
+
90
+ Specs are the cheapest description of intended behaviour - `<name>.component.spec.ts`
91
+ next to a component usually answers "is this supposed to happen?" faster than the
92
+ implementation does.
93
+
94
+ ## 5. The checkout is read-only from here
95
+
96
+ It is a different repository with its own branch, lint, docs and changeset workflow.
97
+ Never edit it while working on a task in this repo, and never copy its internals into
98
+ consumer code - a private helper is not a supported API and disappears without a major
99
+ version (the same goes for anything under a `subtle` namespace).
100
+
101
+ When the fix belongs in the SDK, say so and describe it precisely: file, symbol, and
102
+ the behaviour it should have. If the user wants that fix verified against this app
103
+ before it ships, that is {%skill:sdk-local-build%}.
104
+
105
+ ## Related
106
+
107
+ - Docs and Storybook, which come first: {%skill:sdk-docs%}
108
+ - Testing an unreleased SDK build here: {%skill:sdk-local-build%}
@@ -463,7 +463,7 @@ settings-form/
463
463
  - **Do not** create a changeset for irrelevant changes (e.g., formatting, comments, internal refactoring).
464
464
  - **Do not** create a changeset for fixes to features that have not yet been released.
465
465
  - **Do not** include multiple changes in a single changeset. Each changeset should contain only one change.
466
- - **Always** start a changeset with at least one sentence describing the change. Optional follow-up markdown can be added after the initial sentence.
466
+ - **A changeset note is a TL;DR: one sentence, two at most, under 40 words.** Never a second paragraph, no matter how much work the change took. It is the line a consumer skims to decide whether the release affects them - not a summary of your work. Mechanism, API inventories, caveats and rationale belong in the docs or the commit body, never here.
467
467
  - Create changesets for dependency updates if they are relevant to the project (e.g., major version updates).
468
468
  - Write changesets in the imperative mood. For example:
469
469
  - Add button component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/agent-rules",
3
- "version": "0.1.0-next.0",
3
+ "version": "0.1.0-next.2",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",