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

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 +269 -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 +187 -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
package/README.md CHANGED
@@ -15,6 +15,10 @@ npx ethlete-agents init # writes ethlete-agents.config.json
15
15
  npx ethlete-agents sync # writes the generated rules and skills
16
16
  ```
17
17
 
18
+ `init` pre-fills `vars` with the lint, format and Storybook commands for your repo; check them before
19
+ the first sync. `sync`, `check`, `init` and `migrate` reject a flag they do not know and print the usage
20
+ instead of running, and a config file that does not parse is reported by name.
21
+
18
22
  Commit the generated files, and add a drift check to CI:
19
23
 
20
24
  ```bash
@@ -99,12 +103,26 @@ Prettier rewrites them and `check` then reports drift on every run:
99
103
  `.agents/skills/` is the cross-tool baseline) and adds `claude`, `cursor` or `copilot`
100
104
  when their directory exists; or list an explicit subset.
101
105
  - **`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.
106
+ content. `"sdk"` emits `scope: sdk` and `scope: both`; the SDK repo uses it so its own
107
+ hand-written, authoring-side guides are not overwritten by the consumer-side versions.
104
108
  - **`vars`** - values for the template tokens a guide declares. Defaults live in
105
109
  `content/defaults.json`; a guide whose variable has no default and no value is
106
- skipped with a warning rather than emitted with a dangling placeholder.
107
- - **`exclude`** - content names to skip entirely.
110
+ skipped with a warning rather than emitted with a dangling placeholder. Some are
111
+ derived from the repo instead of defaulted - the `gitFlow*` ones from the `gitFlow`
112
+ block, `packageRunner` (`yarn`, `pnpm exec`, `bunx` or `npx`) from the root
113
+ `package.json`'s `packageManager` or the lockfile, and `commitRuleSource` / `commitValidation` from whether a commitlint config
114
+ exists (`commitlint.config.*`, `.commitlintrc*`, or a `commitlint` key in
115
+ `package.json`). Without one, the git-commit guide presents the format as the repo's
116
+ convention and never mentions a `commitlint` run - an agent that goes looking for a
117
+ promised validator and finds nothing reports the discrepancy instead of just
118
+ committing. `lintCommand` / `lintFixCommand` run the repo's `lint` script, else
119
+ `nx lint <project>` when Nx is installed, and `storybookStartCommand` runs its `storybook`
120
+ script; `sync` warns when a lint var names a script `package.json` does not have. Setting
121
+ any derived var in `vars` overrides the detection.
122
+ - **`exclude`** - rule or skill names to skip entirely for every configured agent and
123
+ developer. For example, `"exclude": ["git-flow", "handoff"]` prevents those skills
124
+ from being generated; the next `sync` also removes copies generated previously. Unknown
125
+ names produce a warning so a typo cannot silently leave a skill enabled.
108
126
  - **`claudeMdImportsAgentsMd`** - set (usually by `migrate`) when `CLAUDE.md` is an
109
127
  `@AGENTS.md` import or symlink; the claude target then skips `.claude/rules/ethlete/`
110
128
  so the rules don't load twice. `sync` warns when the flag is set but the import is
@@ -113,56 +131,346 @@ Prettier rewrites them and `check` then reports drift on every run:
113
131
  Content that declares `requires` is only emitted when those packages are installed, so
114
132
  a repo without `@ethlete/query` never sees the query guide.
115
133
 
134
+ ## Git flow
135
+
136
+ The branch convention lives in the same config, as one machine-readable grammar that the
137
+ CLI, a git hook, a CI job and `@ethlete/timetrack` all read:
138
+
139
+ ```json
140
+ {
141
+ "gitFlow": {
142
+ "keyPrefixes": ["FIP"],
143
+ "baseBranches": { "development": "next", "production": "main" }
144
+ }
145
+ }
146
+ ```
147
+
148
+ ```bash
149
+ npx ethlete-agents git-flow start FIP-2177 # name it and branch off the right base
150
+ npx ethlete-agents git-flow check # the current branch
151
+ npx ethlete-agents git-flow check "$SOURCE" --target "$TARGET"
152
+ npx ethlete-agents git-flow check --all # adoption report
153
+ npx ethlete-agents git-flow repair dev-game-codes --key FIP-2900
154
+ npx ethlete-agents git-flow explain feat/FIP-2177-user-management
155
+ ```
156
+
157
+ The shapes:
158
+
159
+ | Shape | Branch from | Merges into |
160
+ | ------------------------------------------ | ----------------------- | ------------------------------ |
161
+ | `feat/<KEY>-<subject>` | development | development |
162
+ | `sub/feat/<KEY>-<subject>/<KEY>-<subject>` | the main feature branch | the main feature branch |
163
+ | `release/<YYYY.MM.DD>` | development | development **and** production |
164
+ | `sub/release/<YYYY.MM.DD>/<KEY>-<subject>` | the release branch | the release branch |
165
+ | `hotfix/<KEY>-<subject>` | production | production |
166
+
167
+ **Why nested branches carry a `sub/` prefix.** Git refuses a ref that is both a branch and
168
+ a directory of branches, so `feat/FIP-2177-user-management/FIP-2178-reset` cannot exist
169
+ while `feat/FIP-2177-user-management` does - the push is rejected with `refname conflict`.
170
+ The prefix moves the nested tree out of the way while keeping the parent's full path inside
171
+ the child's name, so the merge request target is still derivable from the name alone. The
172
+ unprefixed spelling still parses, reports why it cannot exist, and `repair` moves it.
173
+ Configurable as `subPrefix`.
174
+
175
+ - **`enforcement`** - `"advisory"` (default) reports everything and blocks nothing, so a
176
+ repo can adopt the convention before it gates on it. `"gated"` applies each rule's
177
+ `severity`. A direct push to a base branch is blocked in both modes, and
178
+ `wrong-mr-target` can be raised to `"error"` on its own without ending the naming
179
+ grace period.
180
+ - **`keyPrefixes`** - the project's issue prefixes. Leave it empty and anything shaped
181
+ like `keyPattern` counts, which reads `chore/angular-22` as issue `ANGULAR-22`.
182
+ - **`severity`** - per rule: `unknown-type`, `missing-key`, `key-case`,
183
+ `missing-subject`, `type-alias`, `deprecated-prefix`, `release-date`,
184
+ `wrong-mr-target`, `protected-push`.
185
+ - **`deprecatedShapes`** - legacy spellings that still classify correctly and only earn a
186
+ rename suggestion. `dev-*` ships as the old spelling of a main feature branch.
187
+
188
+ The grammar is also importable on its own - `@ethlete/agent-rules/git-flow` has no
189
+ dependencies and touches no Node built-ins, so it runs in a browser:
190
+
191
+ ```ts
192
+ import { parseBranch, planStart, resolveGitFlowConfig } from '@ethlete/agent-rules/git-flow';
193
+
194
+ const { storyKey, taskKey, findings } = parseBranch({ branch, config: resolveGitFlowConfig() });
195
+ ```
196
+
197
+ ### `start` - the prospective flow
198
+
199
+ `git-flow start <KEY>` reads the issue from Jira, computes the name from the grammar and
200
+ creates the branch off the correct base. It prints the plan first and asks before writing;
201
+ `--dry-run` stops after the plan and `--yes` skips the question. It refuses on a dirty
202
+ working tree, when the branch already exists, and when the base branch is nowhere to be
203
+ found.
204
+
205
+ A Task with a parent Story nests under that Story's feature branch, which therefore has to
206
+ exist already - `start` says so rather than inventing a parent. `--of <branch>` picks the
207
+ parent explicitly, `--hotfix` branches off production, `--release <date>` makes a release
208
+ branch, and `--subject <text>` skips Jira entirely.
209
+
210
+ The issue is read through the Timetrack app - see [Jira, through Timetrack](#jira-through-timetrack).
211
+ No repository holds a Jira credential, so the only thing the committed config still says about
212
+ Jira is how an issue type becomes a branch type:
213
+
214
+ ```json
215
+ {
216
+ "jira": {
217
+ "typeByIssueType": { "Bug": "fix" }
218
+ }
219
+ }
220
+ ```
221
+
222
+ - **`typeByIssueType`** - the branch type per Jira issue type; anything unlisted becomes
223
+ `feat`. `--type` overrides it per call.
224
+
225
+ The branch subject comes from the instance's own subject field, which Timetrack resolves
226
+ because the field id is a property of the instance rather than of this repo. An issue that
227
+ sets no subject falls back to its summary, and the printed plan says which of the two it used.
228
+
229
+ ### `repair` - renaming a branch that does not conform
230
+
231
+ `git-flow repair [ref]` derives the conforming name (`--key FIP-2900` when the old name
232
+ carries no issue key, `--to <branch>` to override), renames the branch locally and on the
233
+ remote, and retargets the open merge requests aimed at it through the GitLab API.
234
+ `GITLAB_TOKEN` needs the `api` scope, and is sent only to the hosts named in `GITLAB_HOST`
235
+ (comma-separated) or, in a GitLab CI job, `CI_SERVER_HOST`.
236
+
237
+ Everything is checked before the first mutation, and it refuses rather than half-finishing:
238
+
239
+ - An open merge request whose **source** is the branch blocks the repair. GitLab cannot
240
+ move a merge request to another source branch, and closing it would lose its discussion -
241
+ merge or close it first.
242
+ - A branch that is pushed but whose merge requests cannot be listed (no token, or a remote
243
+ that is not a configured GitLab host) blocks too. `--no-mr-check` asserts that none point at it.
244
+ - If a retarget fails halfway, the old branch is still there and the recovery commands are
245
+ printed.
246
+
247
+ ## Jira, through Timetrack
248
+
249
+ A Jira token in every repository is a secret nobody can rotate. There is one on this machine
250
+ instead, in the Timetrack app's keychain entry, and every repository asks the running app:
251
+
252
+ ```bash
253
+ npx ethlete-agents timetrack status # is it reachable, and what does it hold?
254
+ npx ethlete-agents timetrack issue FIP-2177 # summary, type, parent, branch subject
255
+ npx ethlete-agents timetrack search "password" # open issues of the picked projects
256
+ npx ethlete-agents timetrack project # which project does this repo log into?
257
+ npx ethlete-agents timetrack create --summary "…" # file a ticket with the instance's settings
258
+ npx ethlete-agents timetrack log --issue FIP-2177 --minutes 45
259
+ npx ethlete-agents timetrack approval <id> # where a queued write stands
260
+ ```
261
+
262
+ `timetrack snapshot [from] [to] [--out <file>]` records the rows of real days (default: the last 7) with
263
+ the git HEAD and uncommitted files of the current repo; without `--out` the file lands in the app's data
264
+ directory under `snapshots/`, never in a repo. `timetrack snapshot --compare <file>` reads the same days
265
+ again and prints every row gone, new or changed (`--json` for a script); it exits 1 when anything changed.
266
+
267
+ `--json` prints the raw answer instead of lines. `git-flow start` uses the same channel.
268
+
269
+ **Every write waits for the user.** `create`, `log` and every other write answer at once with an
270
+ approval id and write nothing; the user approves or rejects the request in the app, and
271
+ `approval <id>` reads the outcome. A request still waiting at the end of its day expires.
272
+ `TIMETRACK_CLIENT` names the caller in the queue.
273
+
274
+ **How it connects.** The app binds a loopback socket and writes its port and a fresh token
275
+ into `agent.json` in its own data directory (`~/.local/share/io.ethlete.timetrack/` on Linux,
276
+ `~/Library/Application Support/…` on macOS, `%APPDATA%\…` on Windows), readable by its owner
277
+ alone. The token lives no longer than the run, so a caller left over from an earlier one is
278
+ refused rather than trusted, and a request carrying an `Origin` header is refused outright -
279
+ a page the user happens to have open must not reach Jira through a port it guessed.
280
+ `TIMETRACK_AGENT_DISCOVERY` names the file for a machine that keeps it elsewhere.
281
+
282
+ **What the app answers, not this package.** The instance, the credentials, the picked
283
+ projects, the subject field and the ticket shape are all settings there. That is what makes
284
+ `create` file a ticket indistinguishable from one the app filed, and it is why none of them
285
+ appear in a repo's config any more.
286
+
287
+ **`log` writes to the day, not to Tempo.** It adds the same row the app's own timeline draws
288
+ for work nothing observed, and the user reviews the day before syncing it. A worklog posted
289
+ behind the review would double-book against whatever the evidence already proposed for that
290
+ hour.
291
+
292
+ **When the app is not running** every command says so and exits non-zero. There is no
293
+ fallback to an environment variable on purpose - a fallback is how the per-repo secret comes
294
+ back.
295
+
116
296
  ## Hooks (opt-in)
117
297
 
118
- Claude Code hooks run commands on the developer's machine, so none are emitted by
119
- default - opt in per hook in the config:
298
+ Hooks run commands on the developer's machine, so none are emitted by default - opt in
299
+ per hook in the config:
120
300
 
121
301
  ```json
