@ethlete/agent-rules 0.1.0-next.16 → 0.1.0-next.18

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 (75) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +14 -6
  3. package/content/hooks/subagent-model-policy.py +5 -3
  4. package/content/rules/app-styling.md +47 -0
  5. package/content/rules/comments.md +4 -1
  6. package/content/rules/nx-layout.md +47 -0
  7. package/content/rules/styling.md +1 -1
  8. package/content/rules/subagent-models.md +7 -7
  9. package/content/skills/angular-patterns/SKILL.md +23 -0
  10. package/content/skills/app-testing/SKILL.md +141 -0
  11. package/content/skills/design-exploration/SKILL.md +3 -1
  12. package/content/skills/git-flow/SKILL.md +4 -4
  13. package/content/skills/query/SKILL.md +150 -82
  14. package/content/skills/rxjs-signals/SKILL.md +20 -0
  15. package/content/skills/sdk-docs/SKILL.md +35 -7
  16. package/content/skills/sdk-update/SKILL.md +12 -7
  17. package/content/skills/story-styling/SKILL.md +7 -6
  18. package/content/skills/styleguide/lint-rule-lookup.md +1 -1
  19. package/content/skills/theming/SKILL.md +3 -4
  20. package/content/skills/timetrack/SKILL.md +154 -49
  21. package/content/skills/verify-in-app/SKILL.md +107 -0
  22. package/migrations/app-styling-utilities.md +114 -0
  23. package/migrations/list-state-query-form.md +103 -0
  24. package/migrations/nx-layout.md +23 -0
  25. package/migrations/sdk-components-over-hand-built-ui.md +69 -0
  26. package/migrations/search-query-field.md +45 -0
  27. package/migrations.json +43 -0
  28. package/package.json +4 -1
  29. package/src/index.js +5 -4
  30. package/src/index.js.map +1 -1
  31. package/src/lib/config.d.ts +2 -0
  32. package/src/lib/config.js +16 -3
  33. package/src/lib/config.js.map +1 -1
  34. package/src/lib/filter.d.ts +2 -0
  35. package/src/lib/filter.js +4 -4
  36. package/src/lib/filter.js.map +1 -1
  37. package/src/lib/frontmatter.js +32 -5
  38. package/src/lib/frontmatter.js.map +1 -1
  39. package/src/lib/git-flow/parse.js +38 -12
  40. package/src/lib/git-flow/parse.js.map +1 -1
  41. package/src/lib/git-flow-repair.js +12 -1
  42. package/src/lib/git-flow-repair.js.map +1 -1
  43. package/src/lib/git.js +8 -2
  44. package/src/lib/git.js.map +1 -1
  45. package/src/lib/gitlab.js +11 -1
  46. package/src/lib/gitlab.js.map +1 -1
  47. package/src/lib/migrate.js +1 -1
  48. package/src/lib/migrate.js.map +1 -1
  49. package/src/lib/output-style.js +11 -3
  50. package/src/lib/output-style.js.map +1 -1
  51. package/src/lib/owned-paths.js +8 -2
  52. package/src/lib/owned-paths.js.map +1 -1
  53. package/src/lib/package-runner.d.ts +1 -0
  54. package/src/lib/package-runner.js +34 -0
  55. package/src/lib/package-runner.js.map +1 -0
  56. package/src/lib/plan.js +29 -3
  57. package/src/lib/plan.js.map +1 -1
  58. package/src/lib/render.d.ts +4 -0
  59. package/src/lib/render.js +8 -2
  60. package/src/lib/render.js.map +1 -1
  61. package/src/lib/sync.js +22 -12
  62. package/src/lib/sync.js.map +1 -1
  63. package/src/lib/targets/codex.js +1 -1
  64. package/src/lib/targets/codex.js.map +1 -1
  65. package/src/lib/targets/copilot.js +1 -1
  66. package/src/lib/targets/copilot.js.map +1 -1
  67. package/src/lib/targets/git-hooks.js +2 -1
  68. package/src/lib/targets/git-hooks.js.map +1 -1
  69. package/src/lib/targets/hooks-shared.js +1 -1
  70. package/src/lib/targets/hooks-shared.js.map +1 -1
  71. package/src/lib/timetrack-command.js +344 -60
  72. package/src/lib/timetrack-command.js.map +1 -1
  73. package/src/lib/timetrack.d.ts +185 -16
  74. package/src/lib/timetrack.js +66 -15
  75. package/src/lib/timetrack.js.map +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @ethlete/agent-rules
2
2
 
