@event4u/agent-config 9.10.0 → 9.12.0
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +111 -0
- package/README.md +20 -16
- package/agents/templates/.ai-council.yml.example +481 -0
- package/agents/templates/.ai-video.xml.example +247 -0
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/design-fidelity.md +2 -2
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- package/dist/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/dist/cli/initRouting.js +74 -11
- package/dist/cli/initRouting.js.map +1 -1
- package/dist/cli/main.js +14 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
- package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
- package/dist/cli-delegate/chunk-A3HJ6NTI.js +1084 -0
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/chunk-FTAISGDQ.js +117 -0
- package/dist/cli-delegate/chunk-GME3Q44L.js +82 -0
- package/dist/cli-delegate/chunk-GRRPEPRN.js +336 -0
- package/dist/cli-delegate/chunk-HJ6DAJHR.js +502 -0
- package/dist/cli-delegate/chunk-HYWSRNUX.js +179 -0
- package/dist/cli-delegate/chunk-J55WI3RR.js +47 -0
- package/dist/cli-delegate/chunk-MCJYCQPF.js +35 -0
- package/dist/cli-delegate/chunk-NDJLNC6Q.js +169 -0
- package/dist/cli-delegate/chunk-OMIVVCG2.js +26 -0
- package/dist/cli-delegate/chunk-OY53J6II.js +7336 -0
- package/dist/cli-delegate/chunk-QFKQLQ7X.js +10835 -0
- package/dist/cli-delegate/chunk-TEGJU2JX.js +6908 -0
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
- package/dist/cli-delegate/cmd_analyze_session.js +294 -0
- package/dist/cli-delegate/cmd_conformance.js +643 -0
- package/dist/cli-delegate/cmd_converge.js +326 -0
- package/dist/cli-delegate/cmd_doctor.js +142 -0
- package/dist/cli-delegate/cmd_explain.js +2576 -0
- package/dist/cli-delegate/cmd_export.js +298 -0
- package/dist/cli-delegate/cmd_fleet.js +333 -0
- package/dist/cli-delegate/cmd_memory_get.js +3878 -0
- package/dist/cli-delegate/cmd_migrate.js +11 -0
- package/dist/cli-delegate/cmd_preflight.js +153 -0
- package/dist/cli-delegate/cmd_prune.js +446 -0
- package/dist/cli-delegate/cmd_refresh.js +28 -0
- package/dist/cli-delegate/cmd_settings_check.js +574 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +225 -0
- package/dist/cli-delegate/cmd_sync.js +266 -0
- package/dist/cli-delegate/cmd_uninstall.js +657 -0
- package/dist/cli-delegate/cmd_update.js +425 -0
- package/dist/cli-delegate/cmd_upgrade.js +444 -0
- package/dist/cli-delegate/cmd_validate.js +307 -0
- package/dist/cli-delegate/cmd_versions.js +370 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +45 -16
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -7
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/install/emit_host_rules_cli.js +2 -1
- package/dist/install/emit_host_rules_cli.js.map +1 -1
- package/dist/install/install.mjs +634 -409
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +7 -3
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/toolDetection.js +40 -12
- package/dist/install/toolDetection.js.map +1 -1
- package/dist/install/wizard-plan.js +12 -3
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1553 -2728
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +6 -22
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +14 -0
- package/package.json +4 -2
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- package/src/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/src/config/discovery/packs.yml +7 -0
- package/src/config/evaluator-budgets.json +1 -1
- package/src/config/pack-size-budget.json +27 -0
- package/src/install/emit_host_rules_cli.ts +2 -1
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +7 -3
- package/src/install/toolDetection.ts +39 -8
- package/src/install/wizard-plan.ts +13 -3
- package/src/scripts/_cli/cmd_conformance.ts +2 -1
- package/src/scripts/_cli/cmd_converge.ts +2 -1
- package/src/scripts/_cli/cmd_doctor.ts +214 -13
- package/src/scripts/_cli/cmd_export.ts +2 -1
- package/src/scripts/_cli/cmd_fleet.ts +2 -1
- package/src/scripts/_cli/cmd_migrate.ts +2 -1
- package/src/scripts/_cli/cmd_preflight.ts +2 -1
- package/src/scripts/_cli/cmd_refresh.ts +2 -1
- package/src/scripts/_cli/cmd_update.ts +2 -1
- package/src/scripts/_cli/cmd_upgrade.ts +2 -1
- package/src/scripts/_cli/cmd_versions.ts +2 -1
- package/src/scripts/_cli/detection_report.ts +480 -0
- package/src/scripts/_dispatch.bash +52 -14
- package/src/scripts/_lib/changelog_eras.ts +103 -9
- package/src/scripts/_lib/environment_detector.ts +441 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- package/src/scripts/_lib/package_root.ts +72 -0
- package/src/scripts/ai_council/config.ts +15 -7
- package/src/scripts/ai_council/modes.ts +68 -2
- package/src/scripts/ai_council/transport_resolver.ts +331 -0
- package/src/scripts/check_bundle_path_leakage.ts +10 -4
- package/src/scripts/check_dependency_floors.ts +153 -0
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/check_pack_size.ts +188 -0
- package/src/scripts/condense.ts +7 -28
- package/src/scripts/council_cli.ts +6 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_eval_fixture_citations.ts +113 -0
- package/src/scripts/lint_ui_stack_bundles.ts +201 -0
|
@@ -53,7 +53,10 @@ degrades gracefully: with no brand layer, only stage 1 runs.
|
|
|
53
53
|
`$type: "typography"` and `$value` objects carrying `fontFamily`,
|
|
54
54
|
`fontSize`, `fontWeight`, `lineHeight`.
|
|
55
55
|
5. **Verify** — confirm both chosen fonts exist on Google Fonts (check the
|
|
56
|
-
`Google Fonts URL` column in the CSV
|
|
56
|
+
`Google Fonts URL` column in the CSV — this is an **availability** check,
|
|
57
|
+
"does this font exist?", and is deliberately separate from the **delivery**
|
|
58
|
+
decision, "how does it get onto the page?" — see § Delivery below; conflating
|
|
59
|
+
the two is what produced a single hard-wired hotlink); run
|
|
57
60
|
`./scripts-run <skills-root>/design-tokens/scripts/tokens validate --dir src/`
|
|
58
61
|
to confirm no hardcoded font-size or font-family values remain outside the
|
|
59
62
|
token file; exit code 0 is the evidence.
|
|
@@ -92,6 +95,31 @@ Consumer brand tokens outrank the corpus filter
|
|
|
92
95
|
([`brand-source-of-truth`](../../rules/brand-source-of-truth.md)) — if the
|
|
93
96
|
brand already registers fonts, use them and skip the filter.
|
|
94
97
|
|
|
98
|
+
## Delivery — hosting mode (self-hosted by default)
|
|
99
|
+
|
|
100
|
+
The delivery policy is **owned** by
|
|
101
|
+
[`design-fidelity-mechanics`](../../../docs/guidelines/design-fidelity-mechanics.md)
|
|
102
|
+
§ Asset & imagery discipline ([`ADR-205`](../../../docs/decisions/ADR-205-webfont-delivery-ownership.md));
|
|
103
|
+
this skill is a consumer and emits the route, never a competing policy. A font
|
|
104
|
+
CDN link transmits the **visitor's IP** to that third party on every page view —
|
|
105
|
+
a German court (LG München I) ruled on exactly that for hotlinked Google Fonts. <!-- md-language-check: ignore -->
|
|
106
|
+
|
|
107
|
+
**Default: self-hosted.** Take the row's `Self-Hosted Route` column and resolve
|
|
108
|
+
it against the detected target stack:
|
|
109
|
+
|
|
110
|
+
| Detected target | Route to emit | Where that answer comes from |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| Next.js | `next/font/google` (self-hosts at build) or `next/font/local` | `data/stacks/nextjs.csv` rows 22–24 — already prescribes this and lists the CDN `<link>` in its **Don't** column |
|
|
113
|
+
| Any bundler stack (Vite / webpack — React, Vue, Svelte, Nuxt, Astro) | the font's `@fontsource/*` package + a CSS import of the package | the package registry; verify it exists before suggesting it (`supply-chain-intake`) |
|
|
114
|
+
| Server-rendered asset pipeline (Laravel/Vite, Rails, Django) | copy the `woff2` into the project's asset directory, serve it, declare `@font-face` | the project's own pipeline (`design-fidelity-mechanics` § owned-asset path) |
|
|
115
|
+
| No build step / plain HTML | `@font-face` over a locally-served `woff2`, plus `<link rel="preload" as="font">` | `data/stacks/astro.csv` row 23 (preload of a local `woff2`) |
|
|
116
|
+
| Stack unknown | the plain `@font-face` floor, and **say** the route is unresolved — never fall back to the hotlink to avoid the question | — |
|
|
117
|
+
|
|
118
|
+
**Hotlink: opt-in only.** Emit the row's `CSS Import` value **only** when the
|
|
119
|
+
consumer explicitly asked for the CDN route, and state in the same breath that
|
|
120
|
+
it transmits the visitor's IP to the third party. Never pick it because it is
|
|
121
|
+
the shorter line.
|
|
122
|
+
|
|
95
123
|
## Output format
|
|
96
124
|
|
|
97
125
|
1. **Chosen pairing + rationale** — heading font, body font, the CSV row's
|
|
@@ -99,19 +127,31 @@ brand already registers fonts, use them and skip the filter.
|
|
|
99
127
|
2. **DTCG type-token block** — the `typography` section ready to paste into
|
|
100
128
|
`tokens.json` (DTCG `$type: "typography"`, `$value` with `fontFamily`,
|
|
101
129
|
`fontSize`, `fontWeight`, `lineHeight` per level).
|
|
102
|
-
3. **
|
|
103
|
-
`
|
|
104
|
-
`Tailwind Config`.
|
|
130
|
+
3. **Font delivery block** — the self-hosted route for the detected stack per
|
|
131
|
+
§ Delivery (from the row's `Self-Hosted Route` column), and the Tailwind
|
|
132
|
+
`fontFamily` config snippet from `Tailwind Config`. The row's
|
|
133
|
+
`CSS Import` hotlink appears here **only** under an explicit consumer opt-in,
|
|
134
|
+
with its IP-transmission note.
|
|
105
135
|
|
|
106
136
|
## Gotcha
|
|
107
137
|
|
|
108
|
-
- **Stale CSV URL → silent system-font fallback
|
|
109
|
-
is Reference data on a freshness contract — never
|
|
110
|
-
Fonts API call. If a font was retired or renamed, the
|
|
111
|
-
build time and the browser silently falls back to the
|
|
112
|
-
broken visual design with no obvious error.
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
- **Stale CSV URL → silent system-font fallback (opt-in hotlink path only).**
|
|
139
|
+
`font-pairings-reference.csv` is Reference data on a freshness contract — never
|
|
140
|
+
substitute a live Google Fonts API call. If a font was retired or renamed, the
|
|
141
|
+
`@import` URL 404s at build time and the browser silently falls back to the
|
|
142
|
+
system font, shipping broken visual design with no obvious error. This caveat
|
|
143
|
+
**still applies verbatim to the opt-in hotlink route**: verify the URL resolves
|
|
144
|
+
(Step 5) before emitting the import line; if it 404s, surface the error and ask
|
|
145
|
+
the user to pick an alternative pairing from the CSV.
|
|
146
|
+
- **The self-hosted default replaces that failure mode, it does not delete it.**
|
|
147
|
+
For a package route (`next/font`, `@fontsource/*`) resolution moves to
|
|
148
|
+
**install** time, where a wrong name fails loudly instead of 404-ing at build.
|
|
149
|
+
What remains is a **family-name mismatch**: the emitted CSS asks for
|
|
150
|
+
`font-family: 'Inter'` while the installed package provides a differently-named
|
|
151
|
+
family (`Inter Tight`), which renders the system font at runtime with no error
|
|
152
|
+
at all — the same silence, one layer later. Assert the family name in the
|
|
153
|
+
emitted CSS matches the one the installed package declares; the row's
|
|
154
|
+
`Google Fonts URL` stays useful as the reference spelling to compare against.
|
|
115
155
|
|
|
116
156
|
## Do NOT
|
|
117
157
|
|
|
@@ -119,6 +159,10 @@ brand already registers fonts, use them and skip the filter.
|
|
|
119
159
|
inline) — every size belongs in `tokens.json` under the `typography` layer.
|
|
120
160
|
- Do NOT bypass `design-tokens` and write raw CSS custom properties for fonts
|
|
121
161
|
— `tokens.json` is the single source; hand-crafted `--font-*` vars drift.
|
|
162
|
+
- Do NOT emit a font-CDN `@import` / `<link>` as the default deliverable — the
|
|
163
|
+
self-hosted route is the default and the hotlink is an explicit opt-in
|
|
164
|
+
(§ Delivery). Reaching for the CDN line because the stack was not detected is
|
|
165
|
+
the exact substitution this skill used to hard-wire.
|
|
122
166
|
- Do NOT treat `font-pairings-reference.csv` as decision logic — it is
|
|
123
167
|
Reference data; the agent picks based on the style constraint, not by
|
|
124
168
|
iterating the CSV as a rules engine.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
model_tier: medium
|
|
3
|
+
name: ui-apply-generic
|
|
4
|
+
description: "Use when implementing a UI brief on a stack with no framework executor — Svelte, Astro, Angular, plain HTML. Carries the stack-independent contract; idiom comes from the stack corpus."
|
|
5
|
+
personas:
|
|
6
|
+
- frontend-engineer
|
|
7
|
+
domain: engineering
|
|
8
|
+
workspaces:
|
|
9
|
+
- engineering
|
|
10
|
+
packs:
|
|
11
|
+
- engineering-base
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ui-apply-generic
|
|
15
|
+
|
|
16
|
+
> The `plain` lane used to be a name with nothing behind it. This skill is the
|
|
17
|
+
> lane: it carries the implementation discipline that previously lived **only**
|
|
18
|
+
> inside the framework executors, and it gets framework idiom from the 16-stack
|
|
19
|
+
> corpus instead of from prose written here.
|
|
20
|
+
>
|
|
21
|
+
> That split is the whole point. One skill plus a corpus serves Svelte, Astro,
|
|
22
|
+
> Angular, Alpine, htmx and everything future at once. Sixteen framework skills
|
|
23
|
+
> would serve the same set and cost a single maintainer sixteen surfaces to keep
|
|
24
|
+
> current — the enumeration this package refuses.
|
|
25
|
+
|
|
26
|
+
## When to use
|
|
27
|
+
|
|
28
|
+
- `state.stack.frontend` resolves to `plain`, or to a label with no
|
|
29
|
+
framework-specific overlay.
|
|
30
|
+
- A stack whose `axes.reactivity` or `axes.meta` is recognised but has no
|
|
31
|
+
executor skill (`svelte`, `angular`, `astro`, `solid`, `qwik`, `htmx`).
|
|
32
|
+
- As the base of a composition: an overlay skill (`livewire`, `flux`,
|
|
33
|
+
`blade-ui`, `react-shadcn-ui`) adds framework specifics **on top** of this
|
|
34
|
+
contract, it does not replace it.
|
|
35
|
+
|
|
36
|
+
Do NOT use when a framework executor already covers the whole job — then this
|
|
37
|
+
skill is the base and the executor is the overlay, dispatched together.
|
|
38
|
+
|
|
39
|
+
## Procedure
|
|
40
|
+
|
|
41
|
+
### Step 1 — Query the stack corpus (mandatory)
|
|
42
|
+
|
|
43
|
+
Read `state.stack.axes` and query per resolved axis value, most specific first:
|
|
44
|
+
`meta` → `reactivity` → `component_lib`.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
<skills-root>/corpus-grounding/scripts/ground search \
|
|
48
|
+
--manifest <skills-root>/design-intelligence/data/manifest.json \
|
|
49
|
+
--stack <axis-value> "<the concrete question>"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`axes.meta: nuxt` with `axes.reactivity: vue` means **two** queries —
|
|
53
|
+
`--stack nuxtjs` and `--stack vue`. A meta-framework does not replace the layer
|
|
54
|
+
it wraps; that conflation is the defect the axes exist to fix.
|
|
55
|
+
|
|
56
|
+
**Cite what you used.** The result names the corpus rows the implementation
|
|
57
|
+
followed. An uncited pick is indistinguishable from the model's memory, and
|
|
58
|
+
memory is exactly what the corpus exists to replace.
|
|
59
|
+
|
|
60
|
+
**No corpus domain for the detected stack** → say so, in the result:
|
|
61
|
+
|
|
62
|
+
> No stack corpus for `<stack>` — proceeding on the generic contract. Framework
|
|
63
|
+
> idiom below is not grounded and should be reviewed by someone who knows this
|
|
64
|
+
> stack.
|
|
65
|
+
|
|
66
|
+
Silence is the failure mode. An unstated gap reads as grounded output.
|
|
67
|
+
|
|
68
|
+
### Step 2 — Apply the stack-independent contract
|
|
69
|
+
|
|
70
|
+
Every item is a hard requirement, and none of them is framework-specific:
|
|
71
|
+
|
|
72
|
+
1. **Verbatim floor.** Microcopy comes from `state.ui_design.microcopy` exactly
|
|
73
|
+
as written — every label, empty-state line, and validation message. No
|
|
74
|
+
paraphrase, no improvement. Where a source artifact was provided, it beats
|
|
75
|
+
the brief (see `design-fidelity`).
|
|
76
|
+
2. **Token discipline.** Colour, spacing, radius and shadow values come from the
|
|
77
|
+
project's tokens. A raw hex or arbitrary px that no token backs is a finding,
|
|
78
|
+
not a choice. Validate with `design-tokens`' `tokens validate`.
|
|
79
|
+
3. **Component reuse.** Check `state.ui_audit.components_found` before creating
|
|
80
|
+
anything. In a run that has already added components, re-read the component
|
|
81
|
+
directories — the audit inventory is computed once per state-file and does
|
|
82
|
+
not refresh (see `existing-ui-audit` § Gotchas).
|
|
83
|
+
4. **a11y floor.** Semantic elements over `div` + role; every interactive
|
|
84
|
+
element reachable and labelled; focus visible; contrast meets the project's
|
|
85
|
+
stated level, AA when unstated.
|
|
86
|
+
5. **All five states.** `empty`, `loading`, `error`, `success`, `disabled` per
|
|
87
|
+
the brief. An explicit `n/a` is legitimate for a surface that genuinely has
|
|
88
|
+
no such state — declaring that is the opposite of inventing filler.
|
|
89
|
+
6. **Asset discipline.** Copy referenced assets into the project's asset path.
|
|
90
|
+
Never hotlink; never fabricate a logo or a screenshot.
|
|
91
|
+
7. **No placeholders.** `<placeholder>`, `lorem`, `todo:`, `tbd`, `xxx` are
|
|
92
|
+
rejected at the boundary — including inside arrays.
|
|
93
|
+
|
|
94
|
+
### Step 3 — Verify, with honest degrade
|
|
95
|
+
|
|
96
|
+
Exercise the result with whatever the host actually has, and name which:
|
|
97
|
+
|
|
98
|
+
| Available | Do |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Browser / Playwright | Render, check the states, exercise one interaction |
|
|
101
|
+
| Screenshot only | Capture and inspect layout at 375 / 768 / 1280 |
|
|
102
|
+
| Neither | Static inspection only, and **say** the render is unverified |
|
|
103
|
+
|
|
104
|
+
Never claim a state or interaction works without having exercised it. A caveat
|
|
105
|
+
is a finding; a silent claim is a defect.
|
|
106
|
+
|
|
107
|
+
## Output format
|
|
108
|
+
|
|
109
|
+
Four requirements, in order — each is checkable by a reader of the result:
|
|
110
|
+
|
|
111
|
+
1. **Name the corpora queried**, or state that none exists for this stack.
|
|
112
|
+
2. **Cite the rows that changed a decision** — not every row read.
|
|
113
|
+
3. **State the verification method and its caveats**, never an unqualified
|
|
114
|
+
"works".
|
|
115
|
+
4. **List components reused before components added**, so a duplicate is
|
|
116
|
+
visible rather than buried.
|
|
117
|
+
|
|
118
|
+
Write `state.ticket["ui_apply"]`:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
ui_apply:
|
|
122
|
+
files: ["resources/js/UserCard.svelte"]
|
|
123
|
+
rendered:
|
|
124
|
+
"resources/js/UserCard.svelte": |
|
|
125
|
+
<full text, microcopy-locked>
|
|
126
|
+
components_added: ["UserCard"]
|
|
127
|
+
components_reused: ["Button"]
|
|
128
|
+
microcopy_lock: true
|
|
129
|
+
stack_corpus: # which corpora answered, or the degrade note
|
|
130
|
+
queried: ["svelte"]
|
|
131
|
+
cited: ["svelte.csv:12 — prefer stores over prop drilling"]
|
|
132
|
+
verify:
|
|
133
|
+
method: static_inspect # or playwright / screenshot
|
|
134
|
+
caveats: ["render not verified — no browser available"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Gotchas
|
|
138
|
+
|
|
139
|
+
- **Do not write framework prose into this skill.** The moment it explains how
|
|
140
|
+
Svelte stores work, it stops being one maintainable surface and starts being
|
|
141
|
+
sixteen. Framework knowledge belongs in the corpus.
|
|
142
|
+
- **`axes.reactivity: unknown` is not `none`.** `unknown` means a project has
|
|
143
|
+
manifests and no recognised reactivity layer — proceed, and say the stack was
|
|
144
|
+
not recognised. `none` means there genuinely is no reactivity layer, which is
|
|
145
|
+
the normal shape for a static page.
|
|
146
|
+
- **A meta-framework needs its own query.** Querying only `--stack vue` for a
|
|
147
|
+
Nuxt project silently drops the routing, data-fetching and SSR idioms that
|
|
148
|
+
are the whole reason Nuxt exists.
|
|
149
|
+
- **The generic contract is a floor, not a ceiling.** Where an overlay skill is
|
|
150
|
+
dispatched with this one, its framework rules win on their subject.
|
|
151
|
+
- **The floor now lives in exactly one place, and that is the point.** Overlay
|
|
152
|
+
composition applies this contract to every lane, so a future change to any of
|
|
153
|
+
its seven items is a one-point edit here rather than four lane patches that
|
|
154
|
+
drift apart. Concretely: the five-states requirement reaches every lane
|
|
155
|
+
through this skill. That requirement is deliberately unconditional — an
|
|
156
|
+
explicit `n/a` is the escape for a surface that genuinely has no such state
|
|
157
|
+
(asserted in `ui_lane_matrix.test.ts`), so this is not a defect being
|
|
158
|
+
broadened. But if the decision is ever revisited, revisit it here.
|
|
159
|
+
|
|
160
|
+
## Do NOT
|
|
161
|
+
|
|
162
|
+
- Emit framework code from memory and present it as grounded.
|
|
163
|
+
- Skip the corpus query because the stack "looks obvious".
|
|
164
|
+
- Claim a render or an interaction was verified when no primitive was available.
|
|
165
|
+
- Invent states, sections, or copy the brief does not carry.
|
|
166
|
+
- Treat a missing corpus as an evidence gap — it is a missing pack, and the two
|
|
167
|
+
must not be recorded the same way.
|
|
168
|
+
|
|
169
|
+
## See also
|
|
170
|
+
|
|
171
|
+
- [`design-intelligence`](../design-intelligence/SKILL.md) — the `--stack`
|
|
172
|
+
corpus this skill queries (16 stacks).
|
|
173
|
+
- [`existing-ui-audit`](../existing-ui-audit/SKILL.md) — the inventory Step 2
|
|
174
|
+
checks, and its once-per-state-file limit.
|
|
175
|
+
- [`design-tokens`](../design-tokens/SKILL.md) — `tokens validate`.
|
|
176
|
+
- [`fe-design`](../fe-design/SKILL.md) — where the brief's selection decisions
|
|
177
|
+
come from, and what it does when the corpus is not installed.
|
|
178
|
+
- [`ui-component-architect`](../ui-component-architect/SKILL.md),
|
|
179
|
+
[`tailwind-engineer`](../tailwind-engineer/SKILL.md) — stack-neutral
|
|
180
|
+
companions this skill composes with.
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "9.
|
|
42
|
+
agent_config_version: "9.12.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -17,14 +17,25 @@ import {
|
|
|
17
17
|
StepResult,
|
|
18
18
|
agent_directive,
|
|
19
19
|
} from '../../delivery_state.js';
|
|
20
|
-
import {
|
|
20
|
+
import { placeholder_paths } from './design.js';
|
|
21
|
+
import {
|
|
22
|
+
is_ambiguous_stack,
|
|
23
|
+
bundle_line,
|
|
24
|
+
unsupported_stack_questions,
|
|
25
|
+
} from './stack_bundles.js';
|
|
21
26
|
|
|
22
27
|
/** Map `state.stack.frontend` → agent-directive skill name. */
|
|
23
28
|
export const STACK_DIRECTIVES: Record<string, string> = {
|
|
24
29
|
'blade-livewire-flux': 'ui-apply-blade-livewire-flux',
|
|
30
|
+
'blade-livewire': 'ui-apply-blade-livewire',
|
|
31
|
+
filament: 'ui-apply-filament',
|
|
25
32
|
'react-shadcn': 'ui-apply-react-shadcn',
|
|
33
|
+
react: 'ui-apply-react',
|
|
26
34
|
vue: 'ui-apply-vue',
|
|
27
35
|
plain: 'ui-apply-plain',
|
|
36
|
+
// Present so the `keys == KNOWN_STACKS` invariant holds. Never emitted —
|
|
37
|
+
// the step intercepts this lane and refuses instead of dispatching.
|
|
38
|
+
unknown: 'ui-apply-unsupported',
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
/** Fallback directive when `state.stack` is missing or malformed. */
|
|
@@ -104,47 +115,40 @@ function _resolve_directive(state: DeliveryState): string {
|
|
|
104
115
|
return DEFAULT_DIRECTIVE;
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Return paths into `envelope['rendered']` whose text matches a pattern.
|
|
120
|
+
*
|
|
121
|
+
* Delegates to the walker shared with `design`, so the producer-side and
|
|
122
|
+
* consumer-side gates cannot drift apart. They previously held
|
|
123
|
+
* byte-identical copies of the same array-blind recursion, which meant the
|
|
124
|
+
* contract's "defense-in-depth" framing described two layers that failed on
|
|
125
|
+
* exactly the same inputs.
|
|
126
|
+
*/
|
|
108
127
|
function _placeholder_violations_in_output(envelope: Record<string, Any>): string[] {
|
|
109
|
-
|
|
110
|
-
if (!_isDict(rendered)) {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
const violations: string[] = [];
|
|
114
|
-
_walk_rendered(rendered, '', violations);
|
|
115
|
-
return violations;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function _walk_rendered(node: Record<string, Any>, prefix: string, violations: string[]): void {
|
|
119
|
-
for (const key of Object.keys(node)) {
|
|
120
|
-
const value = node[key];
|
|
121
|
-
const path = prefix ? `${prefix}.${key}` : String(key);
|
|
122
|
-
if (_isDict(value)) {
|
|
123
|
-
_walk_rendered(value, path, violations);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (typeof value !== 'string') {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const lowered = value.toLowerCase();
|
|
130
|
-
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
131
|
-
if (lowered.includes(pattern)) {
|
|
132
|
-
violations.push(path);
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
128
|
+
return placeholder_paths(envelope['rendered']);
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
/** First-pass halt — emit the stack-specific apply directive. */
|
|
140
132
|
function _delegate_to_stack_skill(state: DeliveryState): StepResult {
|
|
141
133
|
const directive = _resolve_directive(state);
|
|
142
134
|
const stack_label = _stack_label(state);
|
|
135
|
+
if (is_ambiguous_stack(state.stack)) {
|
|
136
|
+
const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
|
|
137
|
+
[]) as ReadonlyArray<string>;
|
|
138
|
+
return new StepResult({
|
|
139
|
+
outcome: Outcome.BLOCKED,
|
|
140
|
+
questions: unsupported_stack_questions('apply', conflicts),
|
|
141
|
+
message:
|
|
142
|
+
`UI apply halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
|
|
143
|
+
'the open question is which project to build for.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
143
146
|
return new StepResult({
|
|
144
147
|
outcome: Outcome.BLOCKED,
|
|
145
148
|
questions: [
|
|
146
149
|
agent_directive(directive),
|
|
147
150
|
`> Stack: \`${stack_label}\`. Implementing the locked design brief.`,
|
|
151
|
+
bundle_line(state.stack, 'build', stack_label),
|
|
148
152
|
'> Microcopy is locked — every button label, empty-state ' +
|
|
149
153
|
'message, and validation message must come verbatim from ' +
|
|
150
154
|
'`state.ui_design.microcopy`.',
|
|
@@ -146,7 +146,17 @@ function _missing_required_keys(design: Record<string, Any>): string[] {
|
|
|
146
146
|
missing.push(key);
|
|
147
147
|
continue;
|
|
148
148
|
}
|
|
149
|
-
if (key === 'states'
|
|
149
|
+
if (key === 'states') {
|
|
150
|
+
// A non-object `states` (a string, a list) used to skip the
|
|
151
|
+
// per-state loop entirely, so `states: "n/a"` satisfied the gate
|
|
152
|
+
// without covering a single state. Report every required key
|
|
153
|
+
// instead — the author gets the same actionable list either way.
|
|
154
|
+
if (!_isDict(value)) {
|
|
155
|
+
for (const state_key of REQUIRED_STATE_KEYS) {
|
|
156
|
+
missing.push(`states.${state_key}`);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
150
160
|
for (const state_key of REQUIRED_STATE_KEYS) {
|
|
151
161
|
if (!_pyTruthy(value[state_key])) {
|
|
152
162
|
missing.push(`states.${state_key}`);
|
|
@@ -157,37 +167,74 @@ function _missing_required_keys(design: Record<string, Any>): string[] {
|
|
|
157
167
|
return missing;
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Return brief paths whose values match a placeholder pattern.
|
|
172
|
+
*
|
|
173
|
+
* Covers `microcopy` **and** `states`. The five required states were checked
|
|
174
|
+
* for truthiness only, so `states.error: "TBD"` satisfied the gate — the brief
|
|
175
|
+
* looked complete while covering nothing. Both slots carry strings the user
|
|
176
|
+
* signs off on, so both are held to the same bar.
|
|
177
|
+
*
|
|
178
|
+
* An explicit `"n/a"` stays legitimate: a static landing page genuinely has no
|
|
179
|
+
* error or disabled state, and *declaring* that is the opposite of inventing
|
|
180
|
+
* filler. That is why the gate demands all five keys rather than branching on
|
|
181
|
+
* page type — the author states the answer instead of the engine guessing which
|
|
182
|
+
* states a surface ought to have.
|
|
183
|
+
*/
|
|
161
184
|
function _placeholder_violations(design: Record<string, Any>): string[] {
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
return [
|
|
186
|
+
...placeholder_paths(design['microcopy'], 'microcopy'),
|
|
187
|
+
...placeholder_paths(design['states'], 'states'),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return every path under `node` whose leaf string carries a placeholder.
|
|
193
|
+
*
|
|
194
|
+
* Traverses objects **and arrays**. The array arm is load-bearing, not
|
|
195
|
+
* defensive: a list is the natural shape for the most common microcopy
|
|
196
|
+
* (`nav_items`, `menu`, `steps`) and for a rendered file split into lines,
|
|
197
|
+
* and a walker that recursed into objects only let
|
|
198
|
+
* `{ nav_items: ["Home", "TODO: Link"] }` through both the brief lock and
|
|
199
|
+
* the rendered-output gate.
|
|
200
|
+
*
|
|
201
|
+
* Array elements are addressed `key[i]` so the halt names the exact element
|
|
202
|
+
* the author has to fix, not just the list.
|
|
203
|
+
*
|
|
204
|
+
* Shared by `design` (producer side, on `microcopy`) and `apply` (consumer
|
|
205
|
+
* side, on the rendered envelope). One implementation on purpose: the two
|
|
206
|
+
* call sites previously carried byte-identical copies of the same bug, so
|
|
207
|
+
* duplicating them bought no independence — see
|
|
208
|
+
* `docs/contracts/ui-track-flow.md` § implement → apply.
|
|
209
|
+
*/
|
|
210
|
+
export function placeholder_paths(node: Any, prefix = ''): string[] {
|
|
164
211
|
const violations: string[] = [];
|
|
165
|
-
|
|
212
|
+
_walk_placeholders(node, prefix, violations);
|
|
166
213
|
return violations;
|
|
167
214
|
}
|
|
168
215
|
|
|
169
|
-
function
|
|
170
|
-
node
|
|
171
|
-
|
|
172
|
-
violations: string[],
|
|
173
|
-
): void {
|
|
174
|
-
for (const key of Object.keys(node)) {
|
|
175
|
-
const value = node[key];
|
|
176
|
-
const path = prefix ? `${prefix}.${key}` : String(key);
|
|
177
|
-
if (_isDict(value)) {
|
|
178
|
-
_walk_microcopy(value, path, violations);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (typeof value !== 'string') {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const lowered = value.toLowerCase();
|
|
216
|
+
function _walk_placeholders(node: Any, prefix: string, violations: string[]): void {
|
|
217
|
+
if (typeof node === 'string') {
|
|
218
|
+
const lowered = node.toLowerCase();
|
|
185
219
|
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
186
220
|
if (lowered.includes(pattern)) {
|
|
187
|
-
violations.push(
|
|
188
|
-
|
|
221
|
+
violations.push(prefix);
|
|
222
|
+
return;
|
|
189
223
|
}
|
|
190
224
|
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(node)) {
|
|
228
|
+
node.forEach((value, index) => {
|
|
229
|
+
_walk_placeholders(value, `${prefix}[${index}]`, violations);
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (_isDict(node)) {
|
|
234
|
+
for (const key of Object.keys(node)) {
|
|
235
|
+
const path = prefix ? `${prefix}.${key}` : String(key);
|
|
236
|
+
_walk_placeholders(node[key], path, violations);
|
|
237
|
+
}
|
|
191
238
|
}
|
|
192
239
|
}
|
|
193
240
|
|
|
@@ -311,15 +358,23 @@ function _halt_unconfirmed(state: DeliveryState, design: Record<string, Any>): S
|
|
|
311
358
|
});
|
|
312
359
|
}
|
|
313
360
|
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Count leaf string entries in `microcopy` (recursive, arrays included).
|
|
363
|
+
*
|
|
364
|
+
* The count is shown in the sign-off the user confirms, so it has to agree
|
|
365
|
+
* with what the placeholder walker inspects — an object-only count reported
|
|
366
|
+
* fewer locked strings than were actually locked.
|
|
367
|
+
*/
|
|
368
|
+
function _count_microcopy(microcopy: Any): number {
|
|
369
|
+
if (typeof microcopy === 'string') return 1;
|
|
370
|
+
if (Array.isArray(microcopy)) {
|
|
371
|
+
return microcopy.reduce((total: number, value) => total + _count_microcopy(value), 0);
|
|
372
|
+
}
|
|
373
|
+
if (_isDict(microcopy)) {
|
|
374
|
+
return Object.values(microcopy).reduce(
|
|
375
|
+
(total: number, value) => total + _count_microcopy(value),
|
|
376
|
+
0,
|
|
377
|
+
);
|
|
323
378
|
}
|
|
324
|
-
return
|
|
379
|
+
return 0;
|
|
325
380
|
}
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
StepResult,
|
|
16
16
|
agent_directive,
|
|
17
17
|
} from '../../delivery_state.js';
|
|
18
|
+
import {
|
|
19
|
+
is_ambiguous_stack,
|
|
20
|
+
bundle_line,
|
|
21
|
+
unsupported_stack_questions,
|
|
22
|
+
} from './stack_bundles.js';
|
|
18
23
|
|
|
19
24
|
/** Maximum number of polish rounds per `/work` run. */
|
|
20
25
|
export const POLISH_CEILING = 2;
|
|
@@ -31,9 +36,15 @@ export const TOKEN_REPEAT_THRESHOLD = 2;
|
|
|
31
36
|
/** Map `state.stack.frontend` → agent-directive skill name. */
|
|
32
37
|
export const STACK_DIRECTIVES: Record<string, string> = {
|
|
33
38
|
'blade-livewire-flux': 'ui-polish-blade-livewire-flux',
|
|
39
|
+
'blade-livewire': 'ui-polish-blade-livewire',
|
|
40
|
+
filament: 'ui-polish-filament',
|
|
34
41
|
'react-shadcn': 'ui-polish-react-shadcn',
|
|
42
|
+
react: 'ui-polish-react',
|
|
35
43
|
vue: 'ui-polish-vue',
|
|
36
44
|
plain: 'ui-polish-plain',
|
|
45
|
+
// Present so the `keys == KNOWN_STACKS` invariant holds. Never emitted —
|
|
46
|
+
// the step intercepts this lane and refuses instead of dispatching.
|
|
47
|
+
unknown: 'ui-polish-unsupported',
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
/** Fallback directive when `state.stack` is missing or malformed. */
|
|
@@ -184,6 +195,17 @@ function _delegate_to_polish_skill(
|
|
|
184
195
|
ceiling: number,
|
|
185
196
|
): StepResult {
|
|
186
197
|
const directive = _resolve_directive(state);
|
|
198
|
+
if (is_ambiguous_stack(state.stack)) {
|
|
199
|
+
const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
|
|
200
|
+
[]) as ReadonlyArray<string>;
|
|
201
|
+
return new StepResult({
|
|
202
|
+
outcome: Outcome.BLOCKED,
|
|
203
|
+
questions: unsupported_stack_questions('polish', conflicts),
|
|
204
|
+
message:
|
|
205
|
+
`UI polish halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
|
|
206
|
+
'the open question is which project to build for.',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
187
209
|
const stack_label = _stack_label(state);
|
|
188
210
|
const next_round = rounds + 1;
|
|
189
211
|
let findings_line =
|
|
@@ -203,6 +225,7 @@ function _delegate_to_polish_skill(
|
|
|
203
225
|
questions: [
|
|
204
226
|
agent_directive(directive),
|
|
205
227
|
`> Stack: \`${stack_label}\`. Polish round ${next_round} of ${ceiling}.`,
|
|
228
|
+
bundle_line(state.stack, 'build', stack_label),
|
|
206
229
|
findings_line,
|
|
207
230
|
'> Fix chart-type / contrast findings against the adopted ' +
|
|
208
231
|
"corpus rows (design-intelligence § 'Grounding the " +
|
|
@@ -16,13 +16,24 @@ import {
|
|
|
16
16
|
StepResult,
|
|
17
17
|
agent_directive,
|
|
18
18
|
} from '../../delivery_state.js';
|
|
19
|
+
import {
|
|
20
|
+
is_ambiguous_stack,
|
|
21
|
+
bundle_line,
|
|
22
|
+
unsupported_stack_questions,
|
|
23
|
+
} from './stack_bundles.js';
|
|
19
24
|
|
|
20
|
-
/** Map `state.stack.frontend` → agent-directive
|
|
25
|
+
/** Map `state.stack.frontend` → agent-directive verb. */
|
|
21
26
|
export const STACK_DIRECTIVES: Record<string, string> = {
|
|
22
27
|
'blade-livewire-flux': 'ui-design-review-blade-livewire-flux',
|
|
28
|
+
'blade-livewire': 'ui-design-review-blade-livewire',
|
|
29
|
+
filament: 'ui-design-review-filament',
|
|
23
30
|
'react-shadcn': 'ui-design-review-react-shadcn',
|
|
31
|
+
react: 'ui-design-review-react',
|
|
24
32
|
vue: 'ui-design-review-vue',
|
|
25
33
|
plain: 'ui-design-review-plain',
|
|
34
|
+
// Present so the `keys == KNOWN_STACKS` invariant holds. Never emitted —
|
|
35
|
+
// the step intercepts this lane and refuses instead of dispatching.
|
|
36
|
+
unknown: 'ui-design-review-unsupported',
|
|
26
37
|
};
|
|
27
38
|
|
|
28
39
|
/** Fallback directive when `state.stack` is missing or malformed. */
|
|
@@ -181,12 +192,24 @@ function _stack_label(state: DeliveryState): string {
|
|
|
181
192
|
function _delegate_to_review_skill(state: DeliveryState): StepResult {
|
|
182
193
|
const directive = _resolve_directive(state);
|
|
183
194
|
const stack_label = _stack_label(state);
|
|
195
|
+
if (is_ambiguous_stack(state.stack)) {
|
|
196
|
+
const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
|
|
197
|
+
[]) as ReadonlyArray<string>;
|
|
198
|
+
return new StepResult({
|
|
199
|
+
outcome: Outcome.BLOCKED,
|
|
200
|
+
questions: unsupported_stack_questions('review', conflicts),
|
|
201
|
+
message:
|
|
202
|
+
`UI review halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
|
|
203
|
+
'the open question is which project to build for.',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
184
206
|
return new StepResult({
|
|
185
207
|
outcome: Outcome.BLOCKED,
|
|
186
208
|
questions: [
|
|
187
209
|
agent_directive(directive),
|
|
188
210
|
`> Stack: \`${stack_label}\`. Reviewing rendered components ` +
|
|
189
211
|
'against the locked design brief.',
|
|
212
|
+
bundle_line(state.stack, 'review', stack_label),
|
|
190
213
|
'> The review pass compares `state.ticket.ui_apply.rendered` ' +
|
|
191
214
|
'against `state.ui_design` (microcopy, states, a11y, layout) ' +
|
|
192
215
|
'and produces a structured `findings` list.',
|