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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/AGENTS.md +28 -0
  2. package/CHANGELOG.md +480 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +362 -29
  5. package/THIRD-PARTY-LICENSES.md +35 -0
  6. package/content/defaults.json +1 -1
  7. package/content/git-hooks/post-checkout.sh +10 -0
  8. package/content/git-hooks/pre-push.sh +5 -0
  9. package/content/hooks/context-warning.py +872 -110
  10. package/content/hooks/subagent-model-policy.py +194 -0
  11. package/content/mods/.claude-plugin/marketplace.json +11 -0
  12. package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
  13. package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
  14. package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
  15. package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
  16. package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
  17. package/content/output-styles/ste-clarity.md +131 -0
  18. package/content/rules/agent-sessions-wake-up.md +11 -0
  19. package/content/rules/app-styling.md +66 -0
  20. package/content/rules/comments.md +4 -1
  21. package/content/rules/lint-and-format.md +7 -5
  22. package/content/rules/nx-layout.md +50 -0
  23. package/content/rules/reactive-state.md +1 -1
  24. package/content/rules/styling.md +9 -7
  25. package/content/rules/subagent-models.md +39 -0
  26. package/content/skills/agent-sessions/SKILL.md +81 -0
  27. package/content/skills/agent-sessions/agent-sessions.mjs +498 -0
  28. package/content/skills/angular-patterns/SKILL.md +30 -7
  29. package/content/skills/api-source/SKILL.md +156 -0
  30. package/content/skills/app-testing/SKILL.md +141 -0
  31. package/content/skills/codex-subagent/SKILL.md +90 -0
  32. package/content/skills/codex-subagent/codex-agent.mjs +229 -0
  33. package/content/skills/design-exploration/SKILL.md +687 -0
  34. package/content/skills/design-exploration/check-story.mjs +139 -0
  35. package/content/skills/design-exploration/shoot-template.mjs +61 -0
  36. package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
  37. package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  38. package/content/skills/domain-modeling/SKILL.md +80 -0
  39. package/content/skills/e2e-triage/SKILL.md +80 -0
  40. package/content/skills/figma-export/SKILL.md +191 -0
  41. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  42. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  43. package/content/skills/figma-export/measure-template.mjs +87 -0
  44. package/content/skills/git-commit/SKILL.md +11 -7
  45. package/content/skills/git-flow/SKILL.md +87 -0
  46. package/content/skills/grill-with-docs/SKILL.md +13 -0
  47. package/content/skills/grilling/SKILL.md +51 -0
  48. package/content/skills/handoff/SKILL.md +125 -5
  49. package/content/skills/query/SKILL.md +165 -64
  50. package/content/skills/rxjs-signals/SKILL.md +33 -4
  51. package/content/skills/sdk-docs/SKILL.md +50 -14
  52. package/content/skills/sdk-local-build/SKILL.md +26 -8
  53. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  54. package/content/skills/sdk-source/SKILL.md +37 -34
  55. package/content/skills/sdk-update/SKILL.md +103 -0
  56. package/content/skills/story-styling/SKILL.md +10 -8
  57. package/content/skills/styleguide/SKILL.md +12 -7
  58. package/content/skills/styleguide/assets.md +54 -0
  59. package/content/skills/styleguide/changesets.md +17 -0
  60. package/content/skills/styleguide/file-structure.md +73 -0
  61. package/content/skills/styleguide/lint-rule-lookup.md +51 -0
  62. package/content/skills/styleguide/storybook-structure.md +25 -0
  63. package/content/skills/theming/SKILL.md +50 -28
  64. package/content/skills/timetrack/SKILL.md +397 -0
  65. package/content/skills/verify-in-app/SKILL.md +107 -0
  66. package/migrations/app-styling-utilities.md +63 -0
  67. package/migrations/list-state-query-form.md +20 -0
  68. package/migrations/nx-layout.md +29 -0
  69. package/migrations/sdk-components-over-hand-built-ui.md +27 -0
  70. package/migrations/search-query-field.md +52 -0
  71. package/migrations.json +48 -0
  72. package/package.json +19 -3
  73. package/src/index.js +85 -30
  74. package/src/index.js.map +1 -1
  75. package/src/lib/cli-args.d.ts +9 -0
  76. package/src/lib/cli-args.js +48 -0
  77. package/src/lib/cli-args.js.map +1 -0
  78. package/src/lib/commitlint.d.ts +10 -0
  79. package/src/lib/commitlint.js +51 -0
  80. package/src/lib/commitlint.js.map +1 -0
  81. package/src/lib/config.d.ts +41 -5
  82. package/src/lib/config.js +117 -10
  83. package/src/lib/config.js.map +1 -1
  84. package/src/lib/filter.d.ts +2 -0
  85. package/src/lib/filter.js +4 -4
  86. package/src/lib/filter.js.map +1 -1
  87. package/src/lib/frontmatter.d.ts +2 -0
  88. package/src/lib/frontmatter.js +33 -5
  89. package/src/lib/frontmatter.js.map +1 -1
  90. package/src/lib/git-flow/build.d.ts +35 -0
  91. package/src/lib/git-flow/build.js +24 -0
  92. package/src/lib/git-flow/build.js.map +1 -0
  93. package/src/lib/git-flow/config.d.ts +59 -0
  94. package/src/lib/git-flow/config.js +50 -0
  95. package/src/lib/git-flow/config.js.map +1 -0
  96. package/src/lib/git-flow/index.d.ts +6 -0
  97. package/src/lib/git-flow/index.js +23 -0
  98. package/src/lib/git-flow/index.js.map +1 -0
  99. package/src/lib/git-flow/parse.d.ts +49 -0
  100. package/src/lib/git-flow/parse.js +300 -0
  101. package/src/lib/git-flow/parse.js.map +1 -0
  102. package/src/lib/git-flow/rename.d.ts +24 -0
  103. package/src/lib/git-flow/rename.js +70 -0
  104. package/src/lib/git-flow/rename.js.map +1 -0
  105. package/src/lib/git-flow/start.d.ts +49 -0
  106. package/src/lib/git-flow/start.js +57 -0
  107. package/src/lib/git-flow/start.js.map +1 -0
  108. package/src/lib/git-flow/validate.d.ts +34 -0
  109. package/src/lib/git-flow/validate.js +72 -0
  110. package/src/lib/git-flow/validate.js.map +1 -0
  111. package/src/lib/git-flow-command.d.ts +4 -0
  112. package/src/lib/git-flow-command.js +165 -0
  113. package/src/lib/git-flow-command.js.map +1 -0
  114. package/src/lib/git-flow-repair.d.ts +17 -0
  115. package/src/lib/git-flow-repair.js +165 -0
  116. package/src/lib/git-flow-repair.js.map +1 -0
  117. package/src/lib/git-flow-start.d.ts +20 -0
  118. package/src/lib/git-flow-start.js +140 -0
  119. package/src/lib/git-flow-start.js.map +1 -0
  120. package/src/lib/git.d.ts +64 -0
  121. package/src/lib/git.js +131 -0
  122. package/src/lib/git.js.map +1 -0
  123. package/src/lib/gitlab.d.ts +35 -0
  124. package/src/lib/gitlab.js +116 -0
  125. package/src/lib/gitlab.js.map +1 -0
  126. package/src/lib/index.d.ts +2 -0
  127. package/src/lib/index.js +26 -11
  128. package/src/lib/index.js.map +1 -1
  129. package/src/lib/migrate.js +6 -1
  130. package/src/lib/migrate.js.map +1 -1
  131. package/src/lib/output-style-command.d.ts +3 -0
  132. package/src/lib/output-style-command.js +69 -0
  133. package/src/lib/output-style-command.js.map +1 -0
  134. package/src/lib/output-style.d.ts +38 -0
  135. package/src/lib/output-style.js +134 -0
  136. package/src/lib/output-style.js.map +1 -0
  137. package/src/lib/owned-paths.js +27 -3
  138. package/src/lib/owned-paths.js.map +1 -1
  139. package/src/lib/package-runner.d.ts +4 -0
  140. package/src/lib/package-runner.js +86 -0
  141. package/src/lib/package-runner.js.map +1 -0
  142. package/src/lib/plain-text.d.ts +9 -0
  143. package/src/lib/plain-text.js +22 -0
  144. package/src/lib/plain-text.js.map +1 -0
  145. package/src/lib/plan.d.ts +6 -6
  146. package/src/lib/plan.js +164 -40
  147. package/src/lib/plan.js.map +1 -1
  148. package/src/lib/prompt.d.ts +8 -0
  149. package/src/lib/prompt.js +35 -0
  150. package/src/lib/prompt.js.map +1 -0
  151. package/src/lib/render.d.ts +25 -2
  152. package/src/lib/render.js +43 -10
  153. package/src/lib/render.js.map +1 -1
  154. package/src/lib/sessions-command.d.ts +4 -0
  155. package/src/lib/sessions-command.js +16 -0
  156. package/src/lib/sessions-command.js.map +1 -0
  157. package/src/lib/sync.d.ts +0 -1
  158. package/src/lib/sync.js +30 -17
  159. package/src/lib/sync.js.map +1 -1
  160. package/src/lib/targets/agents-skills.js +1 -1
  161. package/src/lib/targets/agents-skills.js.map +1 -1
  162. package/src/lib/targets/claude-hooks.d.ts +3 -23
  163. package/src/lib/targets/claude-hooks.js +22 -86
  164. package/src/lib/targets/claude-hooks.js.map +1 -1
  165. package/src/lib/targets/claude-mods.d.ts +20 -0
  166. package/src/lib/targets/claude-mods.js +47 -0
  167. package/src/lib/targets/claude-mods.js.map +1 -0
  168. package/src/lib/targets/claude.js +2 -3
  169. package/src/lib/targets/claude.js.map +1 -1
  170. package/src/lib/targets/codex-hooks.d.ts +12 -0
  171. package/src/lib/targets/codex-hooks.js +35 -0
  172. package/src/lib/targets/codex-hooks.js.map +1 -0
  173. package/src/lib/targets/codex.js +4 -4
  174. package/src/lib/targets/codex.js.map +1 -1
  175. package/src/lib/targets/copilot.js +4 -4
  176. package/src/lib/targets/copilot.js.map +1 -1
  177. package/src/lib/targets/cursor.js +3 -3
  178. package/src/lib/targets/cursor.js.map +1 -1
  179. package/src/lib/targets/git-hooks.d.ts +25 -0
  180. package/src/lib/targets/git-hooks.js +71 -0
  181. package/src/lib/targets/git-hooks.js.map +1 -0
  182. package/src/lib/targets/hooks-shared.d.ts +60 -0
  183. package/src/lib/targets/hooks-shared.js +130 -0
  184. package/src/lib/targets/hooks-shared.js.map +1 -0
  185. package/src/lib/targets/mcp.d.ts +16 -0
  186. package/src/lib/targets/mcp.js +68 -0
  187. package/src/lib/targets/mcp.js.map +1 -0
  188. package/src/lib/targets/shared.d.ts +1 -9
  189. package/src/lib/targets/shared.js +4 -8
  190. package/src/lib/targets/shared.js.map +1 -1
  191. package/src/lib/timetrack-command.d.ts +11 -0
  192. package/src/lib/timetrack-command.js +1063 -0
  193. package/src/lib/timetrack-command.js.map +1 -0
  194. package/src/lib/timetrack-snapshot.d.ts +55 -0
  195. package/src/lib/timetrack-snapshot.js +120 -0
  196. package/src/lib/timetrack-snapshot.js.map +1 -0
  197. package/src/lib/timetrack.d.ts +561 -0
  198. package/src/lib/timetrack.js +279 -0
  199. package/src/lib/timetrack.js.map +1 -0
  200. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
