@ethlete/agent-rules 0.1.0-next.3 → 0.1.0-next.31

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 (200) hide show
  1. package/AGENTS.md +28 -0
  2. package/CHANGELOG.md +480 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +362 -29
  5. package/THIRD-PARTY-LICENSES.md +35 -0
  6. package/content/defaults.json +1 -1
  7. package/content/git-hooks/post-checkout.sh +10 -0
  8. package/content/git-hooks/pre-push.sh +5 -0
  9. package/content/hooks/context-warning.py +872 -110
  10. package/content/hooks/subagent-model-policy.py +194 -0
  11. package/content/mods/.claude-plugin/marketplace.json +11 -0
  12. package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
  13. package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
  14. package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
  15. package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
  16. package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
  17. package/content/output-styles/ste-clarity.md +131 -0
  18. package/content/rules/agent-sessions-wake-up.md +11 -0
  19. package/content/rules/app-styling.md +66 -0
  20. package/content/rules/comments.md +4 -1
  21. package/content/rules/lint-and-format.md +7 -5
  22. package/content/rules/nx-layout.md +50 -0
  23. package/content/rules/reactive-state.md +1 -1
  24. package/content/rules/styling.md +9 -7
  25. package/content/rules/subagent-models.md +39 -0
  26. package/content/skills/agent-sessions/SKILL.md +81 -0
  27. package/content/skills/agent-sessions/agent-sessions.mjs +498 -0
  28. package/content/skills/angular-patterns/SKILL.md +30 -7
  29. package/content/skills/api-source/SKILL.md +156 -0
  30. package/content/skills/app-testing/SKILL.md +141 -0
  31. package/content/skills/codex-subagent/SKILL.md +90 -0
  32. package/content/skills/codex-subagent/codex-agent.mjs +229 -0
  33. package/content/skills/design-exploration/SKILL.md +687 -0
  34. package/content/skills/design-exploration/check-story.mjs +139 -0
  35. package/content/skills/design-exploration/shoot-template.mjs +61 -0
  36. package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
  37. package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  38. package/content/skills/domain-modeling/SKILL.md +80 -0
  39. package/content/skills/e2e-triage/SKILL.md +80 -0
  40. package/content/skills/figma-export/SKILL.md +191 -0
  41. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  42. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  43. package/content/skills/figma-export/measure-template.mjs +87 -0
  44. package/content/skills/git-commit/SKILL.md +11 -7
  45. package/content/skills/git-flow/SKILL.md +87 -0
  46. package/content/skills/grill-with-docs/SKILL.md +13 -0
  47. package/content/skills/grilling/SKILL.md +51 -0
  48. package/content/skills/handoff/SKILL.md +125 -5
  49. package/content/skills/query/SKILL.md +165 -64
  50. package/content/skills/rxjs-signals/SKILL.md +33 -4
  51. package/content/skills/sdk-docs/SKILL.md +50 -14
  52. package/content/skills/sdk-local-build/SKILL.md +26 -8
  53. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  54. package/content/skills/sdk-source/SKILL.md +37 -34
  55. package/content/skills/sdk-update/SKILL.md +103 -0
  56. package/content/skills/story-styling/SKILL.md +10 -8
  57. package/content/skills/styleguide/SKILL.md +12 -7
  58. package/content/skills/styleguide/assets.md +54 -0
  59. package/content/skills/styleguide/changesets.md +17 -0
  60. package/content/skills/styleguide/file-structure.md +73 -0
  61. package/content/skills/styleguide/lint-rule-lookup.md +51 -0
  62. package/content/skills/styleguide/storybook-structure.md +25 -0
  63. package/content/skills/theming/SKILL.md +50 -28
  64. package/content/skills/timetrack/SKILL.md +397 -0
  65. package/content/skills/verify-in-app/SKILL.md +107 -0
  66. package/migrations/app-styling-utilities.md +63 -0
  67. package/migrations/list-state-query-form.md +20 -0
  68. package/migrations/nx-layout.md +29 -0
  69. package/migrations/sdk-components-over-hand-built-ui.md +27 -0
  70. package/migrations/search-query-field.md +52 -0
  71. package/migrations.json +48 -0
  72. package/package.json +19 -3
  73. package/src/index.js +85 -30
  74. package/src/index.js.map +1 -1
  75. package/src/lib/cli-args.d.ts +9 -0
  76. package/src/lib/cli-args.js +48 -0
  77. package/src/lib/cli-args.js.map +1 -0
  78. package/src/lib/commitlint.d.ts +10 -0
  79. package/src/lib/commitlint.js +51 -0
  80. package/src/lib/commitlint.js.map +1 -0
  81. package/src/lib/config.d.ts +41 -5
  82. package/src/lib/config.js +117 -10
  83. package/src/lib/config.js.map +1 -1
  84. package/src/lib/filter.d.ts +2 -0
  85. package/src/lib/filter.js +4 -4
  86. package/src/lib/filter.js.map +1 -1
  87. package/src/lib/frontmatter.d.ts +2 -0
  88. package/src/lib/frontmatter.js +33 -5
  89. package/src/lib/frontmatter.js.map +1 -1
  90. package/src/lib/git-flow/build.d.ts +35 -0
  91. package/src/lib/git-flow/build.js +24 -0
  92. package/src/lib/git-flow/build.js.map +1 -0
  93. package/src/lib/git-flow/config.d.ts +59 -0
  94. package/src/lib/git-flow/config.js +50 -0
  95. package/src/lib/git-flow/config.js.map +1 -0
  96. package/src/lib/git-flow/index.d.ts +6 -0
  97. package/src/lib/git-flow/index.js +23 -0
  98. package/src/lib/git-flow/index.js.map +1 -0
  99. package/src/lib/git-flow/parse.d.ts +49 -0
  100. package/src/lib/git-flow/parse.js +300 -0
  101. package/src/lib/git-flow/parse.js.map +1 -0
  102. package/src/lib/git-flow/rename.d.ts +24 -0
  103. package/src/lib/git-flow/rename.js +70 -0
  104. package/src/lib/git-flow/rename.js.map +1 -0
  105. package/src/lib/git-flow/start.d.ts +49 -0
  106. package/src/lib/git-flow/start.js +57 -0
  107. package/src/lib/git-flow/start.js.map +1 -0
  108. package/src/lib/git-flow/validate.d.ts +34 -0
  109. package/src/lib/git-flow/validate.js +72 -0
  110. package/src/lib/git-flow/validate.js.map +1 -0
  111. package/src/lib/git-flow-command.d.ts +4 -0
  112. package/src/lib/git-flow-command.js +165 -0
  113. package/src/lib/git-flow-command.js.map +1 -0
  114. package/src/lib/git-flow-repair.d.ts +17 -0
  115. package/src/lib/git-flow-repair.js +165 -0
  116. package/src/lib/git-flow-repair.js.map +1 -0
  117. package/src/lib/git-flow-start.d.ts +20 -0
  118. package/src/lib/git-flow-start.js +140 -0
  119. package/src/lib/git-flow-start.js.map +1 -0
  120. package/src/lib/git.d.ts +64 -0
  121. package/src/lib/git.js +131 -0
  122. package/src/lib/git.js.map +1 -0
  123. package/src/lib/gitlab.d.ts +35 -0
  124. package/src/lib/gitlab.js +116 -0
  125. package/src/lib/gitlab.js.map +1 -0
  126. package/src/lib/index.d.ts +2 -0
  127. package/src/lib/index.js +26 -11
  128. package/src/lib/index.js.map +1 -1
  129. package/src/lib/migrate.js +6 -1
  130. package/src/lib/migrate.js.map +1 -1
  131. package/src/lib/output-style-command.d.ts +3 -0
  132. package/src/lib/output-style-command.js +69 -0
  133. package/src/lib/output-style-command.js.map +1 -0
  134. package/src/lib/output-style.d.ts +38 -0
  135. package/src/lib/output-style.js +134 -0
  136. package/src/lib/output-style.js.map +1 -0
  137. package/src/lib/owned-paths.js +27 -3
  138. package/src/lib/owned-paths.js.map +1 -1
  139. package/src/lib/package-runner.d.ts +4 -0
  140. package/src/lib/package-runner.js +86 -0
  141. package/src/lib/package-runner.js.map +1 -0
  142. package/src/lib/plain-text.d.ts +9 -0
  143. package/src/lib/plain-text.js +22 -0
  144. package/src/lib/plain-text.js.map +1 -0
  145. package/src/lib/plan.d.ts +6 -6
  146. package/src/lib/plan.js +164 -40
  147. package/src/lib/plan.js.map +1 -1
  148. package/src/lib/prompt.d.ts +8 -0
  149. package/src/lib/prompt.js +35 -0
  150. package/src/lib/prompt.js.map +1 -0
  151. package/src/lib/render.d.ts +25 -2
  152. package/src/lib/render.js +43 -10
  153. package/src/lib/render.js.map +1 -1
  154. package/src/lib/sessions-command.d.ts +4 -0
  155. package/src/lib/sessions-command.js +16 -0
  156. package/src/lib/sessions-command.js.map +1 -0
  157. package/src/lib/sync.d.ts +0 -1
  158. package/src/lib/sync.js +30 -17
  159. package/src/lib/sync.js.map +1 -1
  160. package/src/lib/targets/agents-skills.js +1 -1
  161. package/src/lib/targets/agents-skills.js.map +1 -1
  162. package/src/lib/targets/claude-hooks.d.ts +3 -23
  163. package/src/lib/targets/claude-hooks.js +22 -86
  164. package/src/lib/targets/claude-hooks.js.map +1 -1
  165. package/src/lib/targets/claude-mods.d.ts +20 -0
  166. package/src/lib/targets/claude-mods.js +47 -0
  167. package/src/lib/targets/claude-mods.js.map +1 -0
  168. package/src/lib/targets/claude.js +2 -3
  169. package/src/lib/targets/claude.js.map +1 -1
  170. package/src/lib/targets/codex-hooks.d.ts +12 -0
  171. package/src/lib/targets/codex-hooks.js +35 -0
  172. package/src/lib/targets/codex-hooks.js.map +1 -0
  173. package/src/lib/targets/codex.js +4 -4
  174. package/src/lib/targets/codex.js.map +1 -1
  175. package/src/lib/targets/copilot.js +4 -4
  176. package/src/lib/targets/copilot.js.map +1 -1
  177. package/src/lib/targets/cursor.js +3 -3
  178. package/src/lib/targets/cursor.js.map +1 -1
  179. package/src/lib/targets/git-hooks.d.ts +25 -0
  180. package/src/lib/targets/git-hooks.js +71 -0
  181. package/src/lib/targets/git-hooks.js.map +1 -0
  182. package/src/lib/targets/hooks-shared.d.ts +60 -0
  183. package/src/lib/targets/hooks-shared.js +130 -0
  184. package/src/lib/targets/hooks-shared.js.map +1 -0
  185. package/src/lib/targets/mcp.d.ts +16 -0
  186. package/src/lib/targets/mcp.js +68 -0
  187. package/src/lib/targets/mcp.js.map +1 -0
  188. package/src/lib/targets/shared.d.ts +1 -9
  189. package/src/lib/targets/shared.js +4 -8
  190. package/src/lib/targets/shared.js.map +1 -1
  191. package/src/lib/timetrack-command.d.ts +11 -0
  192. package/src/lib/timetrack-command.js +1063 -0
  193. package/src/lib/timetrack-command.js.map +1 -0
  194. package/src/lib/timetrack-snapshot.d.ts +55 -0
  195. package/src/lib/timetrack-snapshot.js +120 -0
  196. package/src/lib/timetrack-snapshot.js.map +1 -0
  197. package/src/lib/timetrack.d.ts +561 -0
  198. package/src/lib/timetrack.js +279 -0
  199. package/src/lib/timetrack.js.map +1 -0
  200. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: api-source
