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

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 (178) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/README.md +350 -29
  3. package/THIRD-PARTY-LICENSES.md +35 -0
  4. package/content/git-hooks/post-checkout.sh +10 -0
  5. package/content/git-hooks/pre-push.sh +5 -0
  6. package/content/hooks/context-warning.py +766 -108
  7. package/content/hooks/subagent-model-policy.py +193 -0
  8. package/content/output-styles/ste-clarity.md +131 -0
  9. package/content/rules/app-styling.md +59 -0
  10. package/content/rules/comments.md +53 -20
  11. package/content/rules/lint-and-format.md +6 -5
  12. package/content/rules/nx-layout.md +47 -0
  13. package/content/rules/reactive-state.md +1 -1
  14. package/content/rules/styling.md +9 -7
  15. package/content/rules/subagent-models.md +29 -0
  16. package/content/skills/angular-patterns/SKILL.md +30 -7
  17. package/content/skills/api-source/SKILL.md +130 -0
  18. package/content/skills/app-testing/SKILL.md +141 -0
  19. package/content/skills/codex-subagent/SKILL.md +90 -0
  20. package/content/skills/codex-subagent/codex-agent.mjs +229 -0
  21. package/content/skills/design-exploration/SKILL.md +205 -0
  22. package/content/skills/design-exploration/check-story.mjs +139 -0
  23. package/content/skills/design-exploration/shoot-template.mjs +61 -0
  24. package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
  25. package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  26. package/content/skills/domain-modeling/SKILL.md +80 -0
  27. package/content/skills/figma-export/SKILL.md +191 -0
  28. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  29. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  30. package/content/skills/figma-export/measure-template.mjs +87 -0
  31. package/content/skills/git-commit/SKILL.md +6 -7
  32. package/content/skills/git-flow/SKILL.md +87 -0
  33. package/content/skills/grill-with-docs/SKILL.md +13 -0
  34. package/content/skills/grilling/SKILL.md +34 -0
  35. package/content/skills/handoff/SKILL.md +74 -4
  36. package/content/skills/query/SKILL.md +165 -64
  37. package/content/skills/rxjs-signals/SKILL.md +31 -4
  38. package/content/skills/sdk-docs/SKILL.md +50 -14
  39. package/content/skills/sdk-local-build/SKILL.md +30 -10
  40. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  41. package/content/skills/sdk-source/SKILL.md +49 -21
  42. package/content/skills/sdk-update/SKILL.md +96 -0
  43. package/content/skills/story-styling/SKILL.md +10 -8
  44. package/content/skills/styleguide/SKILL.md +12 -7
  45. package/content/skills/styleguide/assets.md +54 -0
  46. package/content/skills/styleguide/changesets.md +17 -0
  47. package/content/skills/styleguide/file-structure.md +73 -0
  48. package/content/skills/styleguide/lint-rule-lookup.md +51 -0
  49. package/content/skills/styleguide/storybook-structure.md +25 -0
  50. package/content/skills/theming/SKILL.md +41 -28
  51. package/content/skills/timetrack/SKILL.md +397 -0
  52. package/content/skills/verify-in-app/SKILL.md +107 -0
  53. package/migrations/app-styling-utilities.md +63 -0
  54. package/migrations/list-state-query-form.md +20 -0
  55. package/migrations/nx-layout.md +29 -0
  56. package/migrations/sdk-components-over-hand-built-ui.md +27 -0
  57. package/migrations/search-query-field.md +52 -0
  58. package/migrations.json +48 -0
  59. package/package.json +16 -2
  60. package/src/index.js +63 -28
  61. package/src/index.js.map +1 -1
  62. package/src/lib/cli-args.d.ts +9 -0
  63. package/src/lib/cli-args.js +48 -0
  64. package/src/lib/cli-args.js.map +1 -0
  65. package/src/lib/commitlint.d.ts +10 -0
  66. package/src/lib/commitlint.js +51 -0
  67. package/src/lib/commitlint.js.map +1 -0
  68. package/src/lib/config.d.ts +38 -4
  69. package/src/lib/config.js +84 -8
  70. package/src/lib/config.js.map +1 -1
  71. package/src/lib/filter.d.ts +2 -0
  72. package/src/lib/filter.js +4 -4
  73. package/src/lib/filter.js.map +1 -1
  74. package/src/lib/frontmatter.d.ts +2 -0
  75. package/src/lib/frontmatter.js +33 -5
  76. package/src/lib/frontmatter.js.map +1 -1
  77. package/src/lib/git-flow/build.d.ts +35 -0
  78. package/src/lib/git-flow/build.js +24 -0
  79. package/src/lib/git-flow/build.js.map +1 -0
  80. package/src/lib/git-flow/config.d.ts +59 -0
  81. package/src/lib/git-flow/config.js +50 -0
  82. package/src/lib/git-flow/config.js.map +1 -0
  83. package/src/lib/git-flow/index.d.ts +6 -0
  84. package/src/lib/git-flow/index.js +10 -0
  85. package/src/lib/git-flow/index.js.map +1 -0
  86. package/src/lib/git-flow/parse.d.ts +49 -0
  87. package/src/lib/git-flow/parse.js +300 -0
  88. package/src/lib/git-flow/parse.js.map +1 -0
  89. package/src/lib/git-flow/rename.d.ts +24 -0
  90. package/src/lib/git-flow/rename.js +70 -0
  91. package/src/lib/git-flow/rename.js.map +1 -0
  92. package/src/lib/git-flow/start.d.ts +49 -0
  93. package/src/lib/git-flow/start.js +57 -0
  94. package/src/lib/git-flow/start.js.map +1 -0
  95. package/src/lib/git-flow/validate.d.ts +34 -0
  96. package/src/lib/git-flow/validate.js +72 -0
  97. package/src/lib/git-flow/validate.js.map +1 -0
  98. package/src/lib/git-flow-command.d.ts +4 -0
  99. package/src/lib/git-flow-command.js +157 -0
  100. package/src/lib/git-flow-command.js.map +1 -0
  101. package/src/lib/git-flow-repair.d.ts +17 -0
  102. package/src/lib/git-flow-repair.js +157 -0
  103. package/src/lib/git-flow-repair.js.map +1 -0
  104. package/src/lib/git-flow-start.d.ts +20 -0
  105. package/src/lib/git-flow-start.js +132 -0
  106. package/src/lib/git-flow-start.js.map +1 -0
  107. package/src/lib/git.d.ts +64 -0
  108. package/src/lib/git.js +131 -0
  109. package/src/lib/git.js.map +1 -0
  110. package/src/lib/gitlab.d.ts +35 -0
  111. package/src/lib/gitlab.js +108 -0
  112. package/src/lib/gitlab.js.map +1 -0
  113. package/src/lib/index.d.ts +2 -0
  114. package/src/lib/index.js +2 -0
  115. package/src/lib/index.js.map +1 -1
  116. package/src/lib/migrate.js +1 -1
  117. package/src/lib/migrate.js.map +1 -1
  118. package/src/lib/output-style-command.d.ts +3 -0
  119. package/src/lib/output-style-command.js +69 -0
  120. package/src/lib/output-style-command.js.map +1 -0
  121. package/src/lib/output-style.d.ts +38 -0
  122. package/src/lib/output-style.js +134 -0
  123. package/src/lib/output-style.js.map +1 -0
  124. package/src/lib/owned-paths.js +27 -3
  125. package/src/lib/owned-paths.js.map +1 -1
  126. package/src/lib/package-runner.d.ts +3 -0
  127. package/src/lib/package-runner.js +69 -0
  128. package/src/lib/package-runner.js.map +1 -0
  129. package/src/lib/plain-text.d.ts +9 -0
  130. package/src/lib/plain-text.js +22 -0
  131. package/src/lib/plain-text.js.map +1 -0
  132. package/src/lib/plan.d.ts +6 -6
  133. package/src/lib/plan.js +147 -40
  134. package/src/lib/plan.js.map +1 -1
  135. package/src/lib/prompt.d.ts +8 -0
  136. package/src/lib/prompt.js +27 -0
  137. package/src/lib/prompt.js.map +1 -0
  138. package/src/lib/render.d.ts +24 -2
  139. package/src/lib/render.js +39 -10
  140. package/src/lib/render.js.map +1 -1
  141. package/src/lib/sync.d.ts +0 -1
  142. package/src/lib/sync.js +23 -15
  143. package/src/lib/sync.js.map +1 -1
  144. package/src/lib/targets/agents-skills.js +1 -1
  145. package/src/lib/targets/agents-skills.js.map +1 -1
  146. package/src/lib/targets/claude-hooks.d.ts +1 -23
  147. package/src/lib/targets/claude-hooks.js +17 -85
  148. package/src/lib/targets/claude-hooks.js.map +1 -1
  149. package/src/lib/targets/claude.js +1 -2
  150. package/src/lib/targets/claude.js.map +1 -1
  151. package/src/lib/targets/codex-hooks.d.ts +12 -0
  152. package/src/lib/targets/codex-hooks.js +34 -0
  153. package/src/lib/targets/codex-hooks.js.map +1 -0
  154. package/src/lib/targets/codex.js +4 -4
  155. package/src/lib/targets/codex.js.map +1 -1
  156. package/src/lib/targets/copilot.js +3 -3
  157. package/src/lib/targets/copilot.js.map +1 -1
  158. package/src/lib/targets/cursor.js +2 -2
  159. package/src/lib/targets/cursor.js.map +1 -1
  160. package/src/lib/targets/git-hooks.d.ts +24 -0
  161. package/src/lib/targets/git-hooks.js +71 -0
  162. package/src/lib/targets/git-hooks.js.map +1 -0
  163. package/src/lib/targets/hooks-shared.d.ts +58 -0
  164. package/src/lib/targets/hooks-shared.js +128 -0
  165. package/src/lib/targets/hooks-shared.js.map +1 -0
  166. package/src/lib/targets/shared.d.ts +1 -9
  167. package/src/lib/targets/shared.js +4 -8
  168. package/src/lib/targets/shared.js.map +1 -1
  169. package/src/lib/timetrack-command.d.ts +11 -0
  170. package/src/lib/timetrack-command.js +1055 -0
  171. package/src/lib/timetrack-command.js.map +1 -0
  172. package/src/lib/timetrack-snapshot.d.ts +55 -0
  173. package/src/lib/timetrack-snapshot.js +120 -0
  174. package/src/lib/timetrack-snapshot.js.map +1 -0
  175. package/src/lib/timetrack.d.ts +561 -0
  176. package/src/lib/timetrack.js +271 -0
  177. package/src/lib/timetrack.js.map +1 -0
  178. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: git-flow