@@ -0,0 +1,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();
@@ -1,23 +1,22 @@
1
1
  ---
2
2
  name: git-commit
3
- description: How to write git commits in this repo - commitlint format (type(scope): Subject), lean messages, no trailers. Read before committing anything (e.g. the user says "commit this").
3
+ description: How to write git commits in this repo - conventional format (type(scope): Subject), lean messages, no trailers. Read before committing anything (e.g. the user says "commit this").
4
4
  kind: skill
5
5
  scope: both
6
- vars: [commitScopes]
6
+ vars: [commitScopes, commitRuleSource, commitValidation]
7
7
  ---
8
8
 
9
9
  # Git commits
10
10
 
11
- Commits are **lean** and follow the **commitlint rules** in `commitlint.config.js`
12
- (conventional commits with a required scope):
11
+ Commits are **lean** and follow {%commitRuleSource%}:
13
12
 
14
- - **Format: `type(scope): Subject`** - one line. All three parts are enforced:
13
+ - **Format: `type(scope): Subject`** - one line, all three parts required:
15
14
  - `type` ∈ `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
16
15
  `build`, `ci`, `chore`, `revert`
17
- - `scope` is **required**, ∈ {%commitScopes%}
16
+ - `scope` ∈ {%commitScopes%}
18
17
  - Subject is **sentence-case** ("Add the search filter", not
19
18
  "add the search filter")
20
- - When unsure a message passes, check it: `echo "<msg>" | npx commitlint`.
19
+ - {%commitValidation%}
21
20
  - Add a short body only when the change genuinely needs context that the diff
22
21
  can't convey.
23
22
  - **No trailers.** Never append `Co-Authored-By`, `Claude-Session`, or similar
@@ -26,4 +25,9 @@ Commits are **lean** and follow the **commitlint rules** in `commitlint.config.j
26
25
  - **Stage only what belongs to the change.** The working tree often carries