3
+ ## 0.1.0-next.18
4
+
5
+ ### Minor Changes
6
+
7
+ - Timetrack: every write command now queues for the user's approval in the app and prints an approval id; `timetrack approval <id>` reads the outcome, and the agent contract moves to version 3.
8
+ - Timetrack: add `timetrack sync <day>` to plan a day's Tempo sync, and `--write --plan <hash>` to write the confirmed plan; `worklogs --json` now reports a midnight `startTime`.
9
+ - Timetrack: add `timetrack worklog --delete <id> --day <YYYY-MM-DD>` to delete one of the account's own Tempo worklogs; the app refuses any other id.
10
+ - Timetrack: add `timetrack standins --merge <id> --into <id>` and the `standIn.merge` agent op, which waits for the user's approval. `standIn.list` now reports `hiddenOn` and `mergedIds`.
11
+
12
+ ### Patch Changes
13
+
14
+ - `parseBranch` now reads a leading issue key from a deprecated spelling such as `dev-FIP-2721-subject`, so it returns the key and a rename suggestion that carries it.
15
+ - The design exploration skill now tells an agent in the ethlete SDK that the user reads calls in Ethlete Studio (`yarn studio`), so the agent does not start `yarn design` next to it.
16
+ - Git-flow matches a project key prefix as a whole word, keeps the subject with a grouped `keyPattern`, drops the `CI_JOB_TOKEN` fallback, and prints an undo hint when the push after a repair rename fails.
17
+ - `git-flow repair` sends `GITLAB_TOKEN` only to the hosts in `GITLAB_HOST` or `CI_SERVER_HOST`, and no longer follows redirects.
18
+ - `git-flow repair` and `start` no longer mistake a remote branch that only ends with the name, such as `team/feat/x` for `feat/x`, for the branch.
19
+ - `sync` writes only changed files, survives dangling symlinks, warns about an unparseable hook settings file, and rejects duplicate frontmatter keys; `output-style --remove` works for a style no longer shipped.
20
+ - `timetrack` rejects a flag whose value is another flag, a malformed `naming` day and a non-positive `--limit`; `day --out` honours `--json` and resolves against the root.
21
+ - `timetrack` reads a date-only `--at`, `--from` or `--to` as local midnight and accepts a local `HH:MM`, and `timetrack project` resolves a relative path.
22
+ - `sync` refuses a file whose ethlete marker block has lost a marker, instead of deleting the text after it on the next run.
23
+ - Git flow: a lowercase `<word>-<number>` branch subject such as `step-2-rework` is no longer read as an issue key when no key prefixes are configured.
24
+ - `tableSortQueryField()` holds the `et-table` sort and keeps it in the URL, so `[(sort)]="qf.fields.sort().value"` binds it with no mapping.
25
+ - Recommend `sonnet` for scoped subagent work and keep `opus` for hard work, in the subagent-models rule and the subagent-model-policy hook.
26
+
27
+ ## 0.1.0-next.17
28
+
29
+ ### Minor Changes
30
+
31
+ - `et update` now leaves tasks to fix code written under the earlier guidance: app styling, URL-bound list state, hand-debounced search, hand-built charts, avatars and progress bars, and the Nx layout.
32
+ - `timetrack resync --replace` re-reads a checkout's agent logs and replaces the samples the store already holds for those sessions, so a parser fix reaches stored days.
33
+
34
+ ### Patch Changes
35
+
36
+ - The Angular and signals skills cover shared `injectX()` logic, per-row formatting, writing query params and effects that only write a signal; the comment rule says app code rarely has public API.
37
+ - Add the consumer skills `verify-in-app` (drive the served app headlessly with Playwright) and `app-testing` (specs for query, router and overlay code), and link the new query testing docs page from the query skill.
38
+ - The `app-styling` rule and its migration now keep `ViewEncapsulation.None` on app components, as the `require-view-encapsulation-none` lint rule requires, and scope the remaining CSS under the component's host class.
39
+ - Consumer repos get an `app-styling` rule (Tailwind in app templates, 10px rem root) instead of the SDK's component styling rule, and the `query` and `sdk-docs` skills now point to `defineQueryForm` and every component domain.
40
+ - Query skill: bridge a correlated result into RxJS with `executeUntilSettled$`, keeping the Promise form for signal-forms `submit()`.
41
+ - The `list-state-query-form` task binds pagination, page size and a table sort the way that works, and keeps `replaceUrl: true`. The query skill names `observe({ replaceUrl: true })`.
42
+ - The guidance migrations find the call sites they missed in a real app, and generated commands use the repo's package manager.
43
+ - The `app-styling-utilities`, `sdk-components-over-hand-built-ui` and `nx-layout` migration tasks name the real theme utilities, find more call sites, and stop before overriding a repo's own rules or the generated `AGENTS.md` block.
44
+ - Consumer Nx workspaces get an `nx-layout` rule: thin apps, feature code in `libs/domain`, and shared `queries`, `types`, `uikit`, `theme` and `env` libs with `scope:*` tags.
45
+ - The consumer query skill now maps every @ethlete/query capability to its docs page, so agents find APIs like `defineQueryForm` instead of hand-building them.
46
+ - `ethlete-agents` commands run from a subdirectory now use the nearest directory holding `ethlete-agents.config.json` as the repo root, so `check` no longer reports false drift there.
47
+ - The `sdk-docs` skill points consumer agents at the new App setup docs page first: root font size, theme generation and the providers an app needs.
48
+ - `sync` and `check` now name every skill or rule skipped for an unmet `requires` or missing var, and warn when a path var like `themeStylesheet` points at a missing file.
49
+ - `vars.themeStylesheet` may now name a folder of theme files. The `story-styling` skill searches it recursively.
50
+ - `recommendedTs` now uses `ethlete/consistent-type-definitions` instead of `@typescript-eslint/consistent-type-definitions`, so `--fix` no longer turns an interface inside `declare module` or `declare global` (such as the theme-name registry the `@ethlete/core` generators emit) into a type alias that merges into nothing.
51
+ - The `ET100` error now says mutations need `withArgs` too and calls `silenceMissingWithArgsFeatureError` an escape hatch; the query skill recommends `withArgs` for mutations.
52
+
3
53
  ## 0.1.0-next.16