3
+ description: How to name and base a branch in this repo, and what its merge request must target - feature, sub-feature, release, release fix and hotfix. Read BEFORE creating a branch, opening a merge request, or choosing what to branch from (e.g. the user says "start work on FIP-2177" or "open an MR").
4
+ kind: skill
5
+ scope: both
6
+ vars: [gitFlowDevelopmentBranch, gitFlowProductionBranch, gitFlowTypes, gitFlowEnforcement, gitFlowSubPrefix]
7
+ ---
8
+
9
+ # Git flow
10
+
11
+ Work is organised in two levels: a **main feature branch** per Jira Story, into which
12
+ **sub-feature branches** (one per Task) are merged after review. The main feature branch is
13
+ what gets deployed to a test environment and, once accepted, merged into
14
+ `{%gitFlowDevelopmentBranch%}`.
15
+
16
+ **Never name a branch by hand - `start` does it from the grammar:**
17
+
18
+ ```bash
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
+ ```
24
+
25
+ `start` prints its plan (branch, base, MR target) and asks before writing anything; add
26
+ `--dry-run` to see the plan alone. It refuses on a dirty working tree, and a Task nests under
27
+ its parent Story's branch, so that branch has to exist first.
28
+
29
+ It reads the issue through the running Timetrack app, which holds this machine's only Jira
30
+ credentials. If it reports that the app is not running, ask the
31
+ user to start it. `--subject <text>` names a branch without reading Jira at all.
32
+
33
+ ## The five shapes
34
+
35
+ | Shape | Issue | Branch from | MR targets |
36
+ | --------------------------------------------------------------------------------- | ----- | ------------------------------ | ---------------------------------------------------------------- |
37
+ | `feat/FIP-2177-user-management` | Story | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` |
38
+ | `{%gitFlowSubPrefix%}/feat/FIP-2177-user-management/FIP-2178-user-password-reset` | Task | the main feature branch | the main feature branch |
39
+ | `release/2026.04.28` | - | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` and `{%gitFlowProductionBranch%}` |
40
+ | `{%gitFlowSubPrefix%}/release/2026.04.28/FIP-2222-button-not-visible` | Bug | the release branch | the release branch |
41
+ | `hotfix/FIP-2799-password-recovery-broken` | Bug | `{%gitFlowProductionBranch%}` | `{%gitFlowProductionBranch%}` |
42
+
43
+ - The **type** is one of {%gitFlowTypes%}, and a nested branch carries its parent's **full**
44
+ name - that path is what makes the parent machine-readable.
45
+ - The **key** is the Jira issue, uppercase, immediately after the type. The **subject** is the
46
+ Story's subject meta field in kebab-case, not a paraphrase of the summary.
47
+ - A branch with no key still works, but nothing can attribute it to an issue. Add the key.
48
+
49
+ ### Why a nested branch starts with `{%gitFlowSubPrefix%}/`
50
+
51
+ Git refuses a ref that is both a branch and a directory of branches. So
52
+ `feat/FIP-2177-user-management/FIP-2178-user-password-reset` **cannot exist** while
53
+ `feat/FIP-2177-user-management` does - git rejects it locally and the push comes back as
54
+ `refname conflict`. The prefix moves the nested tree out of the way and keeps the parent's
55
+ full path inside the child's name, so the MR target is still readable off the name.
56
+
57
+ Do not "fix" a name by dropping the prefix; the branch it produces cannot be created.
58
+
59
+ ## Rules that are not about naming
60
+
61
+ - **Never rebase a shared branch.** A main feature branch is published and other people's
62
+ sub-features are based on it - bring it up to date by **merging**
63
+ `{%gitFlowDevelopmentBranch%}` into it. Rebase only a local branch you have not pushed.
64
+ - **A sub-feature merges into its parent, never straight into
65
+ `{%gitFlowDevelopmentBranch%}`** - that is the whole point of the two levels, since the
66
+ parent is what gets tested as a unit.
67
+ - **Delete the source branch on merge** (the checkbox in the merge request) for sub-features,
68
+ release fixes and merged main features.
69
+ - **A hotfix branches off `{%gitFlowProductionBranch%}`** and, after rollout, that branch is
70
+ merged back into `{%gitFlowDevelopmentBranch%}` so the two do not drift.
71
+ - **Never push directly to `{%gitFlowDevelopmentBranch%}` or
72
+ `{%gitFlowProductionBranch%}`.** Open a merge request; it needs another developer's review.
73
+
74
+ ## Enforcement is `{%gitFlowEnforcement%}`
75
+
76
+ In `advisory` mode every rule reports and nothing blocks - the older naming shapes (`feature/`
77
+ instead of `feat/`, a lowercase key, no key at all, a `dev-*` integration branch) are accepted
78
+ on purpose while the team adapts. Report the suggestion, do not "fix" someone's existing
79
+ branch unasked - `repair` is how a rename happens, and only when asked for.
80
+
81
+ `dev-*` is the **old spelling of a main feature branch**, not a stray name: sub-features
82
+ legitimately target it. Leave a live one alone.
83
+
84
+ ## Commit messages are a separate thing
85
+
86
+ Commits stay conventional (`feat(platform): Prefer a player's common name`) and carry **no
87
+ issue key** - the branch already has it. Follow the repository's commit-message guidance.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: grill-with-docs
3
+ description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
4
+ kind: skill
5
+ scope: both
6
+ modelInvocation: false
7
+ ---
8
+
9
+ Call the Skill tool twice, for {%skill:grilling%} and {%skill:domain-modeling%}.
10
+
11
+ ---
12
+
13
+ Vendored from [mattpocock/skills](https://github.com/mattpocock/skills) at `3cca18b`, MIT (Copyright (c) 2026 Matt Pocock).
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: grilling
3
+ description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
4
+ kind: skill
5
+ scope: both
6
+ ---
7
+
8
+ Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
9
+
10
+ Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
11
+
12
+ Format a round like so:
13
+
14
+ ```
15
+ ❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
16
+
17
+ ➡️ <your recommended answer>
18
+
19
+ ---
20
+
21
+ ❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
22
+
23
+ ➡️ <your recommended answer>
24
+ ```
25
+
26
+ Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
27
+
28
+ Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
29
+
30
+ The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
31
+
32
+ ---
33
+
34
+ Vendored from [mattpocock/skills](https://github.com/mattpocock/skills) at `3cca18b`, MIT (Copyright (c) 2026 Matt Pocock).
@@ -15,11 +15,78 @@ continue in a fresh session. Two modes:
15
15
  - **save** ("handoff", "wrap up") - write a handoff file.
16
16
  - **resume** ("continue from the handoff") - read one and continue the work.
17
17
 
18
+ Name one invocation form to the user - your own - and never both. The skill is
19
+ installed as `ethlete-handoff`, so under Claude Code the commands are
20
+ `/ethlete-handoff` and `/ethlete-handoff resume [name]`; there is no `/handoff`.
21
+ Codex has no slash commands, so there the same two are plain prompts without the
22
+ leading `/`.
23
+
18
24
  Handoff files live in `{%handoffDir%}/` (gitignored - they are personal,
19
25
  ephemeral working state, not team docs).
20
26
 
27
+ ## Handoff mode
28
+
29
+ Where this repo installs the `context-warning` hook, it names the budget at session start,
30
+ turns handoff mode on at 70% of it, and repeats a short note with the remaining budget on
31
+ every prompt after that. It reaches you at three points, not only when the user types: at
32
+ the start of the session, after a batch of your own tool calls, and as a turn ends. So a
33
+ long autonomous run learns its token count while it works.
34
+
35
+ Handoff mode is not an order to stop. It is the signal to change how you work, so that
36
+ either ending - you finish, or a fresh session takes over - stays cheap:
37
+
38
+ - Work in slices that each end in a committable state. Never leave two half-finished edits
39
+ in the tree.
40
+ - Send bulky reads, searches and test runs to a sub-agent. Its output stays out of your
41
+ context; its answer does not.
42
+ - Write down each decision and dead end as you reach it, in the commit message or in the
43
+ handoff file. Do not save them for a final summary you may have no room for.
44
+ - Start nothing you cannot finish or hand off inside the remaining budget.
45
+
46
+ At the critical tier (85%) the hook asks you to choose one of two, and to tell the user
47
+ which you chose:
48
+
49
+ 1. **Finish the task.** Only when you can name every step that is left, and they plainly
50
+ fit the remaining budget. Finish it, commit, then apply the save test below - the answer
51
+ is then usually that no file is needed at all.
52
+ 2. **Hand off.** Everything else. Finish the in-flight atomic edit, nothing new, then save.
53
+
54
+ "Nearly done" means you can name the last steps now. It does not mean the end feels close.
55
+ At the last tier (95%) the hook takes option 1 away, because a budget that small covers no
56
+ task.
57
+
58
+ ### Ending a turn over the budget
59
+
60
+ From 85% on, the hook also fires as you try to end the turn, and the turn then continues so
61
+ you can act. It adds a third option to the two above:
62
+
63
+ 3. **Cross the boundary on purpose.** Some sessions are worth the premium rate: the task is
64
+ one step from done, or this session holds reasoning that no handoff file survives. Take
65
+ it only for a reason you can name in one sentence right now. Say the reason, say what it
66
+ costs, and keep working.
67
+
68
+ Option 3 is a judgment call, not a way out of the other two. Naming a reason you cannot
69
+ state plainly is the same as taking option 2 late and expensively. The hook asks once per
70
+ tier, so it will not block the end of a turn twice for the same budget. It stays quiet while a
71
+ handoff is being saved, until the user's next prompt.
72
+
21
73
  ## Save mode
22
74
 
75
+ **Decide first whether a handoff is needed at all.** One exists so work can resume, so
76
+ write one only when at least one of these holds:
77
+
78
+ - Uncommitted work from this session sits in the tree.
79
+ - A step the user asked for is unfinished, blocked, or waiting on a result.
80
+ - A decision, dead end, or user constraint from this session is written down nowhere else -
81
+ not in a commit message, a changeset, a docs page, or a plan file.
82
+
83
+ Check it, never assume it: `git status`, `git log`, and the task as the user stated it. A
84
+ context warning is a token count, not evidence that work is left.
85
+
86
+ If none of them holds, write no file. Tell the user in two or three lines what landed, name
87
+ the commits, and say a fresh session needs nothing from this one. A handoff nobody resumes is
88
+ noise, and it costs the next session the tokens to read it.
89
+
23
90
  Write `{%handoffDir%}/<slug>.md` where `<slug>` is a short kebab-case name for
24
91
  the task (use the user-provided slug if they gave one). If the file exists,
25
92
  overwrite it - a handoff always describes the _current_ state.
@@ -90,9 +157,12 @@ resume from it.
90
157
  3. Verify reality still matches: current branch, `git status`, last commit. If
91
158
  they diverge from the handoff (e.g. someone committed in between), say what
92
159
  changed and adapt - the repo is the truth, the handoff is the guide.
93
- 4. Read any guides the handoff's work obviously needs (e.g. {%skill:theming%}
94
- before CSS work) - same rules as always.
160
+ 4. **Read only what you are about to change.** The handoff already carries the decisions,
161
+ the constraints and the dead ends, so do not re-read the plan files it summarises, and do
162
+ not survey the directory. Open a source file when you are going to edit it, and load a
163
+ focused guidance file only when the work needs one.
95
164
  5. Continue with the **Next steps** section. Don't redo work listed under
96
165
  _Done_; don't re-open questions under _Decisions_.
97
- 6. When every next step is complete (including changeset/docs follow-ups),
98
- delete the handoff file so the directory only contains live handoffs.
166
+ 6. When every next step is complete, decide whether the handoff can be removed. Delete it
167
+ only if this workflow created it and the save workflow or user authorized cleanup.
168
+ Otherwise report that it is no longer needed and let the user decide.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: query
3
- description: The signals-first @ethlete/query data-fetching system - the query client, typed query creators, reactive args, and reading results as signals or observables. Read BEFORE writing or reviewing code that fetches data, wires search/autocomplete to an API, adds auth/polling/pagination, or bridges a query into UI or RxJS.
3
+ description: The signals-first @ethlete/query data-fetching system - the query client, typed query creators, reactive args, and reading results as signals or observables. Read BEFORE writing or reviewing code that fetches data, builds a list with filters/search/sort/paging bound to URL query params (defineQueryForm), wires search/autocomplete to an API, adds auth, route guards or polling, or bridges a query into UI or RxJS.
4
4
  kind: skill
5
5
  scope: consumer
6
6
  requires: ['@ethlete/query']
@@ -13,38 +13,109 @@ Signals-first, typesafe data fetching for Angular: request dedup, caching, polli
13
13
  paged queries, bearer auth, GraphQL, and a socket.io realtime client.
14
14
 
15
15
  **The written docs are the source of truth - read the relevant page before
16
- non-trivial query work.** This guide is the index plus the load-bearing facts, so you
17
- don't re-derive them from source.
18
-
19
- | Page | Covers |
20
- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
21
- | {%docsBaseUrl%}/query/ | Overview + the two-generations note |
22
- | {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
23
- | {%docsBaseUrl%}/query/features | `withArgs`, `withPolling`, `withAutoRefresh`, side-effect handlers |
24
- | {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
25
- | {%docsBaseUrl%}/query/auth | Bearer auth: login/refresh, auto token refresh, multi-tab sync |
26
- | {%docsBaseUrl%}/query/caching · `/stacks` · `/errors` · `/gql` · `/ws` | Caching/dedup, pagination, error/retry, GraphQL, WebSockets |
27
- | {%docsBaseUrl%}/query/multi-tab | Opt-in cross-tab sync: shared responses, per-key polling election, mutation fan-out |
28
- | {%docsBaseUrl%}/query/query-forms | Router-synced filter/search forms |
29
- | {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` |
16
+ non-trivial query work.** This guide maps every need to its API and page, plus the
17
+ load-bearing facts, so you don't re-derive them from source or hand-build what ships.
18
+
19
+ | Page | Covers |
20
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------- |
21
+ | {%docsBaseUrl%}/query/ | Overview, the two generations, what else the package ships |
22
+ | {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
23
+ | {%docsBaseUrl%}/query/features | `withArgs`, polling, auto-refresh, side-effect handlers, custom features |
24
+ | {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
25
+ | {%docsBaseUrl%}/query/auth | Bearer auth provider, guards, token refresh, auth features |
26
+ | {%docsBaseUrl%}/query/caching | Cache keys, dedup, `keepUnusedFor`, freshness, refresh and invalidation |
27
+ | {%docsBaseUrl%}/query/multi-tab | Cross-tab response sharing, one poller per key, mutation fan-out |
28
+ | {%docsBaseUrl%}/query/persistence | Successful reads kept in IndexedDB for reloads and offline cold starts |
29
+ | {%docsBaseUrl%}/query/stacks | Many queries of one creator, infinite lists, paged data |
30
+ | {%docsBaseUrl%}/query/dependent-queries | GET → GET dependencies and ordered mutation chains |
31
+ | {%docsBaseUrl%}/query/groups | Several queries behind one state: loading, error and retry follow the one that ran last |
32
+ | {%docsBaseUrl%}/query/batching | Bulk writes with bounded concurrency, per-item results, retry |
33
+ | {%docsBaseUrl%}/query/errors | Error object, opt-in parsers, form submission, violations, retries |
34
+ | {%docsBaseUrl%}/query/testing | Specs: answering requests, `@ethlete/query/testing` helpers and fakes |
35
+ | {%docsBaseUrl%}/query/query-forms | **Any filtered, searched, sorted or paged list** - `defineQueryForm` |
36
+ | {%docsBaseUrl%}/query/gql | GraphQL creators over GET/POST |
37
+ | {%docsBaseUrl%}/query/ws | socket.io rooms and live-updating responses |
38
+ | {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` and its replacements |
39
+ | {%docsBaseUrl%}/query/migrating-from-v2 | Codemods and the screen-by-screen move off the legacy client |
40
+ | {%docsBaseUrl%}/query/migrating-from-ngrx-toolkit | The generator and interop that move a store off `@tomtomb/ngrx-toolkit` |
41
+ | {%docsBaseUrl%}/query-devtools/ | The devtools panel and `provideQueryDevtools()` |
42
+
43
+ ## You need → use → read
44
+
45
+ Before writing a helper, find your need here. Pages are under `{%docsBaseUrl%}/query/`.
46
+
47
+ | You need | Use | Read |
48
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
49
+ | One client per API | `createQueryClient`, `injectApi()` | `queries` |
50
+ | REST reads (auto-executing) | `createGetQuery`, `createHeadQuery`, `createOptionsQuery` | `http` |
51
+ | REST writes (manual) | `createPostQuery`, `createPutQuery`, `createPatchQuery`, `createDeleteQuery` | `http` |
52
+ | The same behind a bearer token | `createSecureGetQuery`, `createSecureHeadQuery`, `createSecureOptionsQuery`, `createSecurePostQuery`, `createSecurePutQuery`, `createSecurePatchQuery`, `createSecureDeleteQuery` | `http`, `auth` |
53
+ | Upload/download progress bar | creator option `reportProgress: true`, then `loading().progress` | `http` |
54
+ | Args that follow signals (route params, inputs, filters) | `withArgs` | `features` |
55
+ | A filtered / searched / sorted / paged list synced to the URL | `defineQueryForm`, `queryField`, `searchQueryField`, `sortQueryField` | `query-forms` |
56
+ | Refetch on an interval | `withPolling` | `features` |
57
+ | Next request once the previous settled, with a cursor from it | `withLongPolling` | `features` |
58
+ | Refetch when some other signal changes | `withAutoRefresh` | `features` |
59
+ | Toast / log / react on success or failure | `withSuccessHandling`, `withErrorHandling`, `withLogging` | `features` |
60
+ | Patch a loaded response in place (e.g. from a socket message) | `withResponseUpdate` | `features`, `ws` |
61
+ | Page falls out of range after a filter shrinks the results | `withPageResetOnError`, `isPageOutOfRangeError` | `features` |
62
+ | Package your own reusable query behavior | `createQueryFeature` | `features` |
63
+ | Refresh reads after a mutation (here and in other tabs) | `injectApi().invalidateQueries({ url })` | `caching` |
64
+ | Show a mutation's result in cached reads before the server answers | `withOptimisticUpdate` | `caching` |
65
+ | One loading, error and retry state for whichever of several mutations ran last | `createQueryGroup` | `groups` |
66
+ | Refetch everything after a client-wide header changed | `injectApi().refreshQueriesInUse()` | `caching` |
67
+ | Show the previous data instantly on back navigation | `keepUnusedFor` + `executionState().cachedResponse` | `caching` |
68
+ | Last known data after a reload or offline | `withQueryPersistence`, `createIndexedDbQueryPersistenceAdapter`, `createNoopQueryPersistenceAdapter` | `persistence` |
69
+ | Tabs share responses, poll once, see each other's mutations | `withMultiTabSync` | `multi-tab` |
70
+ | Parallel detail requests for a list of ids | `createQueryStack`, `transformArrayResponse` | `stacks` |
71
+ | Load more / infinite scroll / classic paging | `createPagedQueryStack`, `ethletePaginationAdapter` (and other adapters) | `stacks` |
72
+ | A GET whose args come from another GET's response | `withArgs` reading the first query's `response()` | `dependent-queries` |
73
+ | Run mutations in order, each using the previous result | `querySequence` | `dependent-queries` |
74
+ | Many edits with progress, time remaining and retry of failures | `createQueryBatch` | `batching` |
75
+ | Submit a signal form through a mutation, violations on fields | `createQuerySubmission` | `errors` |
76
+ | Own submit handler, or server-side validation while typing | `executeUntilSettled`, `executeUntilSettled$`, `mapViolationsToFormErrors`, `validateWithQuery` | `errors` |
77
+ | Understand the error shapes your API returns | `withEthleteApiErrors` (all), `withSymfonyErrors`, `withHtmlErrorParsing` | `errors` |
78
+ | Error text for display, or an error built by hand (tests) | `queryErrorMessage`, `queryErrorMessages`, `createQueryErrorResponse`, `<et-query-error>` | `errors` |
79
+ | Retry failed requests | `withDefaultRetry`, `createDefaultRetryFn` | `errors` |
80
+ | Login / refresh / logout with bearer tokens | `createBearerAuthProvider`, `withAuthenticationQuery`, `withRefreshQuery` | `auth` |
81
+ | Protect routes, check roles or permissions | `createAuthGuard`, `canMatchWith` | `auth` |
82
+ | "Remember me" auto-login | `withPersistentAuth` | `auth` |
83
+ | Log out idle users | `withInactivityLogout` | `auth` |
84
+ | Warn before the session expires | `withTokenExpirationWarning` | `auth` |
85
+ | Revoke the token on logout | `withTokenRevocation` | `auth` |
86
+ | Share login/logout across tabs | `withBearerAuthMultiTabSync` | `auth` |
87
+ | Auth telemetry events | `withTracking` | `auth` |
88
+ | Tokens from SSO or a native shell | `provider.setTokens(access, refresh)` | `auth` |
89
+ | Which of several queries is busy (v2 query collections) | each query's `executionState()` in a `computed`; auth: `provider.executionState()` | `migrating-from-v2` |
90
+ | GraphQL | `createGqlQueryViaGet`, `createGqlQueryViaPost`, `createGqlMutationViaGet`, `createGqlMutationViaPost`, `createSecureGqlQueryViaGet`, `createSecureGqlQueryViaPost`, `createSecureGqlMutationViaGet`, `createSecureGqlMutationViaPost` | `gql` |
91
+ | Realtime rooms over socket.io | `createWebSocketClient` | `ws` |
92
+ | Inspect queries, stacks, auth and cache at runtime | `provideQueryDevtools()` + `@ethlete/query-devtools` | `/query-devtools/` |
30
93
 
31
94
  ## Two generations - use the current one
32
95
 
33
- - **Current (use this):** signals-first, provider-based. `createQueryClient`,
34
- `createGetQuery`/`createPostQuery`/…, `withArgs`. Everything imports from the
35
- single entry `@ethlete/query`.
36
- - **Legacy (maintenance mode):** class-based `V2QueryClient`, `.prepare().execute()`,
37
- `queryComputed`. Don't write new code against it.
96
+ Write new code against the signals-first system (`createQueryClient`, `createGetQuery`, …,
97
+ `withArgs`), all from the single entry `@ethlete/query`. The class-based `V2QueryClient`
98
+ (`.prepare().execute()`, `queryComputed`) is in maintenance mode.
38
99
 
39
100
  ## Core usage
40
101
 
41
- One client per API, one creator per endpoint, one live query per component instance:
102
+ One client per API, one bound creator per method, one query per endpoint, one live query
103
+ per component instance:
42
104
 
43
105
  ```ts
44
106
  import { createQueryClient, createGetQuery, withArgs } from '@ethlete/query';
45
107
 
108
+ // api.ts
46
109
  export const apiClient = createQueryClient({ name: 'api', baseUrl: API_URL });
47
- export const getPost = createGetQuery(apiClient)<GetPostArgs>((p) => `/posts/${p.pathParams.postId}`);
110
+ export const getQuery = createGetQuery(apiClient);
111
+
112
+ // posts.queries.ts
113
+ export type GetPostQueryArgs = {
114
+ response: Post;
115
+ pathParams: { postId: string };
116
+ };
117
+
118
+ export const getPost = getQuery<GetPostQueryArgs>((p) => `/posts/${p.postId}`);
48
119
 
49
120
  // in a component (injection context):
50
121
  postId = input.required<string>();
@@ -54,61 +125,91 @@ post = computed(() => this.postQuery.response());
54
125
 
55
126
  - `GET`/`HEAD`/`OPTIONS` **auto-execute** - immediately when static/argless, or
56
127
  whenever `withArgs` produces new args. Mutations (`POST`/`PUT`/`PATCH`/`DELETE`)
57
- never auto-execute; call `.execute({ args })`. A function route (`pathParams`)
58
- requires `withArgs` (dev-mode error otherwise).
128
+ never auto-execute; declare their args with `withArgs` too and call `.execute()`. A function route (`pathParams`)
129
+ requires `withArgs` (a type error, and a dev-mode error).
130
+ - A route function receives the path params themselves: `(p) => \`/posts/${p.postId}\``.
131
+ - With bearer auth, bind the secure creators the same way:
132
+ `const secureGetQuery = createSecureGetQuery(apiClient, authProviderRef)`.
59
133
  - Queries live in a child injector tied to the creating component; destroyed with it.
60
134
 
61
- ## The query object
135
+ ## Lists with filters, search, sort or paging: `defineQueryForm`
62
136
 
63
- Every state member is an **`ObservableSignal`** - a `Signal` that also has
64
- `.asObservable()`. So each is both a signal (call it) and a stream:
137
+ Never wire list controls to the URL by hand (`injectQueryParams` + `router.navigate` +
138
+ drafts + effects). `defineQueryForm` does URL sync, debounce, defaults and page resets:
65
139
 
66
- - `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
67
- - `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
68
- `executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
69
- - Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
70
-
71
- `query.response.asObservable()` binds to the query's own injector, so callers get
72
- an `Observable<T | null>` **without** needing their own injection context (unlike
73
- raw `toObservable`). It emits `null` first - `pipe(filter(r => r !== null))`.
140
+ ```ts
141
+ qf = defineQueryForm({
142
+ fields: {
143
+ search: searchQueryField(),
144
+ sort: sortQueryField(),
145
+ page: queryField<number>({ defaultValue: 1, isResetBy: ['search', 'sort'] }),
146
+ },
147
+ }).observe();
148
+
149
+ users = getUsers(
150
+ withArgs(() => {
151
+ const { search, sort, page } = this.qf.value();
152
+
153
+ return { queryParams: { query: search, sortBy: sort?.active, sortOrder: sort?.direction, page } };
154
+ }),
155
+ );
156
+ ```
74
157
 
75
- ## Reactive args & features
158
+ Bind form controls with `[formField]="qf.fields.search"`. `et-pagination` and
159
+ `et-page-size-select` are not form controls: bind their `model` to the field's value signal,
160
+ `[(page)]="qf.fields.page().value"`; an `et-table` sort binds the same way to a
161
+ `tableSortQueryField()`, `[(sort)]="qf.fields.sort().value"`, with no mapping. `qf.value()` is the committed, debounced value. Each
162
+ change pushes a history entry; pass `observe({ replaceUrl: true })` to replace it instead. See {%docsBaseUrl%}/query/query-forms for the other field creators,
163
+ filter overlays and `activeFilterCount`.
76
164
 
77
- - **`withArgs(() => ({ pathParams, queryParams, body }))`** - runs like a `computed`;
78
- re-runs when a signal it reads changes and re-executes the query. This is how you
79
- drive **search-as-you-type**: back it with a search signal
80
- (`withArgs(() => ({ queryParams: { search: this.search() } }))`). Return
81
- `CLEAR_QUERY_ARGS` to reset args to `null` (pauses polling/auto-refresh).
82
- - `withPolling({ interval })`, `withAutoRefresh({ onSignalChanges: [...] })`.
83
- - Side-effects: `withSuccessHandling`, `withErrorHandling`, `withLogging`.
165
+ When you move a list that syncs its params by hand, keep the param names the URL already uses, so
166
+ saved links keep working (`queryParamPrefix` when two lists share a route). Keep each old default as
167
+ the field's `defaultValue`. A URL with sort and direction in two params (`?sort=name&dir=asc`)
168
+ keeps two `queryField`s, `dir` with `skipInFilterCount: true`; `sortQueryField()` writes one
169
+ `name:asc` param, so it breaks old links. A date field bound to `et-date-input`,
170
+ `et-date-time-input` or `et-time-input` needs `dateQueryField({ as: 'string' })`.
84
171
 
85
- There is no built-in debounce operator - dedup/caching handles repeated identical
86
- requests; debounce at the input if you need it.
172
+ ## The query object
87
173
 
88
- ## Bridging a query into RxJS / other APIs
174
+ Every state member is an **`ObservableSignal`** - call it, or `.asObservable()` it without an
175
+ injection context of your own (it emits `null` first).
89
176
 
90
- To hand a query's results to something that wants an `Observable<T[]>` (e.g. a
91
- `(query) => Observable<...>` source): drive the query by a search signal and return
92
- its response stream.
177
+ - `response()` → `TResponse | null` (kept while re-executing; cleared on a failed re-exec).
178
+ To keep showing the last good data after a failure, read `cachedResponse` from the
179
+ `loading` or `failure` variant of `executionState()` - do not copy responses into a signal.
180
+ - `loading()`, `error()` (normalized `QueryErrorResponse`), `args()`,
181
+ `executionState()` (`{ type: 'loading' | 'success' | 'failure', … } | null`, great for `@switch`).
182
+ - Methods: `execute({ args?, options? })`, `reset()`, `createSnapshot()`, `asReadonly()`.
93
183
 
94
- ```ts
95
- private search = signal('');
96
- private q = getItems(withArgs(() => ({ queryParams: { q: this.search() } })));
97
-
98
- fetch(query: string) {
99
- this.search.set(query);
100
- return this.q.response.asObservable().pipe(
101
- filter((r): r is ItemsRes => r !== null),
102
- map((r) => r.items),
103
- );
104
- }
105
- ```
184
+ ## Reactive args
185
+
186
+ - **`withArgs(() => ({ pathParams, queryParams, body }))`** runs like a `computed` and
187
+ re-executes on change. Return `null` to park the query (pauses polling/auto-refresh).
188
+ Calling `.execute()` on a parked query is ignored and warns in dev mode
189
+ (`queryExecutedWhileParkedMessage`); pass `execute({ args })` or let the source return args.
190
+ - **Prefer `withArgs` over `execute({ args })`.** Declared args keep a `GET` re-executing and
191
+ polling/auto-refresh restarting off the same signal; a function route throws without it. A
192
+ mutation with `withArgs` is just `.execute()`. Reserve `execute({ args })` for a one-off
193
+ payload no signal holds.
194
+ - Search-as-you-type: read a debounced value (`searchQueryField()` debounces 300ms; every
195
+ query field takes `debounce`) - a raw input signal sends one request per keystroke.
196
+ - `withLongPolling` and `withPolling` throw when combined.
197
+
198
+ ## Bridging into RxJS or callbacks
199
+
200
+ For a callback that must return one correlated result, run a manual query
201
+ (`{ onlyManualExecution: true }`) through `executeUntilSettled$(query, { args })` - cold, its
202
+ snapshot is frozen to that execution, and unsubscribing aborts the request. Keep the Promise
203
+ `executeUntilSettled` for an `async` signal-forms `submit()`. Never set a signal and return the shared `response`
204
+ stream: the retained previous response can be the first non-null emission. See
205
+ {%docsBaseUrl%}/query/queries#the-query-object.
106
206
 
107
207
  ## Gotchas
108
208
 
109
- - Signals-first: read `query.response()` in templates/computeds; it's **nullable**
110
- (`?? []` / `filter(Boolean)` as needed).
111
- - Don't reach for the legacy client for new code.
209
+ - `query.response()` is **nullable** (`?? []` / `filter(Boolean)` as needed).
112
210
  - `.execute()` defaults `args` to the current `args()` when omitted.
113
211
  - Anything under a query's `subtle` namespace is an unsupported escape hatch - never
114
212
  treat it as public API.
213
+ - Use `withArgs` for mutations too. Never reach for `execute({ args })` plus
214
+ `silenceMissingWithArgsFeatureError` unless the args really exist only at call time - the
215
+ flag is an escape hatch, not the mutation pattern.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rxjs-signals
3
- description: How to choose between signals and RxJS, and use each correctly - synchronous state vs asynchronous work, unsubscribing, and avoiding RxJS inside effects/computeds. Read when adding reactive state, wiring up an observable, or deciding whether something should be a signal or a stream. Part of the Ethlete styleguide (judgment beyond what lint enforces).
3
+ description: How to choose between signals and RxJS, and use each correctly - synchronous state vs asynchronous work, unsubscribing, and avoiding RxJS inside effects/computeds. Read when adding reactive state, wiring up an observable, or deciding whether something should be a signal or a stream.
4
4
  kind: skill
5
5
  scope: both
6
6
  requires: ['@ethlete/core']
@@ -34,9 +34,16 @@ 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
+ - **`takeUntilDestroyed()` goes last in its pipe** (`ethlete/take-until-destroyed-last`), so
44
+ higher-order operators such as `switchMap` and their inner subscriptions are covered.
45
+ Other limiting and finalization operators do not have a universal “last” position;
46
+ place them where their semantics belong.
40
47
  - **Side effects go in `tap()`**, never in the `subscribe()` callback - keep
41
48
  `subscribe()` empty.
42
49
  - **Don't reach for RxJS inside `effect()`/`computed()`.** Subscribing per run
@@ -56,6 +63,26 @@ toObservable(page)
56
63
  .subscribe();
57
64
  ```
58
65
 
66
+ ## An effect that only writes a signal is a derivation
67
+
68
+ If an `effect()` does nothing but `.set()` a signal, even behind an `if`, the value is derived
69
+ state: use `computed()` when it is read-only, `linkedSignal()` when the user can still
70
+ overwrite it. `ethlete/prefer-linked-signal` flags the plain cases. Keep `effect()` for work
71
+ that leaves the signal graph: DOM, storage, logging, third-party APIs.
72
+
73
+ ```ts
74
+ // ❌
75
+ effect(() => {
76
+ if (this.items().length) this.selected.set(this.items()[0]);
77
+ });
78
+
79
+ // ✅ the previous value stays when the list is empty
80
+ selected = linkedSignal<Item[], Item | null>({
81
+ source: this.items,
82
+ computation: (items, previous) => items[0] ?? previous?.value ?? null,
83
+ });
84
+ ```
85
+
59
86
  ## Prefer `@ethlete/core` helpers
60
87
 
61
88
  Lint nudges these, but reach for them by default: `injectViewportSize()`,