27
26
  unrelated in-progress work - `git add` the specific files, never `git add -A`
28
27
  blindly.
28
+ - **In a checkout that other sessions share**, commit with
29
+ `git commit -- <paths>`, so their staged files stay out. `git commit -- <path>`
30
+ refuses an untracked file: run `git add -N <that file>` for that one path
31
+ first. Pass a formatter or `eslint --fix` the exact paths you changed, never a
32
+ list from `git diff` or `git status` - that list holds other sessions' files.
29
33
  - Don't push unless asked.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: git-flow
3
+ description: How to name and base a branch in this repo, and what its merge request must target - feature, sub-feature, release, release fix and hotfix. Read BEFORE creating a branch, opening a merge request, or choosing what to branch from (e.g. the user says "start work on FIP-2177" or "open an MR").
4
+ kind: skill
5
+ scope: both
6
+ vars: [gitFlowDevelopmentBranch, gitFlowProductionBranch, gitFlowTypes, gitFlowEnforcement, gitFlowSubPrefix]
7
+ ---
8
+
9
+ # Git flow
10
+
11
+ Work is organised in two levels: a **main feature branch** per Jira Story, into which
12
+ **sub-feature branches** (one per Task) are merged after review. The main feature branch is
13
+ what gets deployed to a test environment and, once accepted, merged into
14
+ `{%gitFlowDevelopmentBranch%}`.
15
+
16
+ **Never name a branch by hand - `start` does it from the grammar:**
17
+
18
+ ```bash
19
+ {%packageRunner%} ethlete-agents git-flow start FIP-2178 # reads the issue, names it, branches off the right base
20
+ {%packageRunner%} ethlete-agents git-flow check # is the current branch conforming?
21
+ {%packageRunner%} ethlete-agents git-flow explain <branch> # what the parser sees, and what it expects
22
+ {%packageRunner%} ethlete-agents git-flow repair <branch> # rename a non-conforming one, retarget its MRs
23
+ ```
24
+
25
+ `start` prints its plan (branch, base, MR target) and asks before writing anything; add
26
+ `--dry-run` to see the plan alone. It refuses on a dirty working tree, and a Task nests under
27
+ its parent Story's branch, so that branch has to exist first.
28
+
29
+ It reads the issue through the running Timetrack app, which holds this machine's only Jira
30
+ credentials. If it reports that the app is not running, ask the
31
+ user to start it. `--subject <text>` names a branch without reading Jira at all.
32
+
33
+ ## The five shapes
34
+
35
+ | Shape | Issue | Branch from | MR targets |
36
+ | --------------------------------------------------------------------------------- | ----- | ------------------------------ | ---------------------------------------------------------------- |
37
+ | `feat/FIP-2177-user-management` | Story | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` |
38
+ | `{%gitFlowSubPrefix%}/feat/FIP-2177-user-management/FIP-2178-user-password-reset` | Task | the main feature branch | the main feature branch |
39
+ | `release/2026.04.28` | - | `{%gitFlowDevelopmentBranch%}` | `{%gitFlowDevelopmentBranch%}` and `{%gitFlowProductionBranch%}` |
40
+ | `{%gitFlowSubPrefix%}/release/2026.04.28/FIP-2222-button-not-visible` | Bug | the release branch | the release branch |
41
+ | `hotfix/FIP-2799-password-recovery-broken` | Bug | `{%gitFlowProductionBranch%}` | `{%gitFlowProductionBranch%}` |
42
+
43
+ - The **type** is one of {%gitFlowTypes%}, and a nested branch carries its parent's **full**
44
+ name - that path is what makes the parent machine-readable.
45
+ - The **key** is the Jira issue, uppercase, immediately after the type. The **subject** is the
46
+ Story's subject meta field in kebab-case, not a paraphrase of the summary.
47
+ - A branch with no key still works, but nothing can attribute it to an issue. Add the key.
48
+
49
+ ### Why a nested branch starts with `{%gitFlowSubPrefix%}/`
50
+
51
+ Git refuses a ref that is both a branch and a directory of branches. So
52
+ `feat/FIP-2177-user-management/FIP-2178-user-password-reset` **cannot exist** while
53
+ `feat/FIP-2177-user-management` does - git rejects it locally and the push comes back as
54
+ `refname conflict`. The prefix moves the nested tree out of the way and keeps the parent's
55
+ full path inside the child's name, so the MR target is still readable off the name.
56
+
57
+ Do not "fix" a name by dropping the prefix; the branch it produces cannot be created.
58
+
59
+ ## Rules that are not about naming
60
+
61
+ - **Never rebase a shared branch.** A main feature branch is published and other people's
62
+ sub-features are based on it - bring it up to date by **merging**
63
+ `{%gitFlowDevelopmentBranch%}` into it. Rebase only a local branch you have not pushed.
64
+ - **A sub-feature merges into its parent, never straight into
65
+ `{%gitFlowDevelopmentBranch%}`** - that is the whole point of the two levels, since the
66
+ parent is what gets tested as a unit.
67
+ - **Delete the source branch on merge** (the checkbox in the merge request) for sub-features,
68
+ release fixes and merged main features.
69
+ - **A hotfix branches off `{%gitFlowProductionBranch%}`** and, after rollout, that branch is
70
+ merged back into `{%gitFlowDevelopmentBranch%}` so the two do not drift.
71
+ - **Never push directly to `{%gitFlowDevelopmentBranch%}` or
72
+ `{%gitFlowProductionBranch%}`.** Open a merge request; it needs another developer's review.
73
+
74
+ ## Enforcement is `{%gitFlowEnforcement%}`
75
+
76
+ In `advisory` mode every rule reports and nothing blocks - the older naming shapes (`feature/`
77
+ instead of `feat/`, a lowercase key, no key at all, a `dev-*` integration branch) are accepted
78
+ on purpose while the team adapts. Report the suggestion, do not "fix" someone's existing
79
+ branch unasked - `repair` is how a rename happens, and only when asked for.
80
+
81
+ `dev-*` is the **old spelling of a main feature branch**, not a stray name: sub-features
82
+ legitimately target it. Leave a live one alone.
83
+
84
+ ## Commit messages are a separate thing
85
+
86
+ Commits stay conventional (`feat(platform): Prefer a player's common name`) and carry **no
87
+ issue key** - the branch already has it. Follow the repository's commit-message guidance.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: grill-with-docs
3
+ description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
4
+ kind: skill
5
+ scope: both
6
+ modelInvocation: false
7
+ ---
8
+
9
+ Call the Skill tool twice, for {%skill:grilling%} and {%skill:domain-modeling%}.
10
+
11
+ ---
12
+
13
+ Vendored from [mattpocock/skills](https://github.com/mattpocock/skills) at `3cca18b`, MIT (Copyright (c) 2026 Matt Pocock).
@@ -0,0 +1,51 @@
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).
35
+
36
+ ## Local addition: write each question so it reads alone
37
+
38
+ This section is ours, not upstream.
39
+
40
+ - **One question, one situation.** Say what the thing is and why the decision matters, in plain
41
+ words, before the choices. The user must understand it without the earlier rounds.
42
+ - **No reference chains.** Never lean on other question numbers, ADR numbers, slice numbers or
43
+ plan shorthand ("as Q6 settled", "per ADR 07"). Name the actual thing instead.
44
+ - **Remind the user of a concept** when a round starts a new topic: one or two sentences, with a
45
+ concrete example ("a sketch is a draft with several variants of one part, for example three
46
+ headers").
47
+ - **Check the facts before you recommend.** A recommendation built on an assumed file format or
48
+ an assumed existing tool is worse than none. Look it up first, and say in the round when a
49
+ look-up proved an earlier recommendation wrong.
50
+ - **Close with the shared state.** When the frontier is empty, list every decision in one short
51
+ summary and ask the user to confirm it before you write it anywhere.