122
302
  {
123
- "hooks": ["context-warning"]
303
+ "hooks": ["context-warning", "subagent-model-policy"]
124
304
  }
125
305
  ```
126
306
 
127
- `sync` writes the script to `.claude/hooks/ethlete/` and registers it in
128
- `.claude/settings.json` (your own entries are left untouched); removing the name from
129
- `hooks` unregisters and deletes it again.
307
+ They are emitted for whichever of the `claude` and `codex` targets is enabled:
308
+
309
+ | Target | Script | Registered in |
310
+ | -------- | ------------------------ | ----------------------- |
311
+ | `claude` | `.claude/hooks/ethlete/` | `.claude/settings.json` |
312
+ | `codex` | `.codex/hooks/ethlete/` | `.codex/hooks.json` |
313
+
314
+ Your own entries in those files are left untouched; removing the name from `hooks`
315
+ unregisters and deletes the script again. Codex only loads project-local hooks once the
316
+ `.codex/` layer is trusted, and honours `[features] hooks = false`.
130
317
 
131
318
  Available hooks:
132
319
 
133
- - **`context-warning`** - warns once per tier (and instructs Claude) when the session
134
- context crosses 70% / 85% of the token budget, recommending `/handoff`. The budget is
135
- capped at the 200k long-context pricing boundary: on 1M-window models every request
136
- past 200k input tokens bills the whole context at a premium rate, so the warnings
137
- fire at ~140k/~170k instead of deep into the expensive range.
320
+ - **`context-warning`** - warns once per tier (and instructs the agent) when the session
321
+ context crosses 70% / 85% / 95% of the token budget, recommending a handoff. Under Claude the
322
+ budget is capped at the 200k long-context pricing boundary: on 1M-window models every
323
+ request past 200k input tokens bills the whole context at a premium rate, so the
324
+ warnings fire at ~140k/~170k instead of deep into the expensive range. Codex uses
325
+ the model-specific 272k pricing boundary for GPT-5.6, GPT-5.5 and GPT-5.4, and the
326
+ rollout's reported context window for models without that pricing rule.
327
+
328
+ The 85% tier asks the agent to choose - finish the task, or hand off - because a session
329
+ is often a step or two from done when it fires. The 95% tier takes that choice away. Every
330
+ prompt after the first warning carries a short reminder instead of a new warning: the
331
+ remaining budget, and how to work so that either ending stays cheap. The `handoff` skill
332
+ calls this handoff mode.
333
+
334
+ Under Claude the hook runs on four events, because a warning that only a user prompt can
335
+ trigger never reaches a long autonomous run. `SessionStart` names the budget before the
336
+ first request. `UserPromptSubmit` carries the tiered warning and the reminder.
337
+ `PostToolBatch` fires between the agent's own model requests, so it learns the count while
338
+ it works. `Stop` fires as a turn ends over 85% and the turn then continues, so the agent
339
+ must take one of three and say which: finish, hand off, or cross the boundary on purpose
340
+ for a reason it states. Each event speaks once per tier. Codex has none of the three, so
341
+ its registration stays on `UserPromptSubmit`.
342
+
343
+ Two things are Claude-only: the separate user-facing line (Codex documents only
344
+ `additionalContext`, so there the warning is folded into the text the model is told to
345
+ relay), and the auto-mode escalation that writes the handoff file unprompted - Codex's
346
+ `permission_mode` values are undocumented, so no value enables it.
347
+
348
+ Codex sub-agents are tracked by their own thread ids even though they share the root
349
+ session id. A sub-agent warning tells that child to report its state to the parent; it
350
+ never tells the user or the main agent to hand off the root session.
351
+
352
+ - **`subagent-model-policy`** (Claude Code only) - gates every call to the subagent tool.
353
+ A call that names no `model` is denied and the model table is returned instead: haiku for
354
+ a lookup, opus for real work, sonnet in between, fable for judgment-heavy work. Without
355
+ it a subagent inherits the model leading the session, which is how an expensive model
356
+ ends up doing every grep it delegates; with it, that mistake costs one tool call.
357
+
358
+ A call that names `fable` asks the user - it is the most expensive option, and only the
359
+ user knows whether the task is judgment-heavy. A fork and an agent type whose own
360
+ definition sets a model both pass untouched, because `model` would change nothing there.
138
361
 
139
362
  Hooks can be turned off per machine - see the local config below.
140
363
 
364
+ ## Git hooks (opt-in)
365
+
366
+ Separate from the agent hooks above, and opt-in for the same reason - a generated block
367
+ that can reject a push is a higher-stakes artifact than a markdown one:
368
+
369
+ ```json
370
+ {
371
+ "gitHooks": ["pre-push", "post-checkout"]
372
+ }
373
+ ```
374
+
375
+ Each one is written as an `# ethlete:git-flow:start` … `end` block **appended** to your
376
+ `.husky/<name>`, so an existing hook there (a git-lfs hook, typically) keeps working and
377
+ keeps reading stdin first - which is why the block never reads stdin itself. Removing the
378
+ name from `gitHooks` takes the block back out and leaves the rest of the file alone.
379
+
380
+ - **`pre-push`** - runs `git-flow check --push` on the current branch. In `advisory` mode
381
+ only a direct push to a base branch can actually stop it.
382
+ - **`post-checkout`** - reports a non-conforming name on a branch that is on no remote yet,
383
+ which is the whole window in which renaming it is free.
384
+
385
+ Only `.husky/` is written, never `.git/hooks/`: the generated files are committed and CI's
386
+ `check` diffs them, so a hook outside the working tree could never be in sync. Without a
387
+ `.husky/` directory `sync` warns and writes nothing. The block calls
388
+ `node_modules/.bin/ethlete-agents` directly rather than through `npx`, so a repo where the
389
+ package is missing gets silence instead of a registry lookup that would fail the push.
390
+ `ETHLETE_GIT_FLOW_SKIP=1` silences both hooks on one machine.
391
+
392
+ ## CI job
393
+
394
+ On GitLab, the merge request target is the half no local hook can see. The job needs no
395
+ configuration beyond the predefined variables:
396
+
397
+ ```yaml
398
+ Git Flow:
399
+ stage: Checks
400
+ rules:
401
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
402
+ allow_failure: true
403
+ script:
404
+ - >
405
+ npx ethlete-agents git-flow check "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
406
+ --target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
407
+ ```
408
+
409
+ `allow_failure: true` on top of `advisory` mode is deliberate belt and braces: the job
410
+ reports for a whole grace period before it can ever be the reason a merge request is red.
411
+
412
+ ## Output styles (Claude Code)
413
+
414
+ An output style replaces Claude Code's own answer style for a whole session. This package
415
+ ships one - **`ste-clarity`**, which writes every answer in ASD-STE100 Simplified Technical
416
+ English - and installs it into the machine's Claude config rather than into a repo, because
417
+ that is where Claude Code reads styles from:
418
+
419
+ ```bash
420
+ npx ethlete-agents output-style # install ste-clarity and switch to it
421
+ npx ethlete-agents output-style --dry-run # print what would change
422
+ npx ethlete-agents output-style --remove # take it back out
423
+ ```
424
+
425
+ Two files, both under `~/.claude` (or `$CLAUDE_CONFIG_DIR`, or `--config-dir <path>`):
426
+
427
+ | File | What changes |
428
+ | ------------------------------ | ------------------------------------------------------------------ |
429
+ | `output-styles/ste-clarity.md` | The style itself, written whole |
430
+ | `settings.json` | `"outputStyle": "ste-clarity"` - every other setting is left alone |
431
+
432
+ - **`--no-activate`** writes the style file but not the setting, so switching to it is then
433
+ `/output-style ste-clarity` inside a session.
434
+ - A style file this command did not write is never overwritten or deleted - it says so and
435
+ stops, and `--force` is the way through. A copy of the same style that differs only in
436
+ layout still counts as its own.
437
+ - Nothing here is repo-local, so `sync` and `check` neither write nor diff it. `--remove`
438
+ clears `outputStyle` only while it still points at that style.
439
+
440
+ **Claude Code only.** Codex has no output-style mechanism - everything it is told comes
441
+ from `AGENTS.md`, which `sync` already writes.
442
+
141
443
  ## Per-machine local config
142
444
 
143
- A gitignored `ethlete-agents.config.local.json` at the repo root holds the values that
144
- differ per developer, without touching any committed file:
445
+ A gitignored `ethlete-agents.config.local.json` at the repo root holds the agent behaviour
446
+ that differs per developer, without touching any committed file:
145
447
 
146
448
  ```json
147
449
  {
148
450
  "disableHooks": true,
149
- "sdkSourcePath": "/absolute/path/to/ethlete-sdk"
451
+ "disableAutoHandoffSave": true
150
452
  }
151
453
  ```
152
454
 
153
455
  - **`disableHooks`** - `true` disables every generated hook; an array
154
456
  (`["context-warning"]`) just the named ones. The hook scripts read the file at
155
457
  runtime, so toggling takes effect on the next prompt - no `sync` needed.
156
- - **`sdkSourcePath`** - a local `ethlete-sdk` checkout. The `sdk-source` and
157
- `sdk-local-build` skills read it when the agent needs the SDK's own sources, or has to
158
- build the SDK and install it here through a `file:` dependency. A relative path is
159
- resolved from the repo root.
458
+ - **`disableAutoHandoffSave`** - keeps the `context-warning` hook's tiered warnings but
459
+ drops the auto-mode escalation: at the critical tier it recommends `/ethlete-handoff`
460
+ instead of saving the handoff file itself.
160
461
 