3
+ description: How to read the source of the API an app in this repo talks to, from a local backend checkout - the one apiRepoPaths names, or the managed clone et api keeps in .ethlete/<name>. Read when a response shape, a status code, an error body, an enum or an auth rule has to be confirmed rather than guessed - and never edit that checkout as part of work in this repo.
4
+ kind: skill
5
+ scope: consumer
6
+ ---
7
+
8
+ # Reading the API source
9
+
10
+ The client's types describe what the frontend _expects_. When the two disagree, the
11
+ server is right, so a question about the contract is answered in the API repository -
12
+ not by reading the frontend's own models harder.
13
+
14
+ Reach for the checkout when:
15
+
16
+ - a response arrives with a field, a shape or a `null` the frontend types do not allow
17
+ - you need the exact status code, error body or validation message for a failure path
18
+ - an enum, a permission or a filter parameter has to match the server's list exactly
19
+ - a request fails and you cannot tell whether the client or the server is wrong
20
+ - you are about to report or fix something in the API itself
21
+
22
+ Do not read it to design frontend code that the API does not serve yet. An endpoint in
23
+ the checkout is not deployed until the API team ships it.
24
+
25
+ ## 1. Resolve the checkout for your app
26
+
27
+ A checkout can come from two places, and `et api` checks them in this order:
28
+
29
+ 1. `apiRepoPaths[<name>]` in the gitignored `ethlete.config.local.json` at the repo root.
30
+ 2. The managed clone `.ethlete/<name>` in the repo root, which `et api clone` creates.
31
+
32
+ A missing `apiRepoPaths` entry is therefore not a missing checkout. Look for the managed clone
33
+ before you report anything.
34
+
35
+ ### With `ethlete.apis.js`
36
+
37
+ When the repo root has `ethlete.apis.js` (or `ethlete.apis.cjs`), it declares each API by name.
38
+ Find the API your app calls in that file, then let the CLI resolve it:
39
+
40
+ ```bash
41
+ yarn et api help <name> # url, services, and the checkout path and state
42
+ ```
43
+
44
+ The `Checkout` section names the directory to search, or the problem that stops it. The `url`
45
+ line is where the local API runs. Use this output, not a memory note, an old handoff or a sibling
46
+ folder that looks right: the configuration on disk can have changed since they were written.
47
+
48
+ If the CLI reports no checkout and the API declares a `repoUrl`, the fix is
49
+ `yarn et api clone <name>`. Ask the user before you run it. It uses the network and writes a
50
+ clone into the repo.
51
+
52
+ ### Without `ethlete.apis.js`
53
+
54
+ The paths are a map in `ethlete.config.local.json`:
55
+
56
+ ```json
57
+ {
58
+ "apiRepoPaths": {
59
+ "hub": "../fut-hub-backend",
60
+ "*": "../shared-backend"
61
+ },
62
+ "apiRepoBranches": {
63
+ "hub": "develop",
64
+ "*": "main"
65
+ }
66
+ }
67
+ ```
68
+
69
+ Older checkouts still keep these keys in `ethlete-agents.config.local.json`. Read that file as a
70
+ fallback when `ethlete.config.local.json` is absent.
71
+
72
+ - **The key is the API name** from `ethlete.apis.js`. Without that file, it is the app as this
73
+ repo names it - the workspace project name, normally also the folder under `apps/`.
74
+ - **A relative path resolves from the repo root**, not from the app folder.
75
+ - **Require an exact key.** If several apps intentionally share one API, configure the explicit
76
+ `"*"` fallback. Never treat an unrelated single entry as a fallback.
77
+
78
+ ### No checkout anywhere - stop and ask
79
+
80
+ Do not guess a sibling folder, and do not clone without approval. Say which app you needed
81
+ the API for, and offer the `apiRepoPaths` snippet above. The file is gitignored, so adding it
82
+ changes nothing for anyone else.
83
+
84
+ Without a checkout, fall back to what the running API tells you: the generated API
85
+ description if the project serves one (`/openapi.json`, `/swagger`, `/api/doc`), and the
86
+ real response body of the call you are debugging.
87
+
88
+ ## 2. Resolve the relevant files before judging checkout state
89
+
90
+ Search by the route, field, or error you already know. Record the contract, serializer,
91
+ handler, and tests that can answer the question. Then inspect checkout state only for
92
+ those paths:
93
+
94
+ ```bash
95
+ git -C <apiRepoPath> status -sb
96
+ git -C <apiRepoPath> status --short -- <relevant-paths>
97
+ git -C <apiRepoPath> diff -- <relevant-paths>
98
+ ```
99
+
100
+ Unrelated dirty files are not evidence about these paths. Continue without blocking on
101
+ them. If a relevant file is dirty, distinguish the worktree implementation from the
102
+ committed or deployed behavior. Ask only when that difference changes the answer or the
103
+ user has to choose which behavior matters.
104
+
105
+ `apiRepoBranches` configures the expected branch per exact app key, with `"*"` as the
106
+ only fallback. If no branch is configured, report the current branch as context; do not
107
+ invent a blocking “development branch” requirement. A different or ahead branch matters
108
+ only when the relevant files differ in that commit range.
109
+
110
+ Use existing remote refs first. `git fetch` preserves the worktree but is not read-only:
111
+ it uses the network and mutates remote refs. Fetch only when freshness is material, and
112
+ request any approval the environment requires. Never switch, pull, stash, or reset the
113
+ checkout yourself.
114
+
115
+ ## 3. The checkout is not the environment the app calls
116
+
117
+ Even a clean, current checkout is only the source. The app talks to a deployed
118
+ environment, which can lag it:
119
+
120
+ - Read the app's API base URL from its environment config, and say which environment
121
+ your answer is about.
122
+ - When the source and the observed response disagree, **the observed response wins** for
123
+ what the app has to handle today. Report the difference instead of writing client code
124
+ against the newer source.
125
+
126
+ ## 4. Search it, don't read it whole
127
+
128
+ The stack is whatever the API team chose, so search by the thing you already know - the
129
+ path, the field name, the error message - rather than by an expected file layout:
130
+
131
+ ```bash
132
+ rg -n "api/v1/matches" <apiRepoPath> --glob '!*test*' # the route
133
+ rg -n "kickoffAt" <apiRepoPath> # a field of the payload
134
+ rg -n "MATCH_NOT_FOUND" <apiRepoPath> # an error code you received
135
+ ```
136
+
137
+ Two things to find first, because they answer most questions on their own:
138
+
139
+ - **The API description** the project generates or checks in (OpenAPI, Swagger, GraphQL
140
+ schema, `.http` files). It is the contract, and it is cheaper to read than the code.
141
+ - **The serializer, resource or DTO** for the entity - the field list the client sees,
142
+ which is usually a subset of the database model, and the place where a name is
143
+ rewritten between the two.
144
+
145
+ The API's own tests describe intended behaviour more directly than the implementation:
146
+ they name the status code and the body for each case.
147
+
148
+ ## 5. The checkout is read-only from here
149
+
150
+ It is a different repository with its own branch, review and release process. Never edit
151
+ it while working on a task in this repo, and never change its git state without being
152
+ asked. A fetch also needs to meet the freshness and approval conditions above.
153
+
154
+ When the fix belongs in the API, say so precisely: the endpoint, the field, and the
155
+ behaviour it should have. Then handle the API as it is today - a client workaround for a
156
+ server bug is a decision for the user, and it needs a comment naming what it waits on.
@@ -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%}.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: codex-subagent
3
+ description: Delegate a task to a Codex agent (gpt-6-luna, gpt-6.1-terra, gpt-6.1-sol, gpt-6-astra) instead of a Claude subagent - a second opinion from another model family, a review, or a cheap parallel lookup. Read before spawning a subagent when another model would answer better, and whenever the user says codex, GPT, or "second opinion".
4
+ kind: skill
5
+ scope: both
6
+ ---
7
+
8
+ # Codex as a subagent
9
+
10
+ `codex exec` runs one Codex agent to completion without a terminal. It is a subagent with its
11
+ own context, its own tools and its own model family. It reads the repository's `AGENTS.md` by
12
+ itself, so it starts with the same rules you have.
13
+
14
+ Use it when the task gains from a model that is not Claude: a second opinion on a design, a
15
+ review of your own diff, a bug hunt that your reading already failed, or work you want to run
16
+ beside your own. Use it also when the Claude usage limit is close, because a Codex run does not
17
+ draw on it - move the delegatable work there and keep the limit for the work you lead. For
18
+ everything else, a Claude subagent stays the cheaper choice.
19
+
20
+ ## The call
21
+
22
+ The wrapper is {%resource:codex-agent.mjs%}. It keeps the Codex trace in a log file and prints
23
+ only the final message, so one run costs you a few hundred tokens instead of the whole trace.
24
+ Below, `<skill-dir>` is the directory this file sits in.
25
+
26
+ ```bash
27
+ node <skill-dir>/codex-agent.mjs -m gpt-6.1-terra -e medium "<prompt>"
28
+ ```
29
+
30
+ Options: `-m` model, `-e` effort (`low`, `medium`, `high`, `xhigh`), `-C` working root,
31
+ `--write`, `--resume <thread>`, `--prompt-file <file>`, `--schema <json-schema-file>`,
32
+ `--timeout <seconds>`, `--trace`. Run it with `--help` for the full list.
33
+
34
+ The run prints a footer with the model, the sandbox, the time, the output tokens, the resume
35
+ id and the log path. Read the log only when a run fails.
36
+
37
+ ## Pick the model
38
+
39
+ | Model | The work it fits |
40
+ | --------------- | ------------------------------------------------------------------------------------------------------------ |
41
+ | `gpt-6-luna` | Fast and cheap: a lookup, a grep, a question about one file. |
42
+ | `gpt-6.1-terra` | Balanced agentic coding. The default for real work, and the wrapper's default. |
43
+ | `gpt-6.1-sol` | A reliable workhorse for a straight task. Its usage limit runs out fast, so ask the user before you pick it. |
44
+ | `gpt-6-astra` | Complex, demanding work. The most expensive of the four, so ask the user before you pick it. |
45
+
46
+ Effort follows both the flag and the prompt. Pass `-e low` for a lookup and `-e high` for a
47
+ hard bug. Say "keep it brief" in the prompt when you want a short answer.
48
+
49
+ ## Read-only by default
50
+
51
+ A run without `--write` gets the `read-only` sandbox. Codex can read and run commands, but it
52
+ cannot change a file. This is the right mode for a review, a second opinion and research.
53
+
54
+ Pass `--write` only when the user asked for a code change from Codex. Two rules hold then:
55
+
56
+ 1. **Codex must not commit, push, or run `git add`.** Say so in the prompt. Other sessions
57
+ share this checkout, so a broad stage takes work that is not yours.
58
+ 2. **Name the files it may touch.** An open brief in a shared checkout is how two agents edit
59
+ the same file.
60
+
61
+ `--resume` carries the sandbox and the working root of the thread it resumes. A thread that
62
+ started read-only stays read-only, so start a new run when you need write access.
63
+
64
+ ## Run several at once
65
+
66
+ The wrapper blocks until the agent is done, which takes minutes for real work. Start each run
67
+ as a background command, and collect the answers when they arrive. Give each run its own
68
+ question. Two agents on one question waste a model.
69
+
70
+ ## Write the prompt for a stranger
71
+
72
+ The Codex agent shares no context with you. It sees your prompt, the repository and
73
+ `AGENTS.md`. So:
74
+
75
+ - State the task as one question, and name what "done" means.
76
+ - Give the file paths you already know. It costs a model minutes to find what you can name.
77
+ - Ask for the shape of the answer: a verdict, a list of findings with `file:line`, a patch in
78
+ a fenced block.
79
+ - Do not paste the repository rules. It reads them itself.
80
+
81
+ Pass `--schema` with a JSON Schema file when you want the answer as JSON you can parse.
82
+
83
+ ## Before you reach for it
84
+
85
+ - `codex` must be on `PATH` and logged in. If it is not, tell the user and stop. Do not
86
+ install it, and do not write a token into the repository.
87
+ - If Codex reports that the project is not trusted, ask the user to run `codex` once in the
88
+ repository and trust it there.
89
+ - A Codex run is not free. Prefer it where a second model earns its cost, not for the
90
+ twentieth grep.
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run one Codex agent to completion and print only its final message.
4
+ *
5
+ * `codex exec` streams its whole reasoning and command trace to stdout. That trace costs a
6
+ * calling agent more context than the answer is worth, so it stays in a log file here.
7
+ *
8
+ * Usage: node codex-agent.mjs [options] "<prompt>"
9
+ */
10
+
11
+ import { spawn } from 'node:child_process';
12
+ import { createWriteStream, mkdirSync, readFileSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { createInterface } from 'node:readline';
16
+
17
+ const DEFAULT_MODEL = 'gpt-6.1-terra';
18
+ const DEFAULT_TIMEOUT_SECONDS = 900;
19
+
20
+ const USAGE = `Usage: node codex-agent.mjs [options] "<prompt>"
21
+
22
+ -m, --model <id> Model id (default ${DEFAULT_MODEL})
23
+ -e, --effort <level> Reasoning effort: low | medium | high | xhigh
24
+ -w, --write Let the agent edit the working tree (default: read-only)
25
+ -C, --cd <dir> Working root for the agent (default: the current directory)
26
+ --resume <thread> Continue an earlier thread instead of starting one
27
+ --prompt-file <f> Read the prompt from a file
28
+ --schema <file> JSON Schema the final message must match
29
+ --timeout <sec> Give up after this many seconds (default ${DEFAULT_TIMEOUT_SECONDS})
30
+ --trace Also print the command and file-change trace
31
+ -h, --help Print this help`;
32
+
33
+ const parseArgs = (argv) => {
34
+ const options = {
35
+ model: DEFAULT_MODEL,
36
+ effort: '',
37
+ write: false,
38
+ cd: process.cwd(),
39
+ resume: '',
40
+ promptFile: '',
41
+ schema: '',
42
+ timeout: DEFAULT_TIMEOUT_SECONDS,
43
+ trace: false,
44
+ prompt: '',
45
+ };
46
+
47
+ const rest = [];
48
+
49
+ for (let index = 0; index < argv.length; index += 1) {
50
+ const arg = argv[index];
51
+ const next = () => {
52
+ index += 1;
53
+ if (index >= argv.length) throw new Error(`${arg} needs a value.`);
54
+ return argv[index];
55
+ };
56
+
57
+ if (arg === '-m' || arg === '--model') options.model = next();
58
+ else if (arg === '-e' || arg === '--effort') options.effort = next();
59
+ else if (arg === '-w' || arg === '--write') options.write = true;
60
+ else if (arg === '-C' || arg === '--cd') options.cd = next();
61
+ else if (arg === '--resume') options.resume = next();
62
+ else if (arg === '--prompt-file') options.promptFile = next();
63
+ else if (arg === '--schema') options.schema = next();
64
+ else if (arg === '--timeout') options.timeout = Number(next());
65
+ else if (arg === '--trace') options.trace = true;
66
+ else if (arg === '-h' || arg === '--help') options.help = true;
67
+ else if (arg.startsWith('-') && arg !== '-') throw new Error(`Unknown option ${arg}.`);
68
+ else rest.push(arg);
69
+ }
70
+
71
+ options.prompt = options.promptFile ? readFileSync(options.promptFile, 'utf8') : rest.join(' ');
72
+
73
+ return options;
74
+ };
75
+
76
+ const readStdin = () =>
77
+ new Promise((resolve) => {
78
+ let buffer = '';
79
+ process.stdin.setEncoding('utf8');
80
+ process.stdin.on('data', (chunk) => (buffer += chunk));
81
+ process.stdin.on('end', () => resolve(buffer));
82
+ });
83
+
84
+ const buildArgs = (options, lastMessageFile) => {
85
+ const args = ['exec'];
86
+
87
+ if (options.resume) args.push('resume');
88
+
89
+ args.push('--json', '--skip-git-repo-check', '-o', lastMessageFile, '-m', options.model);
90
+
91
+ // `codex exec resume` carries the sandbox and the working root of the session it resumes,
92
+ // and rejects both flags, so only a fresh run may pass them.
93
+ if (!options.resume) {
94
+ args.push('-C', options.cd, '--sandbox', options.write ? 'workspace-write' : 'read-only');
95
+ }
96
+
97
+ if (options.effort) args.push('-c', `model_reasoning_effort="${options.effort}"`);
98
+ if (options.schema) args.push('--output-schema', options.schema);
99
+ if (options.resume) args.push(options.resume);
100
+
101
+ args.push('-');
102
+
103
+ return args;
104
+ };
105
+
106
+ const describeItem = (item) => {
107
+ if (item.type === 'command_execution') return `$ ${String(item.command ?? '').split('\n')[0]}`;
108
+ if (item.type === 'file_change') {
109
+ const changes = Array.isArray(item.changes) ? item.changes : [];
110
+ return `~ ${changes.map((change) => `${change.kind ?? 'edit'} ${change.path ?? ''}`).join(', ')}`;
111
+ }
112
+ if (item.type === 'error') return `! ${item.message ?? 'unknown error'}`;
113
+ return '';
114
+ };
115
+
116
+ const duration = (milliseconds) => {
117
+ const seconds = Math.round(milliseconds / 1000);
118
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
119
+ };
120
+
121
+ const run = async () => {
122
+ const options = parseArgs(process.argv.slice(2));
123
+
124
+ if (options.help) {
125
+ process.stdout.write(`${USAGE}\n`);
126
+ return 0;
127
+ }
128
+
129
+ if (!options.prompt.trim() && !process.stdin.isTTY) options.prompt = await readStdin();
130
+
131
+ if (!options.prompt.trim()) {
132
+ process.stderr.write(`${USAGE}\n`);
133
+ return 2;
134
+ }
135
+
136
+ const logDir = join(tmpdir(), 'codex-agent');
137
+ mkdirSync(logDir, { recursive: true });
138
+
139
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
140
+ const logFile = join(logDir, `${stamp}.jsonl`);
141
+ const lastMessageFile = join(logDir, `${stamp}.last.md`);
142
+ const log = createWriteStream(logFile);
143
+ const started = Date.now();
144
+
145
+ const child = spawn('codex', buildArgs(options, lastMessageFile), {
146
+ stdio: ['pipe', 'pipe', 'pipe'],
147
+ env: process.env,
148
+ });
149
+
150
+ child.stdin.end(options.prompt);
151
+
152
+ const state = { thread: '', usage: null, errors: [], trace: [], failed: false };
153
+
154
+ createInterface({ input: child.stdout }).on('line', (line) => {
155
+ log.write(`${line}\n`);
156
+
157
+ let event;
158
+
159
+ try {
160
+ event = JSON.parse(line);
161
+ } catch {
162
+ return;
163
+ }
164
+
165
+ if (event.type === 'thread.started') state.thread = event.thread_id ?? '';
166
+ else if (event.type === 'turn.completed') state.usage = event.usage ?? null;
167
+ else if (event.type === 'turn.failed') {
168
+ state.failed = true;
169
+ state.errors.push(event.error?.message ?? 'the turn failed');
170
+ } else if (event.type === 'item.completed') {
171
+ const description = describeItem(event.item ?? {});
172
+ if (description) state.trace.push(description);
173
+ if (event.item?.type === 'error') state.errors.push(event.item.message ?? 'unknown error');
174
+ }
175
+ });
176
+
177
+ let stderrText = '';
178
+ child.stderr.setEncoding('utf8');
179
+ child.stderr.on('data', (chunk) => (stderrText += chunk));
180
+
181
+ const timer = setTimeout(() => child.kill('SIGKILL'), Math.max(1, options.timeout) * 1000);
182
+ const code = await new Promise((resolve) => child.on('close', resolve));
183
+
184
+ clearTimeout(timer);
185
+ log.end();
186
+
187
+ let finalMessage;
188
+
189
+ try {
190
+ finalMessage = readFileSync(lastMessageFile, 'utf8').trim();
191
+ rmSync(lastMessageFile, { force: true });
192
+ } catch {
193
+ finalMessage = '';
194
+ }
195
+
196
+ const failed = code !== 0 || state.failed || (!finalMessage && state.errors.length > 0);
197
+
198
+ if (options.trace && state.trace.length > 0) process.stdout.write(`${state.trace.join('\n')}\n\n`);
199
+ if (finalMessage) process.stdout.write(`${finalMessage}\n`);
200
+
201
+ if (failed) {
202
+ const reason = state.errors.join('\n') || stderrText.trim() || `codex exited with code ${code}`;
203
+ process.stdout.write(`\ncodex agent FAILED: ${reason}\n`);
204
+ }
205
+
206
+ const parts = [
207
+ `codex ${options.model}`,
208
+ options.write ? 'workspace-write' : 'read-only',
209
+ duration(Date.now() - started),
210
+ ];
211
+
212
+ if (state.usage) parts.push(`${state.usage.output_tokens ?? 0} out tok`);
213
+
214
+ process.stdout.write(`\n--- ${parts.join(' · ')}\n`);
215
+
216
+ if (state.thread) process.stdout.write(`resume: --resume ${state.thread}\n`);
217
+
218
+ process.stdout.write(`log: ${logFile}\n`);
219
+
220
+ return failed ? 1 : 0;
221
+ };
222
+
223
+ run().then(
224
+ (code) => process.exit(code),
225
+ (error) => {
226
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
227
+ process.exit(2);
228
+ },
229
+ );