@ethlete/agent-rules 0.1.0-next.10 → 0.1.0-next.12

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 (49) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +23 -13
  3. package/content/hooks/context-warning.py +25 -6
  4. package/content/rules/lint-and-format.md +5 -4
  5. package/content/rules/reactive-state.md +1 -1
  6. package/content/rules/styling.md +8 -6
  7. package/content/skills/angular-patterns/SKILL.md +6 -6
  8. package/content/skills/api-source/SKILL.md +28 -19
  9. package/content/skills/figma-export/SKILL.md +16 -18
  10. package/content/skills/git-flow/SKILL.md +2 -2
  11. package/content/skills/handoff/SKILL.md +4 -4
  12. package/content/skills/query/SKILL.md +23 -12
  13. package/content/skills/rxjs-signals/SKILL.md +9 -3
  14. package/content/skills/sdk-docs/SKILL.md +6 -5
  15. package/content/skills/sdk-local-build/SKILL.md +25 -7
  16. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  17. package/content/skills/sdk-source/SKILL.md +32 -32
  18. package/content/skills/story-styling/SKILL.md +3 -2
  19. package/content/skills/styleguide/SKILL.md +11 -6
  20. package/content/skills/styleguide/assets.md +54 -0
  21. package/content/skills/styleguide/changesets.md +17 -0
  22. package/content/skills/styleguide/file-structure.md +73 -0
  23. package/content/skills/styleguide/lint-rule-lookup.md +50 -0
  24. package/content/skills/styleguide/storybook-structure.md +25 -0
  25. package/content/skills/theming/SKILL.md +24 -17
  26. package/content/skills/timetrack/SKILL.md +1 -1
  27. package/package.json +1 -1
  28. package/src/lib/config.d.ts +3 -0
  29. package/src/lib/config.js +1 -0
  30. package/src/lib/config.js.map +1 -1
  31. package/src/lib/plan.d.ts +6 -5
  32. package/src/lib/plan.js +75 -12
  33. package/src/lib/plan.js.map +1 -1
  34. package/src/lib/sync.js +1 -4
  35. package/src/lib/sync.js.map +1 -1
  36. package/src/lib/targets/agents-skills.js +1 -1
  37. package/src/lib/targets/agents-skills.js.map +1 -1
  38. package/src/lib/targets/claude.js +0 -1
  39. package/src/lib/targets/claude.js.map +1 -1
  40. package/src/lib/targets/codex.js +2 -2
  41. package/src/lib/targets/codex.js.map +1 -1
  42. package/src/lib/targets/copilot.js +1 -1
  43. package/src/lib/targets/copilot.js.map +1 -1
  44. package/src/lib/targets/cursor.js +1 -1
  45. package/src/lib/targets/cursor.js.map +1 -1
  46. package/src/lib/targets/shared.d.ts +1 -8
  47. package/src/lib/targets/shared.js +2 -7
  48. package/src/lib/targets/shared.js.map +1 -1
  49. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @ethlete/agent-rules
2
2
 