4
54
 
5
55
  ### Patch Changes
package/README.md CHANGED
@@ -99,18 +99,19 @@ Prettier rewrites them and `check` then reports drift on every run:
99
99
  `.agents/skills/` is the cross-tool baseline) and adds `claude`, `cursor` or `copilot`
100
100
  when their directory exists; or list an explicit subset.
101
101
  - **`profile`** - `"consumer"` (default) emits `scope: consumer` and `scope: both`
102
- content. `"sdk"` emits only `both`; the SDK repo uses it so its own hand-written,
103
- authoring-side guides are not overwritten by the consumer-side versions.
102
+ content. `"sdk"` emits `scope: sdk` and `scope: both`; the SDK repo uses it so its own
103
+ hand-written, authoring-side guides are not overwritten by the consumer-side versions.
104
104
  - **`vars`** - values for the template tokens a guide declares. Defaults live in
105
105
  `content/defaults.json`; a guide whose variable has no default and no value is
106
106
  skipped with a warning rather than emitted with a dangling placeholder. Some are
107
107
  derived from the repo instead of defaulted - the `gitFlow*` ones from the `gitFlow`
108
- block, and `commitRuleSource` / `commitValidation` from whether a commitlint config
108
+ block, `packageRunner` (`yarn`, `pnpm exec`, `bunx` or `npx`) from the root
109
+ `package.json`'s `packageManager` or the lockfile, and `commitRuleSource` / `commitValidation` from whether a commitlint config
109
110
  exists (`commitlint.config.*`, `.commitlintrc*`, or a `commitlint` key in
110
111
  `package.json`). Without one, the git-commit guide presents the format as the repo's
111
112
  convention and never mentions a `commitlint` run - an agent that goes looking for a
112
113
  promised validator and finds nothing reports the discrepancy instead of just
113
- committing. Setting either one in `vars` overrides the detection.
114
+ committing. Setting any derived var in `vars` overrides the detection.
114
115
  - **`exclude`** - rule or skill names to skip entirely for every configured agent and
115
116
  developer. For example, `"exclude": ["git-flow", "handoff"]` prevents those skills
116
117
  from being generated; the next `sync` also removes copies generated previously. Unknown
@@ -223,7 +224,8 @@ sets no subject falls back to its summary, and the printed plan says which of th
223
224
  `git-flow repair [ref]` derives the conforming name (`--key FIP-2900` when the old name
224
225
  carries no issue key, `--to <branch>` to override), renames the branch locally and on the
225
226
  remote, and retargets the open merge requests aimed at it through the GitLab API.
226
- `GITLAB_TOKEN` needs the `api` scope.
227
+ `GITLAB_TOKEN` needs the `api` scope, and is sent only to the hosts named in `GITLAB_HOST`
228
+ (comma-separated) or, in a GitLab CI job, `CI_SERVER_HOST`.
227
229
 
228
230
  Everything is checked before the first mutation, and it refuses rather than half-finishing:
229
231
 
@@ -231,7 +233,7 @@ Everything is checked before the first mutation, and it refuses rather than half
231
233
  move a merge request to another source branch, and closing it would lose its discussion -
232
234
  merge or close it first.
233
235
  - A branch that is pushed but whose merge requests cannot be listed (no token, or a remote
234
- that is not GitLab) blocks too. `--no-mr-check` asserts that none point at it.
236
+ that is not a configured GitLab host) blocks too. `--no-mr-check` asserts that none point at it.
235
237
  - If a retarget fails halfway, the old branch is still there and the recovery commands are
236
238
  printed.
237
239
 
