@ethlete/agent-rules 0.1.0-next.4 → 0.1.0-next.6
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 +26 -0
- package/README.md +192 -11
- package/content/git-hooks/post-checkout.sh +10 -0
- package/content/git-hooks/pre-push.sh +5 -0
- package/content/hooks/context-warning.py +259 -97
- package/content/skills/figma-export/SKILL.md +193 -0
- package/content/skills/figma-export/dump-figma-layers.py +83 -0
- package/content/skills/figma-export/dump-figma-svg.py +235 -0
- package/content/skills/figma-export/measure-template.mjs +87 -0
- package/content/skills/git-flow/SKILL.md +83 -0
- package/content/skills/handoff/SKILL.md +4 -0
- package/content/skills/query/SKILL.md +23 -13
- package/package.json +12 -1
- package/src/index.js +10 -5
- package/src/index.js.map +1 -1
- package/src/lib/config.d.ts +30 -2
- package/src/lib/config.js +20 -3
- package/src/lib/config.js.map +1 -1
- package/src/lib/git-flow/build.d.ts +35 -0
- package/src/lib/git-flow/build.js +24 -0
- package/src/lib/git-flow/build.js.map +1 -0
- package/src/lib/git-flow/config.d.ts +59 -0
- package/src/lib/git-flow/config.js +50 -0
- package/src/lib/git-flow/config.js.map +1 -0
- package/src/lib/git-flow/index.d.ts +5 -0
- package/src/lib/git-flow/index.js +9 -0
- package/src/lib/git-flow/index.js.map +1 -0
- package/src/lib/git-flow/parse.d.ts +49 -0
- package/src/lib/git-flow/parse.js +274 -0
- package/src/lib/git-flow/parse.js.map +1 -0
- package/src/lib/git-flow/start.d.ts +22 -0
- package/src/lib/git-flow/start.js +24 -0
- package/src/lib/git-flow/start.js.map +1 -0
- package/src/lib/git-flow/validate.d.ts +34 -0
- package/src/lib/git-flow/validate.js +72 -0
- package/src/lib/git-flow/validate.js.map +1 -0
- package/src/lib/git-flow-command.d.ts +4 -0
- package/src/lib/git-flow-command.js +157 -0
- package/src/lib/git-flow-command.js.map +1 -0
- package/src/lib/git-flow-repair.d.ts +17 -0
- package/src/lib/git-flow-repair.js +150 -0
- package/src/lib/git-flow-repair.js.map +1 -0
- package/src/lib/git-flow-start.d.ts +20 -0
- package/src/lib/git-flow-start.js +147 -0
- package/src/lib/git-flow-start.js.map +1 -0
- package/src/lib/git.d.ts +27 -0
- package/src/lib/git.js +49 -0
- package/src/lib/git.js.map +1 -0
- package/src/lib/gitlab.d.ts +35 -0
- package/src/lib/gitlab.js +98 -0
- package/src/lib/gitlab.js.map +1 -0
- package/src/lib/jira.d.ts +28 -0
- package/src/lib/jira.js +83 -0
- package/src/lib/jira.js.map +1 -0
- package/src/lib/owned-paths.js +19 -1
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/plan.js +29 -5
- package/src/lib/plan.js.map +1 -1
- package/src/lib/prompt.d.ts +8 -0
- package/src/lib/prompt.js +27 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/render.d.ts +12 -1
- package/src/lib/render.js +23 -6
- package/src/lib/render.js.map +1 -1
- package/src/lib/targets/claude-hooks.d.ts +1 -23
- package/src/lib/targets/claude-hooks.js +15 -84
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/codex-hooks.d.ts +12 -0
- package/src/lib/targets/codex-hooks.js +33 -0
- package/src/lib/targets/codex-hooks.js.map +1 -0
- package/src/lib/targets/git-hooks.d.ts +25 -0
- package/src/lib/targets/git-hooks.js +70 -0
- package/src/lib/targets/git-hooks.js.map +1 -0
- package/src/lib/targets/hooks-shared.d.ts +38 -0
- package/src/lib/targets/hooks-shared.js +95 -0
- package/src/lib/targets/hooks-shared.js.map +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dump the properties of every layer in a Figma "copy as CSS" export, in document order.
|
|
3
|
+
|
|
4
|
+
python3 dump-figma-layers.py <export.css> ['<name regex>']
|
|
5
|
+
|
|
6
|
+
Without a regex every layer is printed except obvious vector artwork. With one, only the
|
|
7
|
+
layers whose name matches it.
|
|
8
|
+
|
|
9
|
+
Figma emits sub-comments — `/* Auto layout */`, `/* Inside auto layout */`, `/* or 16px */`
|
|
10
|
+
and design-token names like `/* Brand/Chalk White/500 (base) */` — that carry properties
|
|
11
|
+
belonging to the layer named above them. They are folded into that layer rather than treated
|
|
12
|
+
as new ones: a parser that splits on every comment reports frames with zero properties.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
COMMENT = re.compile(r'^/\* (.+) \*/$')
|
|
19
|
+
LAYOUT_NOTE = re.compile(r'^(Auto layout|Inside auto layout|or [\d.]+px|Hug|Fixed|Fill|Effect style)$')
|
|
20
|
+
ARTWORK = re.compile(
|
|
21
|
+
r'^(Polygon|Vector|Ellipse|Rectangle \d|Star|Union|Subtract|Group|Mask|Clip'
|
|
22
|
+
r'|Texture|Frame \d{6,})'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_blocks(path):
|
|
27
|
+
"""Yield ('comment', name) and ('prop', text), skipping Figma's multi-line prose notes."""
|
|
28
|
+
lines = [line.rstrip('\n') for line in open(path)]
|
|
29
|
+
index, total = 0, len(lines)
|
|
30
|
+
|
|
31
|
+
while index < total:
|
|
32
|
+
stripped = lines[index].strip()
|
|
33
|
+
|
|
34
|
+
if stripped.startswith('/*') and not stripped.endswith('*/'):
|
|
35
|
+
while index < total and not lines[index].rstrip().endswith('*/'):
|
|
36
|
+
index += 1
|
|
37
|
+
index += 1
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
comment = COMMENT.match(stripped)
|
|
41
|
+
|
|
42
|
+
if comment:
|
|
43
|
+
following = lines[index + 1].strip() if index + 1 < total else ''
|
|
44
|
+
yield 'comment', comment.group(1), following
|
|
45
|
+
elif ':' in stripped and not stripped.startswith('/*'):
|
|
46
|
+
yield 'prop', stripped, ''
|
|
47
|
+
|
|
48
|
+
index += 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
if len(sys.argv) < 2:
|
|
53
|
+
sys.exit(__doc__)
|
|
54
|
+
|
|
55
|
+
path = sys.argv[1]
|
|
56
|
+
match = re.compile(sys.argv[2]).search if len(sys.argv) > 2 else None
|
|
57
|
+
show, index = False, 0
|
|
58
|
+
|
|
59
|
+
for kind, text, following in read_blocks(path):
|
|
60
|
+
if kind == 'prop':
|
|
61
|
+
if show:
|
|
62
|
+
print(f' {text}')
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# A layer name is followed by a blank line; a sub-comment sits directly on top of the
|
|
66
|
+
# declarations it annotates. The name list covers the few notes Figma writes before
|
|
67
|
+
# another comment, where that spacing rule cannot decide.
|
|
68
|
+
is_declaration = ':' in following and not following.startswith('/*')
|
|
69
|
+
|
|
70
|
+
if is_declaration or LAYOUT_NOTE.match(text):
|
|
71
|
+
if show:
|
|
72
|
+
print(f' /* {text} */')
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
index += 1
|
|
76
|
+
show = match(text) is not None if match else not ARTWORK.match(text)
|
|
77
|
+
|
|
78
|
+
if show:
|
|
79
|
+
print(f'\n--- [{index}] {text}')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
main()
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dump the box tree of a Figma SVG export: every shape with absolute coordinates.
|
|
3
|
+
|
|
4
|
+
python3 dump-figma-svg.py <export.svg>
|
|
5
|
+
|
|
6
|
+
Prints one line per drawn element in document order, indented by group nesting, then a
|
|
7
|
+
summary of repeated shapes and of the gaps between shapes that share a row.
|
|
8
|
+
|
|
9
|
+
An SVG export has no layer names and — because Figma outlines text on export — no strings
|
|
10
|
+
or font metrics. What it does have is exact geometry: every rect carries its own x/y/
|
|
11
|
+
width/height/rx, so padding and gaps are differences you can read off rather than numbers
|
|
12
|
+
you have to trust a label for. Outlined text is measured by the bounding box of its path
|
|
13
|
+
coordinates, which runs a fraction of a pixel wide because Bezier control points sit
|
|
14
|
+
outside the curve — enough to place a text run, not to size a glyph.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
|
|
21
|
+
SVG_NS = '{http://www.w3.org/2000/svg}'
|
|
22
|
+
TRANSLATE = re.compile(r'translate\(\s*([-\d.]+)[\s,]+([-\d.]+)\s*\)')
|
|
23
|
+
TOKEN = re.compile(r'([MmLlHhVvCcSsQqTtAaZz])|(-?\d*\.?\d+(?:e[-+]?\d+)?)', re.IGNORECASE)
|
|
24
|
+
ARITY = {'m': 2, 'l': 2, 'h': 1, 'v': 1, 'c': 6, 's': 4, 'q': 4, 't': 2, 'a': 7, 'z': 0}
|
|
25
|
+
SKIP = {'defs', 'clipPath', 'mask', 'filter', 'linearGradient', 'radialGradient', 'pattern'}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tag_of(element):
|
|
29
|
+
return element.tag[len(SVG_NS) :] if element.tag.startswith(SVG_NS) else element.tag
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def number(element, name, fallback=0.0):
|
|
33
|
+
try:
|
|
34
|
+
return float(element.get(name, fallback))
|
|
35
|
+
except ValueError:
|
|
36
|
+
return fallback
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def translation(element):
|
|
40
|
+
match = TRANSLATE.search(element.get('transform') or '')
|
|
41
|
+
|
|
42
|
+
return (float(match.group(1)), float(match.group(2))) if match else (0.0, 0.0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def path_points(data):
|
|
46
|
+
"""Every point a path visits, control points included. `H`/`V`/`A` take counts of their
|
|
47
|
+
own, so pairing the numbers off two at a time reports a box the shape never occupies."""
|
|
48
|
+
tokens = TOKEN.findall(data)
|
|
49
|
+
index, command, x, y = 0, 'm', 0.0, 0.0
|
|
50
|
+
|
|
51
|
+
while index < len(tokens):
|
|
52
|
+
letter, value = tokens[index]
|
|
53
|
+
|
|
54
|
+
if letter:
|
|
55
|
+
command = letter
|
|
56
|
+
index += 1
|
|
57
|
+
|
|
58
|
+
if command.lower() == 'z':
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
arity = ARITY[command.lower()]
|
|
62
|
+
args = [float(tokens[index + step][1]) for step in range(arity) if index + step < len(tokens)]
|
|
63
|
+
|
|
64
|
+
if len(args) < arity:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
index += arity
|
|
68
|
+
relative = command.islower()
|
|
69
|
+
|
|
70
|
+
if command.lower() == 'h':
|
|
71
|
+
x = x + args[0] if relative else args[0]
|
|
72
|
+
elif command.lower() == 'v':
|
|
73
|
+
y = y + args[0] if relative else args[0]
|
|
74
|
+
else:
|
|
75
|
+
pairs = [(args[5], args[6])] if command.lower() == 'a' else list(zip(args[0::2], args[1::2]))
|
|
76
|
+
|
|
77
|
+
for point_x, point_y in pairs:
|
|
78
|
+
yield (x + point_x, y + point_y) if relative else (point_x, point_y)
|
|
79
|
+
|
|
80
|
+
x, y = (x + pairs[-1][0], y + pairs[-1][1]) if relative else pairs[-1]
|
|
81
|
+
|
|
82
|
+
yield x, y
|
|
83
|
+
|
|
84
|
+
if command == 'M':
|
|
85
|
+
command = 'L'
|
|
86
|
+
elif command == 'm':
|
|
87
|
+
command = 'l'
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def path_box(data):
|
|
91
|
+
points = list(path_points(data))
|
|
92
|
+
xs, ys = [point[0] for point in points], [point[1] for point in points]
|
|
93
|
+
|
|
94
|
+
if not xs:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
return min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def box_of(element):
|
|
101
|
+
name = tag_of(element)
|
|
102
|
+
|
|
103
|
+
if name in ('rect', 'image'):
|
|
104
|
+
return number(element, 'x'), number(element, 'y'), number(element, 'width'), number(element, 'height')
|
|
105
|
+
|
|
106
|
+
if name == 'path':
|
|
107
|
+
return path_box(element.get('d') or '')
|
|
108
|
+
|
|
109
|
+
if name in ('polygon', 'polyline'):
|
|
110
|
+
return path_box('M' + (element.get('points') or ''))
|
|
111
|
+
|
|
112
|
+
if name == 'circle':
|
|
113
|
+
radius = number(element, 'r')
|
|
114
|
+
|
|
115
|
+
return number(element, 'cx') - radius, number(element, 'cy') - radius, radius * 2, radius * 2
|
|
116
|
+
|
|
117
|
+
if name == 'ellipse':
|
|
118
|
+
rx, ry = number(element, 'rx'), number(element, 'ry')
|
|
119
|
+
|
|
120
|
+
return number(element, 'cx') - rx, number(element, 'cy') - ry, rx * 2, ry * 2
|
|
121
|
+
|
|
122
|
+
if name == 'line':
|
|
123
|
+
x1, y1 = number(element, 'x1'), number(element, 'y1')
|
|
124
|
+
|
|
125
|
+
return x1, y1, number(element, 'x2') - x1, number(element, 'y2') - y1
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def trim(value):
|
|
131
|
+
return f'{value:.3f}'.rstrip('0').rstrip('.')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def walk(element, offset, depth, boxes):
|
|
135
|
+
for child in element:
|
|
136
|
+
name = tag_of(child)
|
|
137
|
+
|
|
138
|
+
if name in SKIP:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
dx, dy = translation(child)
|
|
142
|
+
origin = (offset[0] + dx, offset[1] + dy)
|
|
143
|
+
|
|
144
|
+
if name in ('g', 'svg'):
|
|
145
|
+
print(f'{" " * depth}<{name}>' + (f' translate({trim(dx)} {trim(dy)})' if (dx or dy) else ''))
|
|
146
|
+
walk(child, origin, depth + 1, boxes)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if name == 'text':
|
|
150
|
+
content = ' '.join(''.join(child.itertext()).split())
|
|
151
|
+
metrics = ' '.join(
|
|
152
|
+
f'{key}={child.get(key)}'
|
|
153
|
+
for key in ('font-family', 'font-size', 'font-weight', 'letter-spacing')
|
|
154
|
+
if child.get(key)
|
|
155
|
+
)
|
|
156
|
+
x, y = number(child, 'x') + origin[0], number(child, 'y') + origin[1]
|
|
157
|
+
print(f'{" " * depth}{"text":<6} {trim(x):>9} {trim(y):>8} {metrics} "{content}"')
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
box = box_of(child)
|
|
161
|
+
|
|
162
|
+
if box is None:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
x, y, width, height = box[0] + origin[0], box[1] + origin[1], box[2], box[3]
|
|
166
|
+
radius = child.get('rx') if name == 'rect' else None
|
|
167
|
+
paint = child.get('fill') or child.get('stroke') or ''
|
|
168
|
+
detail = f' r={radius}' if radius else ''
|
|
169
|
+
detail += f' {"stroke" if child.get("stroke") else "fill"}={paint}' if paint and paint != 'none' else ''
|
|
170
|
+
label = 'text?' if name == 'path' and width >= height * 3 else name
|
|
171
|
+
|
|
172
|
+
print(f'{" " * depth}{label:<6} {trim(x):>9} {trim(y):>8} {trim(width):>8} × {trim(height):<8}{detail}'.rstrip())
|
|
173
|
+
boxes.append((x, y, width, height, name, radius, paint))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def summarise(boxes):
|
|
177
|
+
shapes = {}
|
|
178
|
+
|
|
179
|
+
for x, y, width, height, name, radius, paint in boxes:
|
|
180
|
+
if name != 'rect':
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
shapes.setdefault((round(width, 2), round(height, 2), radius), []).append((x, y, paint))
|
|
184
|
+
|
|
185
|
+
repeats = {key: hits for key, hits in shapes.items() if len(hits) > 1}
|
|
186
|
+
|
|
187
|
+
if repeats:
|
|
188
|
+
print('\nRepeated rects — one component rendered N times, not N designs:')
|
|
189
|
+
|
|
190
|
+
for (width, height, radius), hits in sorted(repeats.items(), key=lambda item: -len(item[1])):
|
|
191
|
+
fills = {paint for _, _, paint in hits}
|
|
192
|
+
varies = f', {len(fills)} fills' if len(fills) > 1 else ''
|
|
193
|
+
print(f' {len(hits)}× {trim(width)} × {trim(height)}' + (f' r={radius}' if radius else '') + varies)
|
|
194
|
+
|
|
195
|
+
rows = {}
|
|
196
|
+
|
|
197
|
+
for x, y, width, height, name, _, _ in boxes:
|
|
198
|
+
if name == 'rect':
|
|
199
|
+
rows.setdefault(round(y, 2), set()).add((x, width))
|
|
200
|
+
|
|
201
|
+
printed = False
|
|
202
|
+
|
|
203
|
+
for y, hits in sorted(rows.items()):
|
|
204
|
+
spans, edge = [], None
|
|
205
|
+
|
|
206
|
+
for x, width in sorted(hits):
|
|
207
|
+
if edge is not None and x >= edge:
|
|
208
|
+
spans.append(round(x - edge, 2))
|
|
209
|
+
|
|
210
|
+
edge = max(edge or 0, x + width)
|
|
211
|
+
|
|
212
|
+
if not spans:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if not printed:
|
|
216
|
+
print('\nGaps between rects sharing a top edge — the auto-layout gap, measured:')
|
|
217
|
+
printed = True
|
|
218
|
+
|
|
219
|
+
print(f' y={trim(y):<8} {len(spans) + 1} columns, gaps: {", ".join(trim(span) for span in spans)}')
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main():
|
|
223
|
+
if len(sys.argv) < 2:
|
|
224
|
+
sys.exit(__doc__)
|
|
225
|
+
|
|
226
|
+
root = ET.parse(sys.argv[1]).getroot()
|
|
227
|
+
boxes = []
|
|
228
|
+
|
|
229
|
+
print(f'canvas {root.get("width")} × {root.get("height")} viewBox={root.get("viewBox")}\n')
|
|
230
|
+
walk(root, (0.0, 0.0), 0, boxes)
|
|
231
|
+
summarise(boxes)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == '__main__':
|
|
235
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measure real rendered geometry against the numbers in a Figma export.
|
|
3
|
+
*
|
|
4
|
+
* Copy into a scratch directory next to a `styles.css` copied out of a production build,
|
|
5
|
+
* replace MARKUP / WIDTHS / DESIGN / probe(), then `node measure-template.mjs`.
|
|
6
|
+
*/
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { writeFileSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
// This runs from a scratch directory, so Playwright cannot be resolved by name — point
|
|
11
|
+
// createRequire at the repo root. Playwright is CommonJS; a named import fails.
|
|
12
|
+
const REPO_ROOT = '/absolute/path/to/repo'; // <-- change me
|
|
13
|
+
const { chromium } = createRequire(`${REPO_ROOT}/`)('playwright');
|
|
14
|
+
|
|
15
|
+
/** One instance of the thing under test. Keep the real component classes verbatim. */
|
|
16
|
+
const MARKUP = (id) => `
|
|
17
|
+
<div id="${id}-card" class="…">
|
|
18
|
+
<h4 id="${id}-title" class="…">Some realistically long label</h4>
|
|
19
|
+
</div>`;
|
|
20
|
+
|
|
21
|
+
/** Container widths to probe, taken from the export's frames — not from viewport breakpoints. */
|
|
22
|
+
const WIDTHS = [956, 640];
|
|
23
|
+
|
|
24
|
+
/** What the export says each width should produce. */
|
|
25
|
+
const DESIGN = {
|
|
26
|
+
956: { cols: 4, cardW: 219, cardH: 60 },
|
|
27
|
+
640: { cols: 3, cardW: 192, cardH: 60 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Column, never row: in a flex row the harness items shrink, and container queries then
|
|
31
|
+
// report results for a width the component would never actually see.
|
|
32
|
+
const page = (blocks) => `<!doctype html><html class="et-surface--dark"><head><meta charset="utf-8">
|
|
33
|
+
<link rel="stylesheet" href="./styles.css"></head>
|
|
34
|
+
<body class="et-surface--dark" style="margin:0;padding:24px;display:flex;flex-direction:column;gap:24px;align-items:flex-start">
|
|
35
|
+
${blocks}
|
|
36
|
+
</body></html>`;
|
|
37
|
+
|
|
38
|
+
const block = (width) => `<div id="w${width}" class="@container" style="width:${width}px">
|
|
39
|
+
<div class="…">${MARKUP(`w${width}`)}</div>
|
|
40
|
+
</div>`;
|
|
41
|
+
|
|
42
|
+
// page.setContent() renders on about:blank, which blocks file:// subresources — the
|
|
43
|
+
// stylesheet would silently never load. Write a real file and navigate to it.
|
|
44
|
+
writeFileSync('harness.html', page(WIDTHS.map(block).join('\n')));
|
|
45
|
+
|
|
46
|
+
const browser = await chromium.launch();
|
|
47
|
+
const tab = await browser.newPage({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: 2 });
|
|
48
|
+
|
|
49
|
+
await tab.goto(`file://${process.cwd()}/harness.html`);
|
|
50
|
+
await tab.waitForTimeout(300);
|
|
51
|
+
|
|
52
|
+
const measured = await tab.evaluate((widths) => {
|
|
53
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
54
|
+
const box = (id) => document.getElementById(id).getBoundingClientRect();
|
|
55
|
+
const font = (id) => getComputedStyle(document.getElementById(id));
|
|
56
|
+
|
|
57
|
+
return widths.map((width) => {
|
|
58
|
+
const card = box(`w${width}-card`);
|
|
59
|
+
const title = font(`w${width}-title`);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
width,
|
|
63
|
+
cardW: round(card.width),
|
|
64
|
+
cardH: round(card.height),
|
|
65
|
+
titleInset: round(box(`w${width}-title`).left - card.left),
|
|
66
|
+
fontSize: title.fontSize,
|
|
67
|
+
lineHeight: title.lineHeight,
|
|
68
|
+
letterSpacing: title.letterSpacing,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}, WIDTHS);
|
|
72
|
+
|
|
73
|
+
let failures = 0;
|
|
74
|
+
|
|
75
|
+
for (const row of measured) {
|
|
76
|
+
const want = DESIGN[row.width];
|
|
77
|
+
const ok = Math.abs(row.cardW - want.cardW) < 1 && Math.abs(row.cardH - want.cardH) < 0.5;
|
|
78
|
+
|
|
79
|
+
if (!ok) failures++;
|
|
80
|
+
|
|
81
|
+
console.log(`${String(row.width).padStart(4)}px | ${ok ? 'OK ' : 'BAD'} |`, row);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(failures === 0 ? '\nALL GEOMETRY MATCHES' : `\n${failures} MISMATCHES`);
|
|
85
|
+
|
|
86
|
+
await tab.screenshot({ path: 'verify.png', fullPage: true });
|
|
87
|
+
await browser.close();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-flow
|
|
3
|
+
description: How to name and base a branch in this repo, and what its merge request must target - feature, sub-feature, release, release fix and hotfix. Read BEFORE creating a branch, opening a merge request, or choosing what to branch from (e.g. the user says "start work on FIP-2177" or "open an MR").
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
vars: [gitFlowDevelopmentBranch, gitFlowProductionBranch, gitFlowTypes, gitFlowEnforcement, gitFlowSubPrefix]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Git flow
|
|
10
|
+
|
|
11
|
+
Work is organised in two levels: a **main feature branch** per Jira Story, into which
|
|
12
|
+
**sub-feature branches** (one per Task) are merged after review. The main feature branch is
|
|
13
|
+
what gets deployed to a test environment and, once accepted, merged into
|
|
14
|
+
`{%gitFlowDevelopmentBranch%}`.
|
|
15
|
+
|
|
16
|
+
**Never name a branch by hand - `start` does it from the grammar:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx ethlete-agents git-flow start FIP-2178 # reads the issue, names it, branches off the right base
|
|
20
|
+
npx ethlete-agents git-flow check # is the current branch conforming?
|
|
21
|
+
npx ethlete-agents git-flow explain <branch> # what the parser sees, and what it expects
|
|
22
|
+
npx ethlete-agents git-flow repair <branch> # rename a non-conforming one, retarget its MRs
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`start` prints its plan (branch, base, MR target) and asks before writing anything; add
|
|
26
|
+
`--dry-run` to see the plan alone. It refuses on a dirty working tree, and a Task nests under
|
|
27
|
+
its parent Story's branch, so that branch has to exist first.
|
|
28
|
+
|
|
29
|
+
## The five shapes
|
|
30
|
+
|
|
31
|
+
| Shape | Issue | Branch from | MR targets |
|
|
32
|
+
| --------------------------------------------------------------------------------- | ----- | ------------------------------ | ---------------------------------------------------------------- |
|
|
33
|
+
| `feat/FIP-2177-user-management` | Story | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` |
|
|
34
|
+
| `{%gitFlowSubPrefix%}/feat/FIP-2177-user-management/FIP-2178-user-password-reset` | Task | the main feature branch | the main feature branch |
|
|
35
|
+
| `release/2026.04.28` | - | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` and `{%gitFlowProductionBranch%}` |
|
|
36
|
+
| `{%gitFlowSubPrefix%}/release/2026.04.28/FIP-2222-button-not-visible` | Bug | the release branch | the release branch |
|
|
37
|
+
| `hotfix/FIP-2799-password-recovery-broken` | Bug | `{%gitFlowProductionBranch%}` | `{%gitFlowProductionBranch%}` |
|
|
38
|
+
|
|
39
|
+
- The **type** is one of {%gitFlowTypes%}, and a nested branch carries its parent's **full**
|
|
40
|
+
name - that path is what makes the parent machine-readable.
|
|
41
|
+
- The **key** is the Jira issue, uppercase, immediately after the type. The **subject** is the
|
|
42
|
+
Story's subject meta field in kebab-case, not a paraphrase of the summary.
|
|
43
|
+
- A branch with no key still works, but nothing can attribute it to an issue. Add the key.
|
|
44
|
+
|
|
45
|
+
### Why a nested branch starts with `{%gitFlowSubPrefix%}/`
|
|
46
|
+
|
|
47
|
+
Git refuses a ref that is both a branch and a directory of branches. So
|
|
48
|
+
`feat/FIP-2177-user-management/FIP-2178-user-password-reset` **cannot exist** while
|
|
49
|
+
`feat/FIP-2177-user-management` does - git rejects it locally and the push comes back as
|
|
50
|
+
`refname conflict`. The prefix moves the nested tree out of the way and keeps the parent's
|
|
51
|
+
full path inside the child's name, so the MR target is still readable off the name.
|
|
52
|
+
|
|
53
|
+
Do not "fix" a name by dropping the prefix; the branch it produces cannot be created.
|
|
54
|
+
|
|
55
|
+
## Rules that are not about naming
|
|
56
|
+
|
|
57
|
+
- **Never rebase a shared branch.** A main feature branch is published and other people's
|
|
58
|
+
sub-features are based on it - bring it up to date by **merging**
|
|
59
|
+
`{%gitFlowDevelopmentBranch%}` into it. Rebase only a local branch you have not pushed.
|
|
60
|
+
- **A sub-feature merges into its parent, never straight into
|
|
61
|
+
`{%gitFlowDevelopmentBranch%}`** - that is the whole point of the two levels, since the
|
|
62
|
+
parent is what gets tested as a unit.
|
|
63
|
+
- **Delete the source branch on merge** (the checkbox in the merge request) for sub-features,
|
|
64
|
+
release fixes and merged main features.
|
|
65
|
+
- **A hotfix branches off `{%gitFlowProductionBranch%}`** and, after rollout, that branch is
|
|
66
|
+
merged back into `{%gitFlowDevelopmentBranch%}` so the two do not drift.
|
|
67
|
+
- **Never push directly to `{%gitFlowDevelopmentBranch%}` or
|
|
68
|
+
`{%gitFlowProductionBranch%}`.** Open a merge request; it needs another developer's review.
|
|
69
|
+
|
|
70
|
+
## Enforcement is `{%gitFlowEnforcement%}`
|
|
71
|
+
|
|
72
|
+
In `advisory` mode every rule reports and nothing blocks - the older naming shapes (`feature/`
|
|
73
|
+
instead of `feat/`, a lowercase key, no key at all, a `dev-*` integration branch) are accepted
|
|
74
|
+
on purpose while the team adapts. Report the suggestion, do not "fix" someone's existing
|
|
75
|
+
branch unasked - `repair` is how a rename happens, and only when asked for.
|
|
76
|
+
|
|
77
|
+
`dev-*` is the **old spelling of a main feature branch**, not a stray name: sub-features
|
|
78
|
+
legitimately target it. Leave a live one alone.
|
|
79
|
+
|
|
80
|
+
## Commit messages are a separate thing
|
|
81
|
+
|
|
82
|
+
Commits stay conventional (`feat(platform): Prefer a player's common name`) and carry **no
|
|
83
|
+
issue key** - the branch already has it. See {%skill:git-commit%}.
|
|
@@ -15,6 +15,10 @@ continue in a fresh session. Two modes:
|
|
|
15
15
|
- **save** ("handoff", "wrap up") - write a handoff file.
|
|
16
16
|
- **resume** ("continue from the handoff") - read one and continue the work.
|
|
17
17
|
|
|
18
|
+
This skill is installed as `ethlete-handoff`, so under Claude Code the commands
|
|
19
|
+
are `/ethlete-handoff` and `/ethlete-handoff resume [name]` - there is no
|
|
20
|
+
`/handoff`. Name them that way whenever you tell the user what to run.
|
|
21
|
+
|
|
18
22
|
Handoff files live in `{%handoffDir%}/` (gitignored - they are personal,
|
|
19
23
|
ephemeral working state, not team docs).
|
|
20
24
|
|
|
@@ -16,17 +16,17 @@ paged queries, bearer auth, GraphQL, and a socket.io realtime client.
|
|
|
16
16
|
non-trivial query work.** This guide is the index plus the load-bearing facts, so you
|
|
17
17
|
don't re-derive them from source.
|
|
18
18
|
|
|
19
|
-
| Page | Covers
|
|
20
|
-
| ---------------------------------------------------------------------- |
|
|
21
|
-
| {%docsBaseUrl%}/query/ | Overview + the two-generations note
|
|
22
|
-
| {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution
|
|
23
|
-
| {%docsBaseUrl%}/query/features | `withArgs`, `withPolling`, `withAutoRefresh`, side-effect handlers
|
|
24
|
-
| {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress
|
|
25
|
-
| {%docsBaseUrl%}/query/auth | Bearer auth: login/refresh, auto token refresh, multi-tab sync
|
|
26
|
-
| {%docsBaseUrl%}/query/caching · `/stacks` · `/errors` · `/gql` · `/ws` | Caching/dedup, pagination, error/retry, GraphQL, WebSockets
|
|
27
|
-
| {%docsBaseUrl%}/query/multi-tab | Opt-in cross-tab sync: shared responses, per-key polling election, mutation fan-out
|
|
28
|
-
| {%docsBaseUrl%}/query/query-forms | Router-synced filter/search forms
|
|
29
|
-
| {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient`
|
|
19
|
+
| Page | Covers |
|
|
20
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
21
|
+
| {%docsBaseUrl%}/query/ | Overview + the two-generations note |
|
|
22
|
+
| {%docsBaseUrl%}/query/queries | **Start here** - client, creators, the query object's signals, auto-execution |
|
|
23
|
+
| {%docsBaseUrl%}/query/features | `withArgs`, `withPolling`, `withLongPolling`, `withAutoRefresh`, side-effect handlers |
|
|
24
|
+
| {%docsBaseUrl%}/query/http | REST creators, typing requests, response transforms, upload progress |
|
|
25
|
+
| {%docsBaseUrl%}/query/auth | Bearer auth: login/refresh, auto token refresh, multi-tab sync |
|
|
26
|
+
| {%docsBaseUrl%}/query/caching · `/stacks` · `/errors` · `/gql` · `/ws` | Caching/dedup, pagination, error/retry, GraphQL, WebSockets |
|
|
27
|
+
| {%docsBaseUrl%}/query/multi-tab | Opt-in cross-tab sync: shared responses, per-key polling election, mutation fan-out |
|
|
28
|
+
| {%docsBaseUrl%}/query/query-forms | Router-synced filter/search forms |
|
|
29
|
+
| {%docsBaseUrl%}/query/legacy | The maintenance-mode `V2QueryClient` |
|
|
30
30
|
|
|
31
31
|
## Two generations - use the current one
|
|
32
32
|
|
|
@@ -77,9 +77,19 @@ raw `toObservable`). It emits `null` first - `pipe(filter(r => r !== null))`.
|
|
|
77
77
|
- **`withArgs(() => ({ pathParams, queryParams, body }))`** - runs like a `computed`;
|
|
78
78
|
re-runs when a signal it reads changes and re-executes the query. This is how you
|
|
79
79
|
drive **search-as-you-type**: back it with a search signal
|
|
80
|
-
(`withArgs(() => ({ queryParams: { search: this.search() } }))`). Return
|
|
81
|
-
|
|
80
|
+
(`withArgs(() => ({ queryParams: { search: this.search() } }))`). Return `null`
|
|
81
|
+
to park the query - args reset to `null`, pausing polling/auto-refresh.
|
|
82
|
+
- **Prefer `withArgs` over passing `args` to `execute()`.** Args declared on the query
|
|
83
|
+
stay reactive: a `GET` re-executes itself when they change, and `withPolling` /
|
|
84
|
+
`withAutoRefresh` restart off the same signal - none of which happens for args handed
|
|
85
|
+
to `execute()`. A function route additionally throws without it. With `withArgs` in
|
|
86
|
+
place a mutation is just `.execute()`, which reuses the current `args()`. Reserve
|
|
87
|
+
`execute({ args })` for a one-off payload no signal holds (a form submit).
|
|
82
88
|
- `withPolling({ interval })`, `withAutoRefresh({ onSignalChanges: [...] })`.
|
|
89
|
+
- **`withLongPolling({ nextArgs })`** for a completion-driven chain instead of an interval: each
|
|
90
|
+
round starts once the previous settled, with args (a cursor) derived from its response. `nextArgs`
|
|
91
|
+
returning `null` ends the chain. Not `withPolling` with a small interval - and the two throw when
|
|
92
|
+
combined.
|
|
83
93
|
- Side-effects: `withSuccessHandling`, `withErrorHandling`, `withLogging`.
|
|
84
94
|
|
|
85
95
|
There is no built-in debounce operator - dedup/caching handles repeated identical
|
package/package.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/agent-rules",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./git-flow": {
|
|
12
|
+
"types": "./src/lib/git-flow/index.d.ts",
|
|
13
|
+
"default": "./src/lib/git-flow/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
6
17
|
"main": "./src/index.js",
|
|
7
18
|
"typings": "./src/index.d.ts",
|
|
8
19
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const config_1 = require("./lib/config");
|
|
8
|
+
const git_flow_command_1 = require("./lib/git-flow-command");
|
|
8
9
|
const migrate_1 = require("./lib/migrate");
|
|
9
10
|
const sync_1 = require("./lib/sync");
|
|
10
11
|
const USAGE = `ethlete-agents — compile @ethlete agent rules and skills into your repo
|
|
@@ -12,6 +13,8 @@ const USAGE = `ethlete-agents — compile @ethlete agent rules and skills into y
|
|
|
12
13
|
ethlete-agents sync Write the generated rules/skills for every detected agent
|
|
13
14
|
ethlete-agents check Exit non-zero when the generated files are out of date (for CI)
|
|
14
15
|
ethlete-agents init Write a starter ${config_1.CONFIG_FILE_NAME}
|
|
16
|
+
ethlete-agents git-flow Name, check and repair branches against the repo's git flow
|
|
17
|
+
(start, check, repair, explain)
|
|
15
18
|
ethlete-agents migrate Convert the repo to the AGENTS.md + .agents/skills layout:
|
|
16
19
|
CLAUDE.md content moves into AGENTS.md (CLAUDE.md becomes an
|
|
17
20
|
@AGENTS.md import), hand-written .claude/skills move to
|
|
@@ -66,6 +69,8 @@ const run = (argv) => {
|
|
|
66
69
|
return (0, sync_1.check)(options);
|
|
67
70
|
case 'init':
|
|
68
71
|
return init(root);
|
|
72
|
+
case 'git-flow':
|
|
73
|
+
return (0, git_flow_command_1.gitFlowCommand)({ root, argv: argv.slice(1) });
|
|
69
74
|
case 'migrate':
|
|
70
75
|
return (0, migrate_1.migrate)(Object.assign(Object.assign({}, options), { dryRun: argv.includes('--dry-run') }));
|
|
71
76
|
default:
|
|
@@ -76,12 +81,12 @@ const run = (argv) => {
|
|
|
76
81
|
tslib_1.__exportStar(require("./lib"), exports);
|
|
77
82
|
// Guarded so the package can also be imported as a library without running the CLI.
|
|
78
83
|
if (require.main === module) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
Promise.resolve()
|
|
85
|
+
.then(() => run(process.argv.slice(2)))
|
|
86
|
+
.then((code) => process.exit(code))
|
|
87
|
+
.catch((error) => {
|
|
83
88
|
console.error(error instanceof Error ? error.message : error);
|
|
84
89
|
process.exit(1);
|
|
85
|
-
}
|
|
90
|
+
});
|
|
86
91
|
}
|
|
87
92
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/agent-rules/src/index.ts"],"names":[],"mappings":";;;;AACA,2BAA6D;AAC7D,+BAA4B;AAC5B,yCAA2F;AAC3F,2CAAwC;AACxC,qCAAyC;AAEzC,MAAM,KAAK,GAAG;;;;6CAI+B,yBAAgB
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/agent-rules/src/index.ts"],"names":[],"mappings":"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"}
|