3
+ ## 0.1.0-next.12
4
+
5
+ ### Patch Changes
6
+
7
+ - [#3069](https://github.com/ethlete-io/ethdk/pull/3069) [`a258308`](https://github.com/ethlete-io/ethdk/commit/a258308e0753eb65d08b3d7fb9a9f14507229047) Thanks [@github-actions](https://github.com/apps/github-actions)! - Agent rules: make generated guidance safer, reference-aware, example-validated, and cheaper to load.
8
+
9
+ - [#3069](https://github.com/ethlete-io/ethdk/pull/3069) [`973462e`](https://github.com/ethlete-io/ethdk/commit/973462ebb9ec7c2eef1aca3fc9f01615824d0074) Thanks [@github-actions](https://github.com/apps/github-actions)! - Agent rules: warn about unknown exclusions and apply Codex model-specific long-context pricing limits in the context warning hook.
10
+
11
+ ## 0.1.0-next.11
12
+
13
+ ### Patch Changes
14
+
15
+ - [`16ae17e`](https://github.com/ethlete-io/ethdk/commit/16ae17e30238ef4539f3e168ca8299a4546ac292) Thanks [@TomTomB](https://github.com/TomTomB)! - The theming skill now states that a component which tints with `--et-theme-color-primary-opacity`
16
+ must compose the color itself, and that an `@property` `initial-value` cannot use a font-relative or
17
+ container unit.
18
+
3
19
  ## 0.1.0-next.10
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -111,7 +111,10 @@ Prettier rewrites them and `check` then reports drift on every run:
111
111
  convention and never mentions a `commitlint` run - an agent that goes looking for a
112
112
  promised validator and finds nothing reports the discrepancy instead of just
113
113
  committing. Setting either one in `vars` overrides the detection.
114
- - **`exclude`** - content names to skip entirely.
114
+ - **`exclude`** - rule or skill names to skip entirely for every configured agent and
115
+ developer. For example, `"exclude": ["git-flow", "handoff"]` prevents those skills
116
+ from being generated; the next `sync` also removes copies generated previously. Unknown
117
+ names produce a warning so a typo cannot silently leave a skill enabled.
115
118
  - **`claudeMdImportsAgentsMd`** - set (usually by `migrate`) when `CLAUDE.md` is an
116
119
  `@AGENTS.md` import or symlink; the claude target then skips `.claude/rules/ethlete/`
117
120
  so the rules don't load twice. `sync` warns when the flag is set but the import is
@@ -298,9 +301,9 @@ Available hooks:
298
301
  context crosses 70% / 85% of the token budget, recommending a handoff. Under Claude the
299
302
  budget is capped at the 200k long-context pricing boundary: on 1M-window models every
300
303
  request past 200k input tokens bills the whole context at a premium rate, so the
301
- warnings fire at ~140k/~170k instead of deep into the expensive range. Codex has no
302
- such boundary, so its budget is the model's own reported context window and the
303
- warnings are pure occupancy.
304
+ warnings fire at ~140k/~170k instead of deep into the expensive range. Codex uses
305
+ the model-specific 272k pricing boundary for GPT-5.6, GPT-5.5 and GPT-5.4, and the
306
+ rollout's reported context window for models without that pricing rule.
304
307
 
305
308
  Two things are Claude-only: the separate user-facing line (Codex documents only
306
309
  `additionalContext`, so there the warning is folded into the text the model is told to
@@ -397,7 +400,8 @@ differ per developer, without touching any committed file:
397
400
  {
398
401
  "disableHooks": true,
399
402
  "sdkSourcePath": "/absolute/path/to/ethlete-sdk",
400
- "apiRepoPaths": { "hub": "../fut-hub-backend" }
403
+ "apiRepoPaths": { "hub": "../fut-hub-backend", "*": "../shared-backend" },
404
+ "apiRepoBranches": { "hub": "develop", "*": "main" }
401
405
  }
402
406
  ```
403
407
 
@@ -414,15 +418,18 @@ differ per developer, without touching any committed file:
414
418
  - **`apiRepoPaths`** - one checkout per app, keyed by the app's project name
415
419
  (`{ "hub": "../fut-hub-backend" }`). The `api-source` skill reads it to confirm a
416
420
  response shape, a status code or an enum in the API's own source instead of guessing it
417
- from the client. Relative paths resolve from the repo root; a map with a single entry is
418
- used for whatever app is in play.
421
+ from the client. Relative paths resolve from the repo root. Matching is exact; use the
422
+ explicit `"*"` key only when apps intentionally share a checkout.
423
+ - **`apiRepoBranches`** - the expected backend branch per app. It uses the same exact-key
424
+ and explicit `"*"` fallback rules. Omit it when branch identity is not part of the
425
+ environment contract; the source guide then treats the current branch as context.
419
426
 
420
427
  Everything in this file is read at runtime, never by `sync`: the generated files stay
421
428
  identical on every machine and in CI, which is what lets `check` diff them. That is also
422
429
  why the file takes nothing beyond these keys - `sync`/`check` warn about unknown keys,
423
430
  about an `sdkSourcePath` that is missing or is not an SDK checkout, and about an
424
- `apiRepoPaths` entry that is not a directory. Add the filename to your repo's
425
- `.gitignore`.
431
+ `apiRepoPaths` entry that is not a directory, and invalid `apiRepoBranches` values. Add
432
+ the filename to your repo's `.gitignore`.
426
433
 
427
434
  ## Authoring content
428
435
 
@@ -447,7 +454,10 @@ compile: it is a Claude Code output style verbatim, so its frontmatter is Claude
447
454
  as it is, apart from a marker line that records where it came from.
448
455
 
449
456
  In a body, `{% varName %}` substitutes a variable, `{% skill:other-name %}` links to
450
- another guide the way the current target expects, and `{% resource:file.mjs %}` links to
451
- a bundled file. The delimiter is `{% … %}`, not `{{ … }}`, so Angular templates in
452
- examples pass through untouched. Resource files get variable substitution too, but no
453
- links.
457
+ another emitted package guide, and `{% resource:file.mjs %}` links to a bundled file.
458
+ Skill links are required dependencies: after scope, package, variable, and exclusion
459
+ filtering, `sync` and `check` fail with the source and missing target instead of emitting
460
+ a dangling name. Optional guidance must be self-contained when its guide is absent. Use
461
+ the structured marker instead of a plain `` `name` skill`` reference so validation can
462
+ see it. The delimiter is `{% … %}`, not `{{ … }}`, so Angular templates in examples pass
463
+ through untouched. Resource files get variable substitution too, but no links.
@@ -16,11 +16,10 @@ that matter here, all captured in AGENT_PROFILES:
16
16
  carry a TokenUsageInfo. Codex's rollout format is explicitly not a stable
17
17
  interface, so the parser searches each line for the usage object instead of
18
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.
19
+ * Context budget. Claude's budget is capped at its 200k long-context pricing
20
+ boundary. Codex uses the model-specific pricing boundary where OpenAI
21
+ documents one, and the reported window otherwise. Crossing either boundary
22
+ costs far more than handing off into a fresh session ever would.
24
23
  * How a handoff is invoked. Claude has a slash command and /clear; Codex has
25
24
  neither and reads the skill from disk. The skill's own name differs per repo
26
25
  (`ethlete-handoff` where the generator installed it, `handoff` where the repo
@@ -63,6 +62,15 @@ CRITICAL_FRACTION = 0.85
63
62
  # context at the long-context premium rate — so the budget never exceeds it.
64
63
  PREMIUM_BOUNDARY = 200_000
65
64
 
65
+ # Codex models whose long-context pricing starts above 272k input tokens. Models
66
+ # without that pricing rule use their reported context window instead.
67
+ CODEX_PREMIUM_BOUNDARIES = (
68
+ ("gpt-5.6", 272_000),
69
+ ("gpt-5.5", 272_000),
70
+ ("gpt-5.4-mini", None),
71
+ ("gpt-5.4", 272_000),
72
+ )
73
+
66
74
  # Context window (tokens) per model, matched by substring against the model id
67
75
  # from the transcript — first match wins. Edit these as model windows change;
68
76
  # anything unmatched falls back to DEFAULT_WINDOW.
@@ -83,6 +91,7 @@ DEFAULT_WINDOW = 200_000
83
91
  AGENT_PROFILES = {
84
92
  "claude": {
85
93
  "premium_boundary": PREMIUM_BOUNDARY,
94
+ "premium_boundaries": (),
86
95
  # Claude's transcript reports no window, so it is resolved from the model id.
87
96
  "default_window": None,
88
97
  "auto_modes": ("auto",),
@@ -101,6 +110,7 @@ AGENT_PROFILES = {
101
110
  },
102
111
  "codex": {
103
112
  "premium_boundary": None,
113
+ "premium_boundaries": CODEX_PREMIUM_BOUNDARIES,
104
114
  # Only reached if a rollout omits model_context_window; the CONTEXT_WINDOWS table
105
115
  # holds Claude model ids and would never match a Codex one.
106
116
  "default_window": 272_000,
@@ -214,6 +224,15 @@ def window_for(model):
214
224
  return DEFAULT_WINDOW
215
225
 
216
226
 
227
+ def premium_boundary_for(profile, model):
228
+ """Model-specific pricing boundary, falling back to the agent-wide value."""
229
+ if model:
230
+ for needle, boundary in profile["premium_boundaries"]:
231
+ if needle in model:
232
+ return boundary
233
+ return profile["premium_boundary"]
234
+
235
+
217
236
  def claude_context_state(transcript_path):
218
237
  """(tokens, model, window) from the last main-chain assistant message.
219
238
 
@@ -376,7 +395,7 @@ def main():
376
395
 
377
396
  tokens, model, reported_window = CONTEXT_READERS[agent](transcript_path)
378
397
  window = reported_window or profile["default_window"] or window_for(model)
379
- boundary = profile["premium_boundary"]
398
+ boundary = premium_boundary_for(profile, model)
380
399
  budget = min(window, boundary) if boundary else window
381
400
  warn_tokens = int(budget * WARN_FRACTION)
382
401
  critical_tokens = int(budget * CRITICAL_FRACTION)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lint-and-format
3
- description: Run lint with --fix before fixing anything by hand, and format every edited file.
3
+ description: Run lint with --fix before fixing anything by hand, and format every edited source file Prettier supports.
4
4
  kind: rule
5
5
  scope: both
6
6
  vars: [lintCommand, lintFixCommand, formatCommand]
@@ -16,10 +16,11 @@ so let them do the work before correcting anything by hand:
16
16
  {%lintCommand%} # then re-run to see what needs a manual fix
17
17
  ```
18
18
 
19
- For the judgment calls lint cannot enforce — signals vs RxJS, templates, lifecycle and DI
20
- patterns — see {%skill:styleguide%}.
19
+ For judgment calls lint cannot enforce, load the repository's focused guidance for the
20
+ code you are changing.
21
21
 
22
- After editing any file, format it before wrapping up:
22
+ Format every edited Prettier-supported source file before wrapping up. Do not send binary,
23
+ generated, or unsupported files to this command:
23
24
 
24
25
  ```bash
25
26
  {%formatCommand%}
@@ -12,4 +12,4 @@ scope: both
12
12
  - **Bridge, don't copy.** Cross the boundary with `toSignal()` / `toObservable()`, never by
13
13
  `.subscribe()`-ing and assigning the value somewhere.
14
14
 
15
- Subscriptions, effects, and the traps in each direction: {%skill:rxjs-signals%}.
15
+ For subscriptions and effects, load the repository's focused reactive-state guidance.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: styling
3
- description: Component CSS is plain CSS in @layer components, and every colour comes from a theme token.
3
+ description: Component CSS is plain CSS in @layer components, and hardcoded colours are never primary values.
4
4
  kind: rule
5
5
  scope: both
6
6
  requires: ['@ethlete/core']
@@ -26,10 +26,12 @@ utility can win.
26
26
  rule, so source order decides. Leave interaction states (`:hover`, `:focus-visible`,
27
27
  `:active`) bare so they escalate and win.
28
28
 
29
- **Never hardcode a colour.** Backgrounds, text, borders and interaction states all resolve
30
- from the surface and colour theming tokens (`--et-surface-*-solid`, `--et-theme-color-*`) —
31
- see {%skill:theming%}.
29
+ **Never use a hardcoded colour as the primary value.** Backgrounds, text, borders and
30
+ interaction states resolve from the surface and colour theming tokens
31
+ (`--et-surface-*-solid`, `--et-theme-color-*`). A static fallback inside
32
+ `var(--token, <fallback>)` is permitted, but not required.
32
33
 
33
34
  Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by the application;
34
- the SDK ships none. Never hardcode a theme-name union in a type, a doc or an example —
35
- semantic colours resolve by theme `type` (e.g. `injectErrorTheme()`).
35
+ the SDK ships none. Never hardcode them as an SDK-defined union or reusable API contract.
36
+ If an app-specific example names one, label it as belonging to that app. Semantic colours
37
+ resolve by theme `type` (e.g. `injectErrorTheme()`).
@@ -22,10 +22,10 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
22
22
 
23
23
  ```html
24
24
  <!-- ❌ runs every CD cycle -->
25
- <button [disabled]="isDisabled()">
26
- <!-- ✅ computed signal -->
27
- <button [disabled]="disabled()"></button>
28
- </button>
25
+ <button [disabled]="isDisabled()"></button>
26
+
27
+ <!-- ✅ computed signal -->
28
+ <button [disabled]="disabled()"></button>
29
29
  ```
30
30
 
31
31
  ## Lifecycle
@@ -51,8 +51,8 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
51
51
 
52
52
  - Inline template/styles for small components; external `.html` / `.css` files
53
53
  for complex ones.
54
- - Component CSS is plain CSS wrapped in `@layer components`, with every colour coming
55
- from a theme token — see {%skill:theming%}.
54
+ - Component CSS is plain CSS wrapped in `@layer components`, with every primary colour
55
+ value coming from the repository's theme tokens.
56
56
 
57
57
  ## Reactive state
58
58
 
@@ -31,7 +31,12 @@ app to checkout. They are per machine, so the map lives in the gitignored
31
31
  ```json
32
32
  {
33
33
  "apiRepoPaths": {
34
- "hub": "../fut-hub-backend"
34
+ "hub": "../fut-hub-backend",
35
+ "*": "../shared-backend"
36
+ },
37
+ "apiRepoBranches": {
38
+ "hub": "develop",
39
+ "*": "main"
35
40
  }
36
41
  }
37
42
  ```
@@ -41,8 +46,8 @@ Read that file before searching anywhere. The rules:
41
46
  - **The key is the app** as this repo names it - the workspace project name, which is
42
47
  normally also the folder under `apps/`. Match the app you are working in.
43
48
  - **A relative path resolves from the repo root**, not from the app folder.
44
- - **One entry means one API.** If the map holds a single entry, use it whatever it is
45
- called.
49
+ - **Require an exact app key.** If several apps intentionally share one API, configure
50
+ the explicit `"*"` fallback. Never treat an unrelated single entry as a fallback.
46
51
  - **No matching entry - stop and ask.** Do not guess a sibling folder and do not clone
47
52
  the repository. Say which app you needed the API for and offer the snippet above; the
48
53
  file is gitignored, so adding it changes nothing for anyone else.
@@ -51,28 +56,32 @@ Without a checkout, fall back to what the running API tells you: the generated A
51
56
  description if the project serves one (`/openapi.json`, `/swagger`, `/api/doc`), and the
52
57
  real response body of the call you are debugging.
53
58
 
54
- ## 2. Check the branch before you read anything
59
+ ## 2. Resolve the relevant files before judging checkout state
55
60
 
56
- A checkout sits on whatever branch its developer left it on, and the code you would
57
- quote must be the code that serves this app:
61
+ Search by the route, field, or error you already know. Record the contract, serializer,
62
+ handler, and tests that can answer the question. Then inspect checkout state only for
63
+ those paths:
58
64
 
59
65
  ```bash
60
- git -C <apiRepoPath> fetch --quiet # read-only, safe
61
- git -C <apiRepoPath> status -sb # branch, ahead/behind, dirty files
66
+ git -C <apiRepoPath> status -sb
67
+ git -C <apiRepoPath> status --short -- <relevant-paths>
68
+ git -C <apiRepoPath> diff -- <relevant-paths>
62
69
  ```
63
70
 
64
- **The state to expect is the API's own development branch, up to date with its remote.**
65
- Anything else, and you are describing a different API than the one the app calls.
71
+ Unrelated dirty files are not evidence about these paths. Continue without blocking on
72
+ them. If a relevant file is dirty, distinguish the worktree implementation from the
73
+ committed or deployed behavior. Ask only when that difference changes the answer or the
74
+ user has to choose which behavior matters.
66
75
 
67
- When it is not in that state, **say so and ask** - never switch, pull, stash or reset it
68
- yourself:
76
+ `apiRepoBranches` configures the expected branch per exact app key, with `"*"` as the
77
+ only fallback. If no branch is configured, report the current branch as context; do not
78
+ invent a blocking “development branch” requirement. A different or ahead branch matters
79
+ only when the relevant files differ in that commit range.
69
80
 
70
- - **On another branch** - name it and ask. A feature branch may be exactly the endpoint
71
- you were sent to look at, but it is not what the app talks to today.
72
- - **Behind its remote** - report how far. The behaviour you are about to call a bug may
73
- already be fixed upstream.
74
- - **Dirty** - it holds someone's work in progress. Say so rather than quoting it as API
75
- behaviour.
81
+ Use existing remote refs first. `git fetch` preserves the worktree but is not read-only:
82
+ it uses the network and mutates remote refs. Fetch only when freshness is material, and
83
+ request any approval the environment requires. Never switch, pull, stash, or reset the
84
+ checkout yourself.
76
85
 
77
86
  ## 3. The checkout is not the environment the app calls
78
87
 
@@ -111,7 +120,7 @@ they name the status code and the body for each case.
111
120
 
112
121
  It is a different repository with its own branch, review and release process. Never edit
113
122
  it while working on a task in this repo, and never change its git state without being
114
- asked (`fetch` is fine).
123
+ asked. A fetch also needs to meet the freshness and approval conditions above.
115
124
 
116
125
  When the fix belongs in the API, say so precisely: the endpoint, the field, and the
117
126
  behaviour it should have. Then handle the API as it is today - a client workaround for a
@@ -15,18 +15,15 @@ Three things arrive from Figma, and each answers a different question:
15
15
  | **`.css`** (Copy as CSS) | Named layers, auto-layout properties, typography metrics, design-token names | Any hierarchy at all — the dump is flat |
16
16
  | **`.png`** (a screenshot) | Figma's own blue measurement overlays, and what the designer chose to frame | Nothing machine-readable |
17
17
 
18
- **Ask for the `.svg` and the `.css` together, and do not start until you have both.** The two
19
- are complements, not alternatives: the SVG is the only export you can both look at and
20
- measure, and the CSS is the only one that names layers and records type. A PNG earns its place
21
- only when it is a _screenshot_ carrying dev-mode annotations — a PNG _render_ of the same frame
22
- adds nothing the SVG does not. None of the three tells you the colours.
23
-
24
- Say what you are missing and what it would settle, in one line — "I have the SVG; the `.css`
25
- export would give me the font sizes and whether these cards Hug or Fill" — and wait. Every
26
- number in your diff has to trace back to something in an export or to a token; a plausible
27
- `17px` you inferred from a 12px outlined glyph is worse than an open question, because it
28
- survives review as though it had been specified. If the pair genuinely cannot be produced, say
29
- in the write-up which numbers are therefore guesses.
18
+ **Ask for the `.svg` and the `.css` together.** That pair is the preferred complete input:
19
+ the SVG is the export you can look at and measure, and the CSS names layers and records
20
+ type. A PNG earns its place only when it is a _screenshot_ carrying dev-mode annotations.
21
+ Exports expose rendered color values, but they do not identify the authoritative semantic
22
+ theme token.
23
+
24
+ If one artifact is missing, inspect the existing code and the artifact you do have. List
25
+ the exact facts that remain unknown and do not invent their measurements. Ask before a
26
+ structural choice or an untraceable numeric value, not before useful read-only inspection.
30
27
 
31
28
  ## 1. Read the export before touching code
32
29
 
@@ -126,8 +123,8 @@ it; the SVG has the tree but no names. The image is what disambiguates:
126
123
  - **Authoritative:** geometry and typography metrics — widths, padding, gaps, `flex-grow`,
127
124
  border radius, font size / weight / line-height / letter-spacing, and the breakpoints at
128
125
  which the layout changes.
129
- - **Never authoritative: colour.** Backgrounds, text, borders and interaction states resolve
130
- from the surface and colour theming tokens — see {%skill:theming%}. A hex in the
126
+ - **Never authoritative: semantic colour choice.** Backgrounds, text, borders and
127
+ interaction states resolve from the repository's theme tokens. A hex in the
131
128
  export is information about the _designer's_ palette, not a value to paste. Where the
132
129
  export's colour and the token disagree, keep the token and note the delta for the design
133
130
  review; the export can be wrong about contrast in a way the tokens are not. This holds
@@ -181,13 +178,14 @@ Harness gotchas, each of which will cost you an hour:
181
178
  ~1px wide of reality, so a label that wraps in the harness may well fit in the app. Check
182
179
  before calling a wrap a defect.
183
180
  - Playwright is CommonJS and unresolvable from a scratch directory — `createRequire` against
184
- the repo root, as the template does. The same applies when driving a story:
185
- {%skill:verify-in-storybook%}.
181
+ the repo root, as the template does. When driving a story, follow the repository's
182
+ installed Storybook verification guidance if present.
186
183
 
187
184
  ## 5. Close the loop
188
185
 
189
186
  Report the measured numbers next to the export's, per width — not "matches the design". Say
190
187
  explicitly which parts of the export you deliberately did **not** implement and why (colour
191
188
  kept as tokens, a label the product decided never to render, a field the API lacks). Then
192
- delete the export files once their component is signed off, so the folder always shows only
193
- what is still outstanding.
189
+ report that the export files are no longer needed. Delete only artifacts this workflow
190
+ created and only when the workflow or user explicitly authorizes cleanup; otherwise let the
191
+ user decide whether supplied exports remain useful records.
@@ -27,7 +27,7 @@ npx ethlete-agents git-flow repair <branch> # rename a non-conforming one,
27
27
  its parent Story's branch, so that branch has to exist first.
28
28
 
29
29
  It reads the issue through the running Timetrack app, which holds this machine's only Jira
30
- credentials - see the `timetrack` skill. If it reports that the app is not running, ask the
30
+ credentials. If it reports that the app is not running, ask the
31
31
  user to start it. `--subject <text>` names a branch without reading Jira at all.
32
32
 
33
33
  ## The five shapes
@@ -84,4 +84,4 @@ legitimately target it. Leave a live one alone.
84
84
  ## Commit messages are a separate thing
85
85
 
86
86
  Commits stay conventional (`feat(platform): Prefer a player's common name`) and carry **no
87
- issue key** - the branch already has it. See {%skill:git-commit%}.
87
+ issue key** - the branch already has it. Follow the repository's commit-message guidance.
@@ -94,9 +94,9 @@ resume from it.
94
94
  3. Verify reality still matches: current branch, `git status`, last commit. If
95
95
  they diverge from the handoff (e.g. someone committed in between), say what
96
96
  changed and adapt - the repo is the truth, the handoff is the guide.
97
- 4. Read any guides the handoff's work obviously needs (e.g. {%skill:theming%}
98
- before CSS work) - same rules as always.
97
+ 4. Read any focused repository guidance the handoff's work needs - same rules as always.
99
98
  5. Continue with the **Next steps** section. Don't redo work listed under
100
99
  _Done_; don't re-open questions under _Decisions_.
101
- 6. When every next step is complete (including changeset/docs follow-ups),
102
- delete the handoff file so the directory only contains live handoffs.
100
+ 6. When every next step is complete, decide whether the handoff can be removed. Delete it
101
+ only if this workflow created it and the save workflow or user authorized cleanup.
102
+ Otherwise report that it is no longer needed and let the user decide.
@@ -97,23 +97,34 @@ requests; debounce at the input if you need it.
97
97
 
98
98
  ## Bridging a query into RxJS / other APIs
99
99
 
100
- To hand a query's results to something that wants an `Observable<T[]>` (e.g. a
101
- `(query) => Observable<...>` source): drive the query by a search signal and return
102
- its response stream.
100
+ For a callback that must start one request and return one correlated result, use a
101
+ manual query with `executeUntilSettled()`. Its frozen snapshot cannot be replaced by a
102
+ later execution, and the observable completes after that one result.
103
103
 
104
104
  ```ts
105
- private search = signal('');
106
- private q = getItems(withArgs(() => ({ queryParams: { q: this.search() } })));
107
-
108
- fetch(query: string) {
109
- this.search.set(query);
110
- return this.q.response.asObservable().pipe(
111
- filter((r): r is ItemsRes => r !== null),
112
- map((r) => r.items),
113
- );
105
+ class ItemSource {
106
+ private itemsQuery = getItems({ onlyManualExecution: true });
107
+
108
+ fetch(query: string) {
109
+ return defer(() => executeUntilSettled(this.itemsQuery, { args: { queryParams: { q: query } } })).pipe(
110
+ map((snapshot) => {
111
+ const response = snapshot.response();
112
+
113
+ if (response === null) throw snapshot.error();
114
+
115
+ return response.items;
116
+ }),
117
+ );
118
+ }
114
119
  }
115
120
  ```
116
121
 
122
+ Do not set a search signal and immediately return the shared `response` stream: the
123
+ previous response is retained during re-execution and can be the first non-null emission.
124
+ Unsubscribing from the wrapper stops result delivery but does not by itself abort the
125
+ promise-backed execution; use the query's reactive `withArgs` lifecycle when cancellation
126
+ is a requirement rather than a callback contract.
127
+
117
128
  ## Gotchas
118
129
 
119
130
  - Signals-first: read `query.response()` in templates/computeds; it's **nullable**
@@ -34,9 +34,15 @@ const data = toSignal(obs$);
34
34
 
35
35
  ## Using RxJS correctly
36
36
 
37
- - **Always unsubscribe.** Prefer `takeUntilDestroyed()` (needs an injection
38
- context); otherwise `take` / `takeUntil` / `takeWhile`, or store and call
39
- `.unsubscribe()`. Place the limiting operator **last** in the pipe.
37
+ - **Tear down long-lived or manual subscriptions.** Finite streams that complete on their
38
+ own need no artificial lifecycle operator. For Angular lifecycle cleanup, prefer
39
+ `takeUntilDestroyed()` (it needs an injection context) or explicitly unsubscribe.
40
+ Do not use `takeWhile` as destruction cleanup: without another emission it stays
41
+ subscribed. Use `take(1)` or `first()` only when one emission is the operation's
42
+ intended semantics.
43
+ - **Place lifecycle teardown after higher-order operators** such as `switchMap`, so their
44
+ inner subscriptions are also covered. Other limiting and finalization operators do not
45
+ have a universal “last” position; place them where their semantics belong.
40
46
  - **Side effects go in `tap()`**, never in the `subscribe()` callback - keep
41
47
  `subscribe()` empty.
42
48
  - **Don't reach for RxJS inside `effect()`/`computed()`.** Subscribing per run
@@ -28,7 +28,7 @@ Page URLs follow `{%docsBaseUrl%}/<lib>/<topic>`. The library sections:
28
28
  | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
29
29
  | `/components/` | The active UI library - one guide per domain (see the list below) |
30
30
  | `/core/` | Framework primitives: `theming`, `overlay-runtime`, `signal-utils`, `element-signals`, `animations`, `scrolling`, `drag-resize`, `directives-pipes`, `providers`, `seo`, `utilities` |
31
- | `/query/` | Data fetching - see the dedicated {%skill:query%} guide first |
31
+ | `/query/` | Data fetching; load the dedicated query guide first when this package emitted one |
32
32
  | `/cdk/` | The predecessor UI toolkit, maintenance mode. Only for code that still uses it |
33
33
  | `/contentful/`, `/cli/`, `/eslint/`, `/types/` | The remaining packages |
34
34
 
@@ -66,13 +66,14 @@ That list is a snapshot. The site itself is machine-readable, so fetch rather th
66
66
  `-next` prereleases should read `{%docsBaseUrl%}` and `{%sdkStorybookUrl%}` only if
67
67
  they are the matching prerelease deployments, otherwise expect drift and verify
68
68
  against the installed `.d.ts` in `node_modules/@ethlete/<lib>`.
69
- - **`node_modules` is the tiebreaker.** If the docs and the installed package disagree,
70
- the installed type definitions win - report the drift rather than working around it.
69
+ - **Use the SDK skills instead of searching `node_modules` ad hoc.** The installed `.d.ts`
70
+ files are authoritative for the public type surface this consumer can compile against.
71
+ Source matching that installed build is authoritative for runtime implementation details.
72
+ A dirty or ahead `next` checkout is not automatically source for the installed package.
73
+ If docs and installed types disagree, report the drift.
71
74
  - **Never treat a `subtle` namespace as public API.** Anything exposed under `subtle` is
72
75
  an unsupported escape hatch that can change without a major version.
73
76
 
74
77
  ## Related
75
78
 
76
- - Data fetching has its own guide: {%skill:query%}
77
- - Theming tokens and how to register themes: {%skill:theming%}
78
79
  - When the docs cannot answer it, read the SDK source: {%skill:sdk-source%}
@@ -44,6 +44,21 @@ If a build stalls trying to reach Nx Cloud, re-run it with `NX_NO_CLOUD=true`.
44
44
 
45
45
  ## 3. Point this repo at the build
46
46
 
47
+ Preflight the manifest and lockfile before changing either:
48
+
49
+ Use {%resource:sdk-local-baseline.mjs%} to capture their exact bytes:
50
+
51
+ ```bash
52
+ git status --short -- <manifest> yarn.lock
53
+ sdk_local_baseline_dir=$(mktemp -d)
54
+ node <path-to-sdk-local-baseline.mjs> capture <manifest> yarn.lock "$sdk_local_baseline_dir"
55
+ ```
56
+
57
+ If either file already differs and you cannot tell who owns the edit, stop and ask. If
58
+ the user authorizes the experiment, keep the exact baseline above; pre-existing edits
59
+ are part of it and must survive byte-for-byte. If either file changes for another reason
60
+ during the experiment, stop and take a new agreed baseline before cleanup.
61
+
47
62
  Edit the version specifiers in `package.json` (the one declaring the dependency - in a
48
63
  workspace that is the workspace package, not necessarily the root):
49
64
 
@@ -97,17 +112,20 @@ reinstalling; the restart in step 3 is still required.)
97
112
  ## 6. Clean up when you are done
98
113
 
99
114
  Leaving a `file:` dependency behind breaks every other checkout and CI, because the path
100
- does not exist there. Restore it as part of the same task, not later:
115
+ does not exist there. Restore the exact recorded baseline as part of the same task:
101
116
 
102
117
  ```bash
103
- git checkout package.json # or hand-restore the original version specifier
104
- yarn install
105
- git status --short # package.json and yarn.lock must both be clean
118
+ node <path-to-sdk-local-baseline.mjs> restore <manifest> yarn.lock "$sdk_local_baseline_dir"
119
+ yarn install --immutable
120
+ node <path-to-sdk-local-baseline.mjs> verify <manifest> yarn.lock "$sdk_local_baseline_dir"
106
121
  ```
107
122
 
108
- Never commit a `file:` specifier or the lockfile it produced. If the verified fix is
109
- still unreleased, say what has to be published (which lib, which version) instead of
110
- shipping a local path.
123
+ The verification proves that the local-build delta is gone while preserving any edits
124
+ that existed before it. Do not require either file to be globally clean, and do
125
+ not run whole-file `git checkout` or `git restore`. Report the temporary baseline path;
126
+ it can be removed after verification because this workflow created it. Never commit a
127
+ `file:` specifier or the lockfile it produced. If the verified fix is still unreleased,
128
+ say what has to be published instead of shipping a local path.
111
129
 
112
130
  ## Related
113
131