@@ -247,10 +249,16 @@ npx ethlete-agents timetrack search "password" # open issues of the picked p
247
249
  npx ethlete-agents timetrack project # which project does this repo log into?
248
250
  npx ethlete-agents timetrack create --summary "…" # file a ticket with the instance's settings
249
251
  npx ethlete-agents timetrack log --issue FIP-2177 --minutes 45
252
+ npx ethlete-agents timetrack approval <id> # where a queued write stands
250
253
  ```
251
254
 
252
255
  `--json` prints the raw answer instead of lines. `git-flow start` uses the same channel.
253
256
 
257
+ **Every write waits for the user.** `create`, `log` and every other write answer at once with an
258
+ approval id and write nothing; the user approves or rejects the request in the app, and
259
+ `approval <id>` reads the outcome. A request still waiting at the end of its day expires.
260
+ `TIMETRACK_CLIENT` names the caller in the queue.
261
+
254
262
  **How it connects.** The app binds a loopback socket and writes its port and a fresh token
255
263
  into `agent.json` in its own data directory (`~/.local/share/io.ethlete.timetrack/` on Linux,
256
264
  `~/Library/Application Support/…` on macOS, `%APPDATA%\…` on Windows), readable by its owner
@@ -43,8 +43,10 @@ would run on the model leading this session - which is how one expensive model e
43
43
  Call the tool again with `model` set:
44
44
 
45
45
  - `haiku` - mechanical lookups: grep, find, read a file, run a command and report what it said.
46
- - `opus` - the default for real work: code changes, tests, debugging, reviewing a diff.
47
- - `sonnet` - a middle ground where opus is more than the task needs.
46
+ - `sonnet` - scoped work with a clear target: a fix in a named file, a spec, a docs page, a lint cleanup, \
47
+ research that needs reasoning. Sonnet 5.5 is close to opus here and costs less.
48
+ - `opus` - hard work: a bug with no known cause, a change across many files, a review of a diff, and any task \
49
+ sonnet did not finish.
48
50
  - `fable` - judgment-heavy work: planning, design, cross-cutting review, leading other subagents. The most \
49
51
  expensive of the four, so choosing it asks the user first.
50
52
 
@@ -54,7 +56,7 @@ effort, so a call that names one needs no `model` of its own."""
54
56
 
55
57
  FABLE_REASON = """This subagent would run on fable, the most expensive model. Approve it where the task is \
56
58
  judgment-heavy - planning, design, cross-cutting review, or leading other subagents. Deny it for implementation \
57
- (`model: "opus"`) or for a mechanical lookup (`model: "haiku"`), then repeat the call with that model."""
59
+ (`model: "sonnet"` or `"opus"`) or for a mechanical lookup (`model: "haiku"`), then repeat the call with that model."""
58
60
 
59
61
 
60
62
  def agent_name(argv):
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: app-styling
3
+ description: App components are styled with Tailwind utilities in their templates; app CSS stays out of @layer components; the SDK needs a 10px rem root; hardcoded colours are never primary values.
4
+ kind: rule
5
+ scope: consumer
6
+ requires: ['@ethlete/core']
7
+ ---
8
+
9
+ ## Styling
10
+
11
+ Every component in this workspace (views, pages, shells, shared UI in `libs/`) is laid out
12
+ and styled with **Tailwind utility classes in its template**, including the generated
13
+ theme utilities (`bg-et-surface-bg`, `border-et-surface-border`, `text-et-<theme>`). Do not
14
+ invent a BEM class system, and do not write a `.css` file for layout, spacing or
15
+ typography a utility expresses. Every app component sets `encapsulation: ViewEncapsulation.None`,
16
+ as the `require-view-encapsulation-none` lint rule requires. Its CSS is then global, so give the
17
+ component a host class (`host: { class: 'app-player-card' }`) and scope every selector under it,
18
+ the way SDK components scope theirs under their `et-` classes. Never write a bare element or
19
+ unprefixed class selector in a component stylesheet.
20
+
21
+ Write CSS only for what utilities cannot express. Keep that CSS **unlayered** or in
22
+ `@layer utilities` — never in `@layer components`, and that includes the global stylesheet. SDK component styles are injected into
23
+ `@layer components` at runtime, after your stylesheet, so an app rule in the same layer
24
+ ties on layer and loses on source order.
25
+
26
+ To change an SDK component's look, set its `--et-*` tokens first (documented per
27
+ component), then use a utility class or unlayered CSS on the element — see "Overriding
28
+ component styles" in the docs site's components overview.
29
+
30
+ The SDK sizes everything in `rem` on a **10px root**: the app's global stylesheet must set
31
+ `html { font-size: 62.5%; }`. Without it every SDK control renders 1.6× too large. With it,
32
+ `1rem` is `10px`, so the app's own `rem` values stay easy to read.
33
+
34
+ The 10px root also shrinks every rem-based Tailwind scale to 62.5% of its nominal size (1.6×
35
+ smaller): `p-4` is 10px instead of 16px, and `max-w-3xl` is 480px instead of 768px. Set
36
+ `--spacing: 0.4rem` in the app's `@theme` so the spacing scale keeps its 4px step, as the SDK's
37
+ own Storybook does. Redefine any other rem scale the app uses (`--text-*`, `--container-*`,
38
+ `--radius-*`) the same way, or use px where a size matters. Breakpoints are not affected: a
39
+ media query's `rem` ignores the root font size.
40
+
41
+ **Never use a hardcoded colour as the primary value.** Backgrounds, text, borders and
42
+ interaction states resolve from the surface and colour theming tokens
43
+ (`--et-surface-*-solid`, `--et-theme-color-*`) or their generated utilities. A static
44
+ fallback inside `var(--token, <fallback>)` is permitted, but not required.
45
+
46
+ Theme **names** (`brand`, `danger`, `dark-elevated`, …) are registered by this app; the SDK
47
+ ships none. Semantic colours resolve by theme `type` (e.g. `injectErrorTheme()`).
@@ -20,6 +20,8 @@ smaller function, or a type; fix that instead of narrating it.
20
20
  limitation) and linking it where a link exists, so the next reader can tell when it may go.
21
21
  4. **Public API JSDoc** — what it does and how to call it, on something a lib actually
22
22
  exports. One or two sentences. Not internals, not history, not why it is shaped that way.
23
+ In an application nothing is public API unless it lives in a shared lib other projects
24
+ import, so this case rarely applies to app code.
23
25
 
24
26
  Nothing else qualifies. Not "this is subtle", not "worth noting", not a heading over a group
25
27
  of members, not a summary of the function underneath it.
@@ -43,7 +45,8 @@ years.
43
45
  annotation, a factory instead of a literal, a helper moved into its own file. The type, the
44
46
  annotation and the import already say what happens.
45
47
  - **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.
48
+ Z", "renamed for clarity", "see ADR-0012". Git and the ADR index know; the next reader does
49
+ not care.
47
50
  - **The same explanation at every call site.** Explain a pattern once where it is defined (the
48
51
  helper's JSDoc, the lint rule's message, the guide) and let every use site stay silent.
49
52
  - **Commented-out code.**
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: nx-layout
3
+ description: Nx workspace layout - thin apps, feature code in libs grouped by kind, path-mirroring aliases, scope tags.
4
+ kind: rule
5
+ scope: consumer
6
+ requires: ['nx']
7
+ ---
8
+
9
+ ## Nx workspace layout
10
+
11
+ Apps are thin shells. All feature code lives in libs, grouped by kind, not by Nx type:
12
+
13
+ ```text
14
+ apps/<app>/src/app/ app.component.ts, app.config.ts, app.routes.ts only
15
+ libs/
16
+ domain/<app>/<feature>/ views, components, services, <feature>.routes.ts
17
+ domain/<app>/shared/ code shared by one app's features (optional)
18
+ queries/ query clients and creators, one folder per backend
19
+ types/ API models and shared types
20
+ uikit/ app-agnostic presentational components
21
+ theme/ surface and colour themes
22
+ env/ environment.ts, environment.production.ts
23
+ assets/ fonts, icons, images
24
+ ```
25
+
26
+ - `app.routes.ts` only lazy-loads domain libs:
27
+ `loadChildren: () => import('@acme/domain/admin/users').then((m) => m.USERS_ROUTES)`.
28
+ Each feature lib exports its own `<FEATURE>_ROUTES`.
29
+ - A small app may use one lib per app (`libs/domain/<app>`) with a folder per feature.
30
+ Never put features, queries, shared UI or theme in the app project.
31
+ - Do not add Nx `feature/ui/data-access/util` folders or `type:*` tags.
32
+ - Every lib has `src/index.ts` and explicit `build` (`@nx/angular:ng-packagr-lite`), `lint`
33
+ and `test` targets in `project.json`. Keep `plugins: []` in `nx.json`.
34
+ - Alias = path with slashes: `@acme/domain/admin/users`, `@acme/queries`, `@acme/types`,
35
+ `@acme/uikit`, `@acme/env`. Project name = path with dashes (`domain-admin-users`).
36
+ Component `prefix` = the repo abbreviation.
37
+ - Tag every project with one `scope:*`: `scope:<app>` for an app and its domain libs,
38
+ `scope:core` for env and types, plus `scope:queries`, `scope:uikit`, `scope:theme`.
39
+ `@nx/enforce-module-boundaries` with `enforceBuildableLibDependency: true` lets
40
+ `scope:<app>` depend on itself and the shared scopes, never on another app.
41
+ - A backend in the same workspace is an app in `apps/` with its libs in
42
+ `libs/domain/<name>-api`. Ban `@angular/*` in backend scopes and `@nestjs/*` in frontend
43
+ scopes with `bannedExternalImports`.
44
+ - Environment config lives only in `libs/env`, never hardcoded in a query client.
45
+
46
+ This is the layout for new workspaces and new code. Do not restructure an existing
47
+ workspace that deviates from it unless the user asks.
@@ -2,7 +2,7 @@
2
2
  name: styling
3
3
  description: Component CSS is plain CSS in @layer components, and hardcoded colours are never primary values.
4
4
  kind: rule
5
- scope: both
5
+ scope: sdk
6
6
  requires: ['@ethlete/core']
7
7
  ---
8
8
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: subagent-models
3
- description: A subagent's model is an explicit choice on every call - haiku for lookups, opus for work, fable for judgment.
3
+ description: A subagent's model is an explicit choice on every call - haiku for lookups, sonnet for scoped work, opus for hard work, fable for judgment.
4
4
  kind: rule
5
5
  scope: both
6
6
  ---
@@ -11,12 +11,12 @@ A subagent spawned without a `model` runs on the model leading the session, so a
11
11
  model ends up doing every small job it delegates. **Set `model` on every call**, matched to the
12
12
  task:
13
13
 
14
- | Model | The work it fits |
15
- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
- | `haiku` | Mechanical lookups: grep, find, read a file, run a command and report what it said. |
17
- | `sonnet` | A middle ground where opus is more than the task needs. |
18
- | `opus` | The default for real work: code changes, tests, debugging, reviewing a diff. |
19
- | `fable` | Judgment-heavy work: planning, design, cross-cutting review, leading other subagents. The most expensive of the four, so ask the user before picking it. |
14
+ | Model | The work it fits |
15
+ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `haiku` | Mechanical lookups: grep, find, read a file, run a command and report what it said. |
17
+ | `sonnet` | Scoped work with a clear target: a fix in a named file, a spec, a docs page, a lint cleanup, research that needs reasoning. Sonnet 5.5 is close to opus here and costs less. |
18
+ | `opus` | Hard work: a bug with no known cause, a change across many files, a review of a diff, and any task sonnet did not finish. |
19
+ | `fable` | Judgment-heavy work: planning, design, cross-cutting review, leading other subagents. The most expensive of the four, so ask the user before picking it. |
20
20
 
21
21
  Effort follows the prompt, not a parameter: scope the prompt to one question, name what "done"
22
22
  means, and say "keep it brief" for a lookup. Two calls need no `model` of their own - a named
@@ -28,6 +28,16 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
28
28
  <button [disabled]="disabled()"></button>
29
29
  ```
30
30
 
31
+ - **Per-row formatting belongs in the data, not in a method per row.** Map the rows once
32
+ in a `computed()` and bind the prepared fields. A pure, module-level formatting function
33
+ is acceptable too; a component method called for each row in `@for` is not.
34
+
35
+ ```ts
36
+ // ❌ <td>{{ formatDate(row.createdAt) }}</td> - re-runs for every row, every CD cycle
37
+ // ✅
38
+ rows = computed(() => this.items().map((item) => ({ ...item, createdAtLabel: formatDate(item.createdAt) })));
39
+ ```
40
+
31
41
  ## Lifecycle
32
42
 
33
43
  - **Prefer the `constructor`** (runs in the injection context) over `ngOnInit` /
@@ -40,6 +50,10 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
40
50
  `createRootProvider` and the `injectX()` helper pattern from `@ethlete/core`
41
51
  rather than an `@Injectable` or `@Service`. (Both decorators are lint-banned;
42
52
  choosing a function over a service at all is the judgment.)
53
+ - **Repeated component logic → one `injectX()` function.** When two components carry the
54
+ same signals, effects or subscriptions, extract them into an `injectX()` that runs in the
55
+ injection context and returns what the components bind. Do not copy the block, and do not
56
+ reach for a base class.
43
57
  - **Directives → plain functions where possible.** With signal APIs, move the core
44
58
  logic into a function so it's reusable without applying a directive; keep a
45
59
  directive only when a host element genuinely needs it. Avoid common input/output
@@ -47,6 +61,15 @@ DOM/`window`, output naming, class-member + decorator-metadata order, no
47
61
  - **Pipes → a `computed()` calling a utility function.** Pipes carry no logic;
48
62
  most can be dropped in favour of a `computed`.
49
63
 
64
+ ## Query params
65
+
66
+ - **Read** them with the `@ethlete/core` signals: `injectQueryParam(key)`,
67
+ `injectQueryParams()`. `ActivatedRoute` and `router.url` are lint-banned.
68
+ - **Write** them with `inject(Router).navigate([], { queryParams, queryParamsHandling: 'merge' })`.
69
+ `merge` keeps the params you do not set; `null` removes a param.
70
+ - **Filter, sort and page state bound to the URL** is a query form: with `@ethlete/query`,
71
+ use `defineQueryForm` instead of writing the params by hand.
72
+
50
73
  ## Components
51
74
 
52
75
  - Inline template/styles for small components; external `.html` / `.css` files
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: app-testing
3
+ description: How to unit-test an app built on the Ethlete SDK - views that fetch through @ethlete/query (HttpTestingController and the @ethlete/query/testing helpers), components that open overlays, and views that read the URL with injectQueryParam under provideRouter. Read before writing or fixing a spec for a component, view or service that uses @ethlete/query, @ethlete/core router signals or @ethlete/components overlays.
4
+ kind: skill
5
+ scope: consumer
6
+ requires: ['@ethlete/core']
7
+ vars: [docsBaseUrl]
8
+ ---
9
+
10
+ # Testing an Ethlete app
11
+
12
+ Specs run in jsdom under `TestBed`. Test through the public API the view uses - mount the
13
+ component, answer its requests, move the URL - and assert on what it renders or exposes. The full
14
+ reference for the query helpers is {%docsBaseUrl%}/query/testing.
15
+
16
+ ## Views that fetch
17
+
18
+ A client made with `createQueryClient` is provided in root, so a spec provides nothing for it. It
19
+ needs Angular's HTTP testing backend, and answers each request by hand:
20
+
21
+ ```ts
22
+ import { provideHttpClient } from '@angular/common/http';
23
+ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
24
+ import { TestBed } from '@angular/core/testing';
25
+
26
+ beforeEach(() => {
27
+ TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
28
+ });
29
+
30
+ afterEach(() => TestBed.inject(HttpTestingController).verify());
31
+
32
+ it('renders the players', async () => {
33
+ const http = TestBed.inject(HttpTestingController);
34
+ const fixture = TestBed.createComponent(PlayersComponent);
35
+ fixture.detectChanges();
36
+ TestBed.tick();
37
+
38
+ http.expectOne((req) => req.url.includes('/players')).flush({ items: ['Müller'] });
39
+ await fixture.whenStable();
40
+
41
+ expect(fixture.nativeElement.textContent).toContain('Müller');
42
+ });
43
+ ```
44
+
45
+ - A GET query executes once its args resolve, which happens on change detection -
46
+ `TestBed.tick()` before `expectOne`, and again after `flush` before reading the query's
47
+ `response()`, `loading()` or `error()`.
48
+ - `req.url` carries the query string, so `expectOne('https://api.example.com/players?search=mul')`
49
+ matches the full URL. Match with a predicate, as above, when the params are not the point.
50
+ - Fail a request with `flush(body, { status: 422, statusText: 'Unprocessable Entity' })`.
51
+ - Mutations (`POST`, `PUT`, `PATCH`, `DELETE`) never auto-execute - call `.execute({ args })`,
52
+ then expect the request.
53
+
54
+ From `@ethlete/query/testing`:
55
+
56
+ | Export | Use |
57
+ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `expectAndFlush(http, url, body, status?)` | `expectOne(url).flush(body)` in one call. |
59
+ | `expectFlushAndWait(http, url, body, status?)` | The same, then `TestBed.tick()`. |
60
+ | `setupQueryTest(config?)` | A scratch client, `HttpTestingController`, `createGet` … `createDelete` creators - for testing code that takes a creator or client. |
61
+ | `setupAuthTest({ querySetup })` | A bearer auth provider on that scratch client, with `login()` / `refresh()` helpers that answer the requests. |
62
+ | `createFakeQueryPersistenceStore()` | In-memory persistence adapter - jsdom has no IndexedDB. |
63
+ | `installFakeBroadcastChannel()`, `installFakeWebLocks()`, `flushMultiTabSync` | Two clients in one spec behave as two tabs. |
64
+ | `createWebSocketTestDouble()` | A scripted socket.io `io` for `createWebSocketClient({ io })`. |
65
+
66
+ `setupQueryTest` calls `TestBed.configureTestingModule` itself and replaces `ErrorHandler` with a
67
+ no-op (`mockErrorHandler: false` keeps the real one). Call it inside `beforeEach`, and call
68
+ `restoreConsole()` in `afterEach` if the file spies on `console`.
69
+
70
+ ## Views that read the URL
71
+
72
+ `injectQueryParam`, `injectQueryParams`, `injectPathParam` and the other router signals from
73
+ `@ethlete/core` read the router, so the spec provides one and navigates it:
74
+
75
+ ```ts
76
+ import { provideRouter, Router } from '@angular/router';
77
+
78
+ TestBed.configureTestingModule({
79
+ providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
80
+ });
81
+
82
+ await TestBed.inject(Router).navigateByUrl('/?search=mul');
83
+ const fixture = TestBed.createComponent(PlayersComponent);
84
+ fixture.detectChanges();
85
+ TestBed.tick();
86
+
87
+ http.expectOne((req) => req.urlWithParams.includes('search=mul')).flush({ items: [] });
88
+
89
+ await TestBed.inject(Router).navigateByUrl('/?search=x');
90
+ TestBed.tick();
91
+ http.expectOne((req) => req.urlWithParams.includes('search=x')).flush({ items: [] });
92
+ ```
93
+
94
+ Navigate to change a param - never set the signal or mock the inject function. A view that also
95
+ reads path params needs the real route in the config (`provideRouter([{ path: 'players/:id',
96
+ component: PlayersComponent }])`) and `RouterTestingHarness` from `@angular/router/testing` to
97
+ render it.
98
+
99
+ ## Components that open overlays
100
+
101
+ An overlay mounts at the end of `document.body`, not in the fixture, and it opens and closes over
102
+ animation frames. Query the document, wait two frames, and close what is left after each test:
103
+
104
+ ```ts
105
+ import { injectOverlayManager } from '@ethlete/components';
106
+
107
+ const flushFrames = () =>
108
+ new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
109
+
110
+ afterEach(async () => {
111
+ TestBed.runInInjectionContext(() => injectOverlayManager())
112
+ .openOverlays()
113
+ .forEach((ref) => ref.forceClose());
114
+ await flushFrames();
115
+ });
116
+
117
+ it('opens the edit dialog', async () => {
118
+ const fixture = TestBed.createComponent(PlayerListComponent);
119
+ fixture.detectChanges();
120
+
121
+ fixture.nativeElement.querySelector('button.edit').click();
122
+ await flushFrames();
123
+
124
+ expect(document.querySelector('.player-edit-dialog')).not.toBeNull();
125
+ });
126
+ ```
127
+
128
+ - Count open overlays with `injectOverlayManager().openOverlays().length` (inside
129
+ `TestBed.runInInjectionContext`).
130
+ - The `OverlayRef` that `createOverlayOpener(...).open()` returns has `componentInstance()`,
131
+ `close(result)` and `afterClosed()` - enough to test what the opener does with the result.
132
+ - A query-param overlay (`defineQueryParamOverlay`) opens from the URL: provide the router and
133
+ navigate with the param set, as above.
134
+ - jsdom has no `ResizeObserver`, `IntersectionObserver`, `matchMedia` or `Element.animate`. If a
135
+ spec throws on one of them, stub it once in the test setup file with an inert class or function,
136
+ not per spec.
137
+
138
+ ## Before you call it done
139
+
140
+ A spec for a bug fix must fail without the fix - revert it, run the spec, see it fail, restore it.
141
+ To check the same change in the running app, read {%skill:verify-in-app%}.
@@ -67,7 +67,9 @@ drawings. A project the config names not draws bare.
67
67
  Start the page with `et design [checkout]`, which serves the port `config.json` names. The
68
68
  checkout defaults to the working directory, and `DE_PORT` overrules the config, so two
69
69
  checkouts can be drawn at once. A repository that builds the tool rather than installing it
70
- wraps this in a script - in the ethlete SDK, `yarn design` and `yarn design:check`.
70
+ wraps this in a script - in the ethlete SDK, `yarn design:check`. There, the user reads the
71
+ calls in Ethlete Studio, started with `yarn studio`, and Studio serves the page itself. Do not
72
+ start `yarn design` next to it.
71
73
 
72
74
  Ethlete Studio carries its own copy of the tool, so it draws a checkout that installs nothing.
73
75
  The machine still needs Node, and the render stage still needs `playwright` where the copy in
@@ -16,10 +16,10 @@ what gets deployed to a test environment and, once accepted, merged into
16
16
  **Never name a branch by hand - `start` does it from the grammar:**
17
17
 
18
18
  ```bash
19
- npx ethlete-agents git-flow start FIP-2178 # reads the issue, names it, branches off the right base
20
- npx ethlete-agents git-flow check # is the current branch conforming?
21
- npx ethlete-agents git-flow explain <branch> # what the parser sees, and what it expects
22
- npx ethlete-agents git-flow repair <branch> # rename a non-conforming one, retarget its MRs
19
+ {%packageRunner%} ethlete-agents git-flow start FIP-2178 # reads the issue, names it, branches off the right base
20
+ {%packageRunner%} ethlete-agents git-flow check # is the current branch conforming?
21
+ {%packageRunner%} ethlete-agents git-flow explain <branch> # what the parser sees, and what it expects
22
+ {%packageRunner%} ethlete-agents git-flow repair <branch> # rename a non-conforming one, retarget its MRs
23
23
  ```
24
24
 
25
25
  `start` prints its plan (branch, base, MR target) and asks before writing anything; add