161
462
  Everything in this file is read at runtime, never by `sync`: the generated files stay
162
463
  identical on every machine and in CI, which is what lets `check` diff them. That is also
163
- why the file takes nothing beyond these keys - `sync`/`check` warn about unknown keys,
164
- and about an `sdkSourcePath` that is missing or is not an SDK checkout. Add the filename
165
- to your repo's `.gitignore`.
464
+ why the file takes nothing beyond these keys - `sync`/`check` warn about unknown keys. Add
465
+ the filename to your repo's `.gitignore`.
466
+
467
+ ### Where the sibling checkouts live
468
+
469
+ `sdkSourcePath`, `apiRepoPaths` and `apiRepoBranches` used to live in the file above. They
470
+ moved to `ethlete.config.local.json`, which [`@ethlete/cli`](../cli/README.md) owns, because
471
+ `et api` needs the same values the skills do. `sync`/`check` still read that file and warn
472
+ about a path that is missing or is not the checkout it claims to be, and they tell you to
473
+ move any of the three keys still found in the agents file.
166
474
 
167
475
  ## Authoring content
168
476
 
@@ -178,11 +486,24 @@ scope: consumer # consumer | sdk | both
178
486
  requires: ['@ethlete/core'] # optional
179
487
  paths: ['**/*.css'] # optional; becomes Claude `paths` and Cursor `globs` on rules
