@ethlete/agent-rules 0.1.0-next.13 → 0.1.0-next.15
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.
- package/CHANGELOG.md +73 -0
- package/README.md +36 -2
- package/THIRD-PARTY-LICENSES.md +35 -0
- package/content/hooks/context-warning.py +450 -112
- package/content/hooks/subagent-model-policy.py +185 -0
- package/content/rules/subagent-models.md +29 -0
- package/content/skills/codex-subagent/SKILL.md +90 -0
- package/content/skills/codex-subagent/codex-agent.mjs +229 -0
- package/content/skills/design-exploration/SKILL.md +203 -0
- package/content/skills/design-exploration/check-story.mjs +139 -0
- package/content/skills/design-exploration/shoot-template.mjs +61 -0
- package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/content/skills/domain-modeling/SKILL.md +80 -0
- package/content/skills/grill-with-docs/SKILL.md +13 -0
- package/content/skills/grilling/SKILL.md +34 -0
- package/content/skills/handoff/SKILL.md +69 -4
- package/content/skills/sdk-update/SKILL.md +89 -0
- package/content/skills/timetrack/SKILL.md +190 -10
- package/package.json +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
- package/src/lib/frontmatter.d.ts +2 -0
- package/src/lib/frontmatter.js +2 -1
- package/src/lib/frontmatter.js.map +1 -1
- package/src/lib/git-flow/config.js +1 -1
- package/src/lib/git-flow/config.js.map +1 -1
- package/src/lib/git.d.ts +37 -0
- package/src/lib/git.js +77 -1
- package/src/lib/git.js.map +1 -1
- package/src/lib/index.d.ts +1 -0
- package/src/lib/index.js +1 -0
- package/src/lib/index.js.map +1 -1
- package/src/lib/plain-text.d.ts +9 -0
- package/src/lib/plain-text.js +22 -0
- package/src/lib/plain-text.js.map +1 -0
- package/src/lib/targets/claude-hooks.js +2 -1
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/codex-hooks.js +2 -1
- package/src/lib/targets/codex-hooks.js.map +1 -1
- package/src/lib/targets/hooks-shared.d.ts +22 -1
- package/src/lib/targets/hooks-shared.js +42 -9
- package/src/lib/targets/hooks-shared.js.map +1 -1
- package/src/lib/targets/shared.js +1 -0
- package/src/lib/targets/shared.js.map +1 -1
- package/src/lib/timetrack-command.js +449 -20
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +241 -0
- package/src/lib/timetrack.js +83 -2
- package/src/lib/timetrack.js.map +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-exploration
|
|
3
|
+
description: How a visual design exploration runs - the step size, what to put in front of the user at each stop, and when you may commit. Read BEFORE any visual design work on a sketch, a prototype, a theme, a palette, or the look of a component. The user is the designer; you find and frame, and you never pick the fix alone.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
vars: [storybookUrl]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Design exploration
|
|
10
|
+
|
|
11
|
+
A design exploration is a dialog. **The user is the designer. You find and frame. The user
|
|
12
|
+
chooses.** A finding is not a licence to pick the fix.
|
|
13
|
+
|
|
14
|
+
The unit of work is a **call**: one open question, with every answer drawn side by side at
|
|
15
|
+
the same geometry. An option wins or loses only against the other options of its call.
|
|
16
|
+
|
|
17
|
+
## The rules
|
|
18
|
+
|
|
19
|
+
1. **One open call at a time.** Take one question. Draw its options. Stop.
|
|
20
|
+
2. **Put the options in the call, then name your pick.** Two to four options, drawn for
|
|
21
|
+
real, side by side, labelled, with what each one costs. Your pick is a proposal.
|
|
22
|
+
3. **Commit only when the user says commit.**
|
|
23
|
+
4. **A clear rejection starts the next call.** Settle and record the rejected call, then draw
|
|
24
|
+
the next focused question automatically, within the user's original scope. Stop only when the
|
|
25
|
+
user asks to pause or end the exploration, or when their feedback leaves no grounded next
|
|
26
|
+
question.
|
|
27
|
+
5. **Keep the answer short.** 120 words at most.
|
|
28
|
+
|
|
29
|
+
Confirm the step size once, at the start. Do not ask again at every stop. A task is not
|
|
30
|
+
finished while the user is still talking about it.
|
|
31
|
+
|
|
32
|
+
## What one stop looks like
|
|
33
|
+
|
|
34
|
+
- The question, in one sentence.
|
|
35
|
+
- The call slug to look at.
|
|
36
|
+
- The options, labelled A, B, C, one line each.
|
|
37
|
+
- Your pick, one line on why.
|
|
38
|
+
- Nothing else. No next step, no second finding.
|
|
39
|
+
|
|
40
|
+
**Do not send a screenshot.** The user keeps the page open. Screenshot only for your own
|
|
41
|
+
check that an option renders.
|
|
42
|
+
|
|
43
|
+
If a second defect appears, add it to the open list and give it one line. Do not draw it.
|
|
44
|
+
|
|
45
|
+
## Where the options are drawn
|
|
46
|
+
|
|
47
|
+
The exploration's plan file names the tool. Two shapes exist.
|
|
48
|
+
|
|
49
|
+
**A repository with a `.ethlete/design` folder** draws a call per page, each option in its own
|
|
50
|
+
iframe. The tool is `et design`, from `@ethlete/cli`, so the repository itself needs no design
|
|
51
|
+
tooling. A call argues in the repository it is about:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
.ethlete/design/
|
|
55
|
+
config.json the port, the default call, and one entry per project
|
|
56
|
+
calls/<project>/<group>/<slug>/
|
|
57
|
+
call.ts the eyebrow, the headline, the intro, frameWidth, the options and the round prose
|
|
58
|
+
fixture.ts the data every option shares
|
|
59
|
+
option-a.ts one default-exported drawing per option
|
|
60
|
+
option-b.ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The first segment of a slug is the **project**. `config.json` gives each project its own
|
|
64
|
+
`styles` and its own `head`, so one project's type scale and fonts never reach another's
|
|
65
|
+
drawings. A project the config names not draws bare.
|
|
66
|
+
|
|
67
|
+
Start the page with `et design [checkout]`, which serves the port `config.json` names. The
|
|
68
|
+
checkout defaults to the working directory, and `DE_PORT` overrules the config, so two
|
|
69
|
+
checkouts can be drawn at once. A repository that builds the tool rather than installing it
|
|
70
|
+
wraps this in a script - in the ethlete SDK, `yarn design` and `yarn design:check`.
|
|
71
|
+
|
|
72
|
+
Ethlete Studio carries its own copy of the tool, so it draws a checkout that installs nothing.
|
|
73
|
+
The machine still needs Node, and the render stage still needs `playwright` where the copy in
|
|
74
|
+
use can reach it.
|
|
75
|
+
|
|
76
|
+
`call.ts` calls `defineCall` from `@design-explore`. Each option carries a `key`, a `name`,
|
|
77
|
+
a `claim`, a `cost`, an optional `verdict` of `chosen` or `rejected`, and a `load` that
|
|
78
|
+
imports its own module. The host greys a rejected option and shows it on hover.
|
|
79
|
+
|
|
80
|
+
In a call that runs past about a dozen options, every option names the pass that drew it with
|
|
81
|
+
`round: 'r3'`. **That tag is the only thing that makes a round exist.** The host reads the
|
|
82
|
+
options, draws one band per round in the order the options introduce them, and lists those
|
|
83
|
+
same bands in the sidebar, so a new option can never leave the menu stale. A round whose
|
|
84
|
+
options all carry a verdict is **settled**: the page folds it down to its winner plus one row
|
|
85
|
+
per rejected option, and a link opens it again.
|
|
86
|
+
|
|
87
|
+
A call whose rounds have all ruled is **resolved**. Its winners usually form a chain, each one
|
|
88
|
+
the last plus a change, so the page stops drawing them: it leads with a **Result** band holding
|
|
89
|
+
the final winner, and prints the chain as a row of keys above it. Every round below folds to
|
|
90
|
+
rows. A key in the chain opens the round that drew it.
|
|
91
|
+
|
|
92
|
+
`rounds` is prose only. Each entry gives a `key`, a `title` and a `note` saying what came out
|
|
93
|
+
of that pass, which is how the intro stays short and each pass reads as a reply to the one
|
|
94
|
+
before. An untagged round still draws and still gets a menu row - it says the bare key until
|
|
95
|
+
somebody writes the entry. A `rounds` entry that no option names is stale prose, and the host
|
|
96
|
+
and the check both report it. Write the note in the same pass that sets the verdicts, never
|
|
97
|
+
before the user rules.
|
|
98
|
+
|
|
99
|
+
A call with one option and no `claim` is a **view**: one reference picture, drawn full
|
|
100
|
+
width with no verdict tag. Use it for a picture that answers no question.
|
|
101
|
+
|
|
102
|
+
Two views, and a way to compare:
|
|
103
|
+
|
|
104
|
+
- **rounds** is the default. Round headings, the result band, and the folding above.
|
|
105
|
+
- **all N** is a contact sheet: every option in the call at about a third size, in one grid, so
|
|
106
|
+
a call of two dozen fits on a screen or two. Use it to find the options worth a close look.
|
|
107
|
+
- Clicking an option's name anywhere - a heading, a folded row, a thumbnail - puts it in the
|
|
108
|
+
**compare overlay** at the top of the page. Two drawings that differ by a few pixels can only
|
|
109
|
+
be told apart in one place, so the overlay stacks every pick in one box at full size and the
|
|
110
|
+
reader switches between them: click the box or press space to blink, and with two picks the
|
|
111
|
+
arrow keys wipe a seam across. Nothing is scaled and nothing moves. The picks live in the URL
|
|
112
|
+
under `pick`, so a comparison is a link you can send.
|
|
113
|
+
|
|
114
|
+
Three constraints the tool puts on an option file:
|
|
115
|
+
|
|
116
|
+
- **Never import a package barrel.** The libraries resolve to source, so one barrel makes
|
|
117
|
+
the browser request every module in the library, and the requests fail with
|
|
118
|
+
`ERR_INSUFFICIENT_RESOURCES`. Import the one file you need.
|
|
119
|
+
- **The fixture is shared, and an option may not change it.** Options drawn at three
|
|
120
|
+
geometries cannot be compared.
|
|
121
|
+
- **One file per option**, so several agents can draw at once, and a broken option breaks
|
|
122
|
+
its own frame only.
|
|
123
|
+
|
|
124
|
+
**A repository with a sketch Storybook** draws every option in the **same** story, side by
|
|
125
|
+
side, under the real geometry the thing ships in. The default Storybook is
|
|
126
|
+
{%storybookUrl%}; an app with its own sketch Storybook names its port in the plan file.
|
|
127
|
+
|
|
128
|
+
Either way:
|
|
129
|
+
|
|
130
|
+
- Sketches take inputs only and stay out of the application's build.
|
|
131
|
+
- Prototype, never refactor. Do not touch the shipped component until the treatment is
|
|
132
|
+
settled.
|
|
133
|
+
- Keep the rejected options drawn, marked as rejected.
|
|
134
|
+
|
|
135
|
+
## Check before you look
|
|
136
|
+
|
|
137
|
+
A broken build draws an overlay, so a screenshot of it looks like a design. **One check
|
|
138
|
+
covers that, and it is the whole gate.** Name the files you changed, never a whole project,
|
|
139
|
+
which is far slower and can fight the user's own editor.
|
|
140
|
+
|
|
141
|
+
With `et design`, run the checker from the checkout it draws:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
et design check --lint <changed files> --tsconfig
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`--tsconfig` with no path types every call of the checkout. `--checkout <path>` names another
|
|
148
|
+
one.
|
|
149
|
+
|
|
150
|
+
With a Storybook, copy {%resource:check-story.mjs%} to the **repository root** first:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
node check-story.mjs --lint <changed files> --tsconfig apps/<app>/.storybook/tsconfig.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Both take about four seconds and need no dev server. Then stop and hand the slug to the
|
|
157
|
+
user. Do not also open the page in a browser: it costs a browser launch, and both tools
|
|
158
|
+
transpile without type checking, so the render reports `ok` on a file that does not
|
|
159
|
+
compile. Open it only when the check passes and the drawing still does not appear:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
et design check --call <slug> # every option of the call
|
|
163
|
+
et design check --call <slug> --option b # one of them
|
|
164
|
+
node check-story.mjs --tsconfig <path> --story <story-id>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Delegating a call
|
|
168
|
+
|
|
169
|
+
A call has one job per option, so it fans out. Every brief names `model: opus`.
|
|
170
|
+
|
|
171
|
+
1. **One agent per option.** Give it the call folder, the option key, the fixture it must
|
|
172
|
+
not change, and the one claim its drawing has to support. Tell it to write its own
|
|
173
|
+
option file and nothing else, so two agents never touch one file.
|
|
174
|
+
2. **One check-and-fix agent, after the drawing agents return.** Give it the changed files
|
|
175
|
+
and the call slug. It runs the check above, fixes what it reports, and repeats until the
|
|
176
|
+
check says `ok`. It may not change what an option draws, only what stops it rendering.
|
|
177
|
+
3. **One write-up agent, once the user settles the call.** Give it the verdict in the
|
|
178
|
+
user's own words and the plan file. It records what won, what lost and why, it sets each
|
|
179
|
+
option's `verdict` in `call.ts`, and it writes that round's `note`. It runs while you open
|
|
180
|
+
the next call.
|
|
181
|
+
|
|
182
|
+
## Screenshots
|
|
183
|
+
|
|
184
|
+
**Off by default.** The user has the page open and sends you a picture when something looks
|
|
185
|
+
wrong. Take one only when the code cannot tell you whether two options really differ, and
|
|
186
|
+
never to put in front of the user. Copy {%resource:shoot-template.mjs%} to the repository
|
|
187
|
+
root.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
node shoot.mjs <story-id> 1100 760 out.png
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `waitUntil: 'networkidle'` never settles on an iframe that holds a live dev server
|
|
194
|
+
connection. Use `'domcontentloaded'`.
|
|
195
|
+
- `:hover`, `:focus-visible` and `:active` need a CDP session and `CSS.forcePseudoState`.
|
|
196
|
+
Several hold open at once, so one image shows them all. The template does this.
|
|
197
|
+
|
|
198
|
+
## Writing it down
|
|
199
|
+
|
|
200
|
+
Every settled call goes in the exploration's plan file: what won, what lost, and why. Keep
|
|
201
|
+
an **Open** list for the calls not yet made. A rejected option written down stops the next
|
|
202
|
+
session from drawing it again. After a clear rejection, use that record to frame and draw the
|
|
203
|
+
next call; do not wait for the user to type “continue”.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find out why a story does not render, in the cheapest order.
|
|
3
|
+
*
|
|
4
|
+
* A compile error renders as an overlay on top of the story, so a screenshot of it looks
|
|
5
|
+
* like a design. Run this first, and never screenshot before it says `ok`.
|
|
6
|
+
*
|
|
7
|
+
* Copy to the repository root - Node resolves `playwright` from the working directory.
|
|
8
|
+
*
|
|
9
|
+
* node check-story.mjs --lint <file>… --tsconfig <path> # ~4s, no browser
|
|
10
|
+
* node check-story.mjs --tsconfig <path> --story <story-id> # + the render
|
|
11
|
+
* SB_URL=http://localhost:4401 node check-story.mjs --story <story-id>
|
|
12
|
+
*
|
|
13
|
+
* Three stages, because they cost three different amounts:
|
|
14
|
+
*
|
|
15
|
+
* 1. `eslint` on the files you changed, about a second. Name the files - never a whole
|
|
16
|
+
* project, which is far slower and can fight the user's own editor.
|
|
17
|
+
* 2. `tsc --noEmit` reads the source. It needs no dev server, waits for no rebuild, and
|
|
18
|
+
* catches every syntax and type error in about three seconds. Almost everything you
|
|
19
|
+
* break is caught here.
|
|
20
|
+
* 3. The browser catches what the source cannot: an Angular template error, a runtime
|
|
21
|
+
* throw, a missing story id. It costs a browser launch, and it can only see a build the
|
|
22
|
+
* dev server has already finished, so a fresh edit needs a few seconds first.
|
|
23
|
+
*
|
|
24
|
+
* A type error does not stop this repo's Storybook - it transpiles without checking - so
|
|
25
|
+
* stage 2 alone will report `ok` on a file that does not compile. That is why stage 1 exists.
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const BASE = process.env.SB_URL ?? 'http://localhost:4400';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The type checker to run. `tsc` resolves the one the repository installed. Set
|
|
33
|
+
* `TSC=@typescript/native-preview` for the Go port, which is about a third faster - but it
|
|
34
|
+
* rejects a `baseUrl` in the config, so a repository that still uses one cannot run it.
|
|
35
|
+
*/
|
|
36
|
+
const TSC = process.env.TSC ?? null;
|
|
37
|
+
|
|
38
|
+
const flags = { '--lint': [], '--tsconfig': [], '--story': [] };
|
|
39
|
+
let current = null;
|
|
40
|
+
|
|
41
|
+
for (const arg of process.argv.slice(2)) {
|
|
42
|
+
if (arg in flags) current = arg;
|
|
43
|
+
else if (current) flags[current].push(arg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lintFiles = flags['--lint'];
|
|
47
|
+
const [tsconfig] = flags['--tsconfig'];
|
|
48
|
+
const [id] = flags['--story'];
|
|
49
|
+
|
|
50
|
+
if (lintFiles.length === 0 && !tsconfig && !id) {
|
|
51
|
+
console.error('usage: node check-story.mjs [--lint <file>…] [--tsconfig <path>] [--story <id>]');
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A bundler stack runs to hundreds of frames and says nothing. Keep the message. */
|
|
56
|
+
const trim = (text) =>
|
|
57
|
+
[...new Set(text.split('\n').filter((line) => !/^\s+at /.test(line) && line.trim() !== ''))].slice(0, 12).join('\n');
|
|
58
|
+
|
|
59
|
+
if (lintFiles.length > 0) {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync('npx', ['eslint', ...lintFiles], { encoding: 'utf8', stdio: 'pipe' });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`LINT\n${trim(`${error.stdout ?? ''}${error.stderr ?? ''}`)}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tsconfig) {
|
|
69
|
+
try {
|
|
70
|
+
const cmd = TSC ? ['-y', TSC] : ['tsc'];
|
|
71
|
+
execFileSync('npx', [...cmd, '--noEmit', '-p', tsconfig], { encoding: 'utf8', stdio: 'pipe' });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`TYPE ERROR\n${trim(`${error.stdout ?? ''}${error.stderr ?? ''}`)}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!id) {
|
|
79
|
+
console.log('ok');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { chromium } = await import('playwright');
|
|
84
|
+
|
|
85
|
+
const browser = await chromium.launch();
|
|
86
|
+
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
|
|
87
|
+
|
|
88
|
+
/** Angular throws at render time; the overlay never shows those, so read the console too. */
|
|
89
|
+
const consoleErrors = [];
|
|
90
|
+
page.on('console', (m) => m.type() === 'error' && consoleErrors.push(m.text()));
|
|
91
|
+
page.on('pageerror', (e) => consoleErrors.push(e.message));
|
|
92
|
+
|
|
93
|
+
await page.goto(`${BASE}/iframe.html?id=${id}&viewMode=story`, { waitUntil: 'domcontentloaded' });
|
|
94
|
+
|
|
95
|
+
const rendered = await page
|
|
96
|
+
.locator('#storybook-root > *')
|
|
97
|
+
.first()
|
|
98
|
+
.waitFor({ timeout: 15000 })
|
|
99
|
+
.then(
|
|
100
|
+
() => true,
|
|
101
|
+
() => false,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/** The webpack overlay is an iframe, so its text is only reachable through a frame locator. */
|
|
105
|
+
const webpackOverlayText =
|
|
106
|
+
(await page.locator('#webpack-dev-server-client-overlay').count()) > 0
|
|
107
|
+
? await page
|
|
108
|
+
.frameLocator('#webpack-dev-server-client-overlay')
|
|
109
|
+
.locator('body')
|
|
110
|
+
.innerText()
|
|
111
|
+
.catch(() => '(overlay present, text unreadable)')
|
|
112
|
+
: '';
|
|
113
|
+
/** The Vite overlay keeps its text in a shadow root. */
|
|
114
|
+
const viteOverlayText = await page.evaluate(() => {
|
|
115
|
+
const overlay = document.querySelector('vite-error-overlay');
|
|
116
|
+
if (!overlay) return '';
|
|
117
|
+
return overlay.shadowRoot?.textContent?.trim() || '(overlay present, text unreadable)';
|
|
118
|
+
});
|
|
119
|
+
const overlayText = webpackOverlayText || viteOverlayText;
|
|
120
|
+
|
|
121
|
+
await browser.close();
|
|
122
|
+
|
|
123
|
+
if (overlayText) {
|
|
124
|
+
console.error(`COMPILE ERROR on ${id}\n${trim(overlayText)}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!rendered) {
|
|
129
|
+
console.error(`EMPTY on ${id} - #storybook-root never got a child.`);
|
|
130
|
+
if (consoleErrors.length > 0) console.error(trim(consoleErrors.join('\n')));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (consoleErrors.length > 0) {
|
|
135
|
+
console.error(`RENDERED WITH ERRORS on ${id}\n${trim(consoleErrors.join('\n'))}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`ok ${id}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot one Storybook story, with optional forced pseudo-states.
|
|
3
|
+
*
|
|
4
|
+
* Copy to the repository root - Node resolves `playwright` from the working directory.
|
|
5
|
+
*
|
|
6
|
+
* node shoot.mjs <story-id> [width] [height] [out.png]
|
|
7
|
+
*
|
|
8
|
+
* To hold :hover / :focus-visible / :active open at the same time, fill FORCED below with
|
|
9
|
+
* one entry per element. A CSS selector is matched inside the story's document.
|
|
10
|
+
*/
|
|
11
|
+
import { chromium } from 'playwright';
|
|
12
|
+
|
|
13
|
+
const BASE = process.env.SB_URL ?? 'http://localhost:4400';
|
|
14
|
+
|
|
15
|
+
/** @type {{ selector: string, states: ('hover'|'focus'|'focus-visible'|'active')[] }[]} */
|
|
16
|
+
const FORCED = [
|
|
17
|
+
// { selector: '[data-state="hover"] .the-band', states: ['hover'] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const [id, width = '1100', height = '760', out = 'shot.png'] = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
if (!id) {
|
|
23
|
+
console.error('usage: node shoot.mjs <story-id> [width] [height] [out.png]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const browser = await chromium.launch();
|
|
28
|
+
const page = await browser.newPage({
|
|
29
|
+
viewport: { width: Number(width), height: Number(height) },
|
|
30
|
+
deviceScaleFactor: 2,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await page.goto(`${BASE}/iframe.html?id=${id}&viewMode=story`, { waitUntil: 'domcontentloaded' });
|
|
34
|
+
await page.waitForSelector('#storybook-root > *', { timeout: 15000 });
|
|
35
|
+
|
|
36
|
+
const overlays = await page.locator('#webpack-dev-server-client-overlay, vite-error-overlay').count();
|
|
37
|
+
if (overlays > 0) {
|
|
38
|
+
console.error('compile error overlay is present - the image would lie');
|
|
39
|
+
await browser.close();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (FORCED.length > 0) {
|
|
44
|
+
const cdp = await page.context().newCDPSession(page);
|
|
45
|
+
await cdp.send('DOM.enable');
|
|
46
|
+
await cdp.send('CSS.enable');
|
|
47
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: -1 });
|
|
48
|
+
|
|
49
|
+
for (const { selector, states } of FORCED) {
|
|
50
|
+
const { nodeIds } = await cdp.send('DOM.querySelectorAll', { nodeId: root.nodeId, selector });
|
|
51
|
+
for (const nodeId of nodeIds) {
|
|
52
|
+
await cdp.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses: states });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await page.waitForTimeout(400);
|
|
58
|
+
await page.screenshot({ path: out });
|
|
59
|
+
await browser.close();
|
|
60
|
+
|
|
61
|
+
console.log(out);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ADR Format
|
|
2
|
+
|
|
3
|
+
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
|
|
4
|
+
|
|
5
|
+
Create the `docs/adr/` directory lazily: only when the first ADR is needed.
|
|
6
|
+
|
|
7
|
+
## Template
|
|
8
|
+
|
|
9
|
+
```md
|
|
10
|
+
# {Short title of the decision}
|
|
11
|
+
|
|
12
|
+
{1-3 sentences: what's the context, what did we decide, and why.}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it. An ADR can be a single paragraph. The value is in recording _that_ a decision was made and _why_, not in filling out sections.
|
|
16
|
+
|
|
17
|
+
## Optional sections
|
|
18
|
+
|
|
19
|
+
Only include these when they add genuine value. Most ADRs won't need them.
|
|
20
|
+
|
|
21
|
+
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`): useful when decisions are revisited
|
|
22
|
+
- **Considered Options**: only when the rejected alternatives are worth remembering
|
|
23
|
+
- **Consequences**: only when non-obvious downstream effects need to be called out
|
|
24
|
+
|
|
25
|
+
## Numbering
|
|
26
|
+
|
|
27
|
+
Scan `docs/adr/` for the highest existing number and increment by one.
|
|
28
|
+
|
|
29
|
+
## When to offer an ADR
|
|
30
|
+
|
|
31
|
+
All three of these must be true:
|
|
32
|
+
|
|
33
|
+
1. **Hard to reverse**: the cost of changing your mind later is meaningful
|
|
34
|
+
2. **Surprising without context**: a future reader will look at the code and wonder "why on earth did they do it this way?"
|
|
35
|
+
3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons
|
|
36
|
+
|
|
37
|
+
If a decision is easy to reverse, skip it: you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
|
|
38
|
+
|
|
39
|
+
### What qualifies
|
|
40
|
+
|
|
41
|
+
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
|
|
42
|
+
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
|
|
43
|
+
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library: just the ones that would take a quarter to swap out.
|
|
44
|
+
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
|
|
45
|
+
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
|
|
46
|
+
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
|
|
47
|
+
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it; otherwise someone will suggest GraphQL again in six months.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# CONTEXT.md Format
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
```md
|
|
6
|
+
# {Context Name}
|
|
7
|
+
|
|
8
|
+
{One or two sentence description of what this context is and why it exists.}
|
|
9
|
+
|
|
10
|
+
## Language
|
|
11
|
+
|
|
12
|
+
**Order**:
|
|
13
|
+
{A one or two sentence description of the term}
|
|
14
|
+
_Avoid_: Purchase, transaction
|
|
15
|
+
|
|
16
|
+
**Invoice**:
|
|
17
|
+
A request for payment sent to a customer after delivery.
|
|
18
|
+
_Avoid_: Bill, payment request
|
|
19
|
+
|
|
20
|
+
**Customer**:
|
|
21
|
+
A person or organization that places orders.
|
|
22
|
+
_Avoid_: Client, buyer, account
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`.
|
|
28
|
+
- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.
|
|
29
|
+
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
|
|
30
|
+
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
|
|
31
|
+
|
|
32
|
+
## Single vs multi-context repos
|
|
33
|
+
|
|
34
|
+
**Single context (most repos):** One `CONTEXT.md` at the repo root.
|
|
35
|
+
|
|
36
|
+
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
|
|
37
|
+
|
|
38
|
+
```md
|
|
39
|
+
# Context Map
|
|
40
|
+
|
|
41
|
+
## Contexts
|
|
42
|
+
|
|
43
|
+
- [Ordering](./src/ordering/CONTEXT.md): receives and tracks customer orders
|
|
44
|
+
- [Billing](./src/billing/CONTEXT.md): generates invoices and processes payments
|
|
45
|
+
- [Fulfillment](./src/fulfillment/CONTEXT.md): manages warehouse picking and shipping
|
|
46
|
+
|
|
47
|
+
## Relationships
|
|
48
|
+
|
|
49
|
+
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
|
|
50
|
+
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
|
|
51
|
+
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The skill infers which structure applies:
|
|
55
|
+
|
|
56
|
+
- If `CONTEXT-MAP.md` exists, read it to find contexts
|
|
57
|
+
- If only a root `CONTEXT.md` exists, single context
|
|
58
|
+
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
|
|
59
|
+
|
|
60
|
+
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: domain-modeling
|
|
3
|
+
description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Domain Modeling
|
|
9
|
+
|
|
10
|
+
Actively build and sharpen the project's domain model as you design. This is the _active_ discipline: challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely _reading_ `CONTEXT.md` for vocabulary is not this skill: that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)
|
|
11
|
+
|
|
12
|
+
## File structure
|
|
13
|
+
|
|
14
|
+
Most repos have a single context:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/
|
|
18
|
+
├── CONTEXT.md
|
|
19
|
+
├── docs/
|
|
20
|
+
│ └── adr/
|
|
21
|
+
│ ├── 0001-event-sourced-orders.md
|
|
22
|
+
│ └── 0002-postgres-for-write-model.md
|
|
23
|
+
└── src/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/
|
|
30
|
+
├── CONTEXT-MAP.md
|
|
31
|
+
├── docs/
|
|
32
|
+
│ └── adr/ ← system-wide decisions
|
|
33
|
+
├── src/
|
|
34
|
+
│ ├── ordering/
|
|
35
|
+
│ │ ├── CONTEXT.md
|
|
36
|
+
│ │ └── docs/adr/ ← context-specific decisions
|
|
37
|
+
│ └── billing/
|
|
38
|
+
│ ├── CONTEXT.md
|
|
39
|
+
│ └── docs/adr/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create files lazily: only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
|
|
43
|
+
|
|
44
|
+
## During the session
|
|
45
|
+
|
|
46
|
+
### Challenge against the glossary
|
|
47
|
+
|
|
48
|
+
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y. Which is it?"
|
|
49
|
+
|
|
50
|
+
### Sharpen fuzzy language
|
|
51
|
+
|
|
52
|
+
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account': do you mean the Customer or the User? Those are different things."
|
|
53
|
+
|
|
54
|
+
### Discuss concrete scenarios
|
|
55
|
+
|
|
56
|
+
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
|
|
57
|
+
|
|
58
|
+
### Cross-reference with code
|
|
59
|
+
|
|
60
|
+
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible. Which is right?"
|
|
61
|
+
|
|
62
|
+
### Update CONTEXT.md inline
|
|
63
|
+
|
|
64
|
+
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up: capture them as they happen. Use the format in {%resource:CONTEXT-FORMAT.md%}.
|
|
65
|
+
|
|
66
|
+
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
|
|
67
|
+
|
|
68
|
+
### Offer ADRs sparingly
|
|
69
|
+
|
|
70
|
+
Only offer to create an ADR when all three are true:
|
|
71
|
+
|
|
72
|
+
1. **Hard to reverse**: the cost of changing your mind later is meaningful
|
|
73
|
+
2. **Surprising without context**: a future reader will wonder "why did they do it this way?"
|
|
74
|
+
3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons
|
|
75
|
+
|
|
76
|
+
If any of the three is missing, skip the ADR. Use the format in {%resource:ADR-FORMAT.md%}.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
Vendored from [mattpocock/skills](https://github.com/mattpocock/skills) at `3cca18b`, MIT (Copyright (c) 2026 Matt Pocock).
|
|
@@ -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).
|