180
488
  vars: [docsBaseUrl] # optional
489
+ modelInvocation: false # optional; a skill only the user may start
181
490
  ---
182
491
  ```
183
492
 
493
+ A content file vendored from another repository keeps its upstream wording apart from the
494
+ link syntax, and names its source in the body. `THIRD-PARTY-LICENSES.md` lists each one,
495
+ its upstream commit and its license.
496
+
497
+ `content/output-styles/<name>.md` is a third kind, and the only one this package does not
498
+ compile: it is a Claude Code output style verbatim, so its frontmatter is Claude's
499
+ (`name`, `description`, `keep-coding-instructions`) and `output-style` installs the file
500
+ as it is, apart from a marker line that records where it came from.
501
+
184
502
  In a body, `{% varName %}` substitutes a variable, `{% skill:other-name %}` links to
185
- another guide the way the current target expects, and `{% resource:file.mjs %}` links to
186
- a bundled file. The delimiter is `{% … %}`, not `{{ … }}`, so Angular templates in
187
- examples pass through untouched. Resource files get variable substitution too, but no
188
- links.
503
+ another emitted package guide, and `{% resource:file.mjs %}` links to a bundled file.
504
+ Skill links are required dependencies: after scope, package, variable, and exclusion
505
+ filtering, `sync` and `check` fail with the source and missing target instead of emitting
506
+ a dangling name. Optional guidance must be self-contained when its guide is absent. Use
507
+ the structured marker instead of a plain `` `name` skill`` reference so validation can
508
+ see it. The delimiter is `{% … %}`, not `{{ … }}`, so Angular templates in examples pass
509
+ through untouched. Resource files get variable substitution too, but no links.
@@ -0,0 +1,35 @@
1
+ # Third-party content
2
+
3
+ Content in this package that comes from another repository, with the license it keeps.
4
+ To update one, copy it again from upstream and record the new commit here.
5
+
6
+ ## [mattpocock/skills](https://github.com/mattpocock/skills)
7
+
8
+ Commit `3cca18b` (2026-09-04). Vendored skills, kept as written upstream apart from the
9
+ generator's own link syntax:
10
+
11
+ - `content/skills/grill-with-docs` - the entry point; calls the other two.
12
+ - `content/skills/grilling`
13
+ - `content/skills/domain-modeling`
14
+
15
+ MIT License
16
+
17
+ Copyright (c) 2026 Matt Pocock
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in all
27
+ copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ if [ "$3" = "1" ] && [ -z "$ETHLETE_GIT_FLOW_SKIP" ] && [ -x node_modules/.bin/ethlete-agents ]; then
2
+ ethlete_branch=$(git rev-parse --abbrev-ref HEAD)
3
+
4
+ # Only while the branch is on no remote: that is the whole window in which renaming it is free.
5
+ if [ -z "$(git for-each-ref --format='%(refname)' "refs/remotes/*/$ethlete_branch")" ]; then
6
+ node_modules/.bin/ethlete-agents git-flow check || true
7
+ fi
8
+
9
+ unset ethlete_branch
10
+ fi
@@ -0,0 +1,5 @@
1
+ # Never `npx`: it reaches the registry when the package is absent, and a network error here would
2
+ # reject the push. A missing binary must make this block do nothing at all.
3
+ if [ -z "$ETHLETE_GIT_FLOW_SKIP" ] && [ -x node_modules/.bin/ethlete-agents ]; then
4
+ node_modules/.bin/ethlete-agents git-flow check --push || exit 1
5
+ fi