@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.
Files changed (142) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +111 -0
  3. package/README.md +20 -16
  4. package/agents/templates/.ai-council.yml.example +481 -0
  5. package/agents/templates/.ai-video.xml.example +247 -0
  6. package/dist/agent-src/commands/fix/ci.md +29 -0
  7. package/dist/agent-src/commands/pr/create.md +60 -0
  8. package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
  9. package/dist/agent-src/rules/design-fidelity.md +2 -2
  10. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
  11. package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
  12. package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
  13. package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
  14. package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
  15. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -2
  16. package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
  17. package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
  18. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
  19. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
  24. package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
  25. package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
  26. package/dist/agent-src/templates/scripts/work_engine/state.ts +21 -4
  27. package/dist/cli/initRouting.js +74 -11
  28. package/dist/cli/initRouting.js.map +1 -1
  29. package/dist/cli/main.js +14 -3
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  32. package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
  33. package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
  34. package/dist/cli-delegate/chunk-A3HJ6NTI.js +1084 -0
  35. package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
  36. package/dist/cli-delegate/chunk-FTAISGDQ.js +117 -0
  37. package/dist/cli-delegate/chunk-GME3Q44L.js +82 -0
  38. package/dist/cli-delegate/chunk-GRRPEPRN.js +336 -0
  39. package/dist/cli-delegate/chunk-HJ6DAJHR.js +502 -0
  40. package/dist/cli-delegate/chunk-HYWSRNUX.js +179 -0
  41. package/dist/cli-delegate/chunk-J55WI3RR.js +47 -0
  42. package/dist/cli-delegate/chunk-MCJYCQPF.js +35 -0
  43. package/dist/cli-delegate/chunk-NDJLNC6Q.js +169 -0
  44. package/dist/cli-delegate/chunk-OMIVVCG2.js +26 -0
  45. package/dist/cli-delegate/chunk-OY53J6II.js +7336 -0
  46. package/dist/cli-delegate/chunk-QFKQLQ7X.js +10835 -0
  47. package/dist/cli-delegate/chunk-TEGJU2JX.js +6908 -0
  48. package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
  49. package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
  50. package/dist/cli-delegate/cmd_analyze_session.js +294 -0
  51. package/dist/cli-delegate/cmd_conformance.js +643 -0
  52. package/dist/cli-delegate/cmd_converge.js +326 -0
  53. package/dist/cli-delegate/cmd_doctor.js +142 -0
  54. package/dist/cli-delegate/cmd_explain.js +2576 -0
  55. package/dist/cli-delegate/cmd_export.js +298 -0
  56. package/dist/cli-delegate/cmd_fleet.js +333 -0
  57. package/dist/cli-delegate/cmd_memory_get.js +3878 -0
  58. package/dist/cli-delegate/cmd_migrate.js +11 -0
  59. package/dist/cli-delegate/cmd_preflight.js +153 -0
  60. package/dist/cli-delegate/cmd_prune.js +446 -0
  61. package/dist/cli-delegate/cmd_refresh.js +28 -0
  62. package/dist/cli-delegate/cmd_settings_check.js +574 -0
  63. package/dist/cli-delegate/cmd_settings_migrate.js +225 -0
  64. package/dist/cli-delegate/cmd_sync.js +266 -0
  65. package/dist/cli-delegate/cmd_uninstall.js +657 -0
  66. package/dist/cli-delegate/cmd_update.js +425 -0
  67. package/dist/cli-delegate/cmd_upgrade.js +444 -0
  68. package/dist/cli-delegate/cmd_validate.js +307 -0
  69. package/dist/cli-delegate/cmd_versions.js +370 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +45 -16
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +3 -3
  74. package/dist/discovery/orphan-report.md +1 -1
  75. package/dist/discovery/packs.json +13 -7
  76. package/dist/discovery/trust-report.md +2 -2
  77. package/dist/discovery/workspaces.json +6 -4
  78. package/dist/install/emit_host_rules_cli.js +2 -1
  79. package/dist/install/emit_host_rules_cli.js.map +1 -1
  80. package/dist/install/install.mjs +634 -409
  81. package/dist/install/ruleInScope.js +123 -0
  82. package/dist/install/ruleInScope.js.map +1 -0
  83. package/dist/install/rule_scope.js +7 -3
  84. package/dist/install/rule_scope.js.map +1 -1
  85. package/dist/install/toolDetection.js +40 -12
  86. package/dist/install/toolDetection.js.map +1 -1
  87. package/dist/install/wizard-plan.js +12 -3
  88. package/dist/install/wizard-plan.js.map +1 -1
  89. package/dist/mcp/registry-manifest.json +3 -3
  90. package/dist/mcp/server.json +2 -2
  91. package/dist/mcp/server.mjs +1553 -2728
  92. package/dist/router.json +1 -1
  93. package/dist/scripts/condense.js +6 -22
  94. package/dist/scripts/condense.js.map +1 -1
  95. package/docs/guidelines/design-fidelity-mechanics.md +14 -0
  96. package/package.json +4 -2
  97. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -30
  98. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
  99. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +23 -0
  100. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
  101. package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
  102. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
  103. package/src/agent-src/templates/scripts/work_engine/state.ts +21 -4
  104. package/src/config/discovery/packs.yml +7 -0
  105. package/src/config/evaluator-budgets.json +1 -1
  106. package/src/config/pack-size-budget.json +27 -0
  107. package/src/install/emit_host_rules_cli.ts +2 -1
  108. package/src/install/ruleInScope.ts +133 -0
  109. package/src/install/rule_scope.ts +7 -3
  110. package/src/install/toolDetection.ts +39 -8
  111. package/src/install/wizard-plan.ts +13 -3
  112. package/src/scripts/_cli/cmd_conformance.ts +2 -1
  113. package/src/scripts/_cli/cmd_converge.ts +2 -1
  114. package/src/scripts/_cli/cmd_doctor.ts +214 -13
  115. package/src/scripts/_cli/cmd_export.ts +2 -1
  116. package/src/scripts/_cli/cmd_fleet.ts +2 -1
  117. package/src/scripts/_cli/cmd_migrate.ts +2 -1
  118. package/src/scripts/_cli/cmd_preflight.ts +2 -1
  119. package/src/scripts/_cli/cmd_refresh.ts +2 -1
  120. package/src/scripts/_cli/cmd_update.ts +2 -1
  121. package/src/scripts/_cli/cmd_upgrade.ts +2 -1
  122. package/src/scripts/_cli/cmd_versions.ts +2 -1
  123. package/src/scripts/_cli/detection_report.ts +480 -0
  124. package/src/scripts/_dispatch.bash +52 -14
  125. package/src/scripts/_lib/changelog_eras.ts +103 -9
  126. package/src/scripts/_lib/environment_detector.ts +441 -0
  127. package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
  128. package/src/scripts/_lib/package_root.ts +72 -0
  129. package/src/scripts/ai_council/config.ts +15 -7
  130. package/src/scripts/ai_council/modes.ts +68 -2
  131. package/src/scripts/ai_council/transport_resolver.ts +331 -0
  132. package/src/scripts/check_bundle_path_leakage.ts +10 -4
  133. package/src/scripts/check_dependency_floors.ts +153 -0
  134. package/src/scripts/check_installer_import_purity.ts +295 -0
  135. package/src/scripts/check_pack_size.ts +188 -0
  136. package/src/scripts/condense.ts +7 -28
  137. package/src/scripts/council_cli.ts +6 -2
  138. package/src/scripts/evaluator_umbrella.sh +34 -1
  139. package/src/scripts/install-hooks.sh +36 -4
  140. package/src/scripts/install.ts +210 -5
  141. package/src/scripts/lint_eval_fixture_citations.ts +113 -0
  142. 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); run
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. **CSS/Tailwind import line** — the `@import url(…)` from the CSV's
103
- `CSS Import` column, and the Tailwind `fontFamily` config snippet from
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.** `font-pairings-reference.csv`
109
- is Reference data on a freshness contract — never substitute a live Google
110
- Fonts API call. If a font was retired or renamed, the `@import` URL 404s at
111
- build time and the browser silently falls back to the system font, shipping
112
- broken visual design with no obvious error. Always verify the URL resolves
113
- (Step 5) before emitting the import line; if it 404s, surface the error and
114
- ask the user to pick an alternative pairing from the CSV.
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.10.0"
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 { PLACEHOLDER_PATTERNS } from './design.js';
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
- /** Return paths into `envelope['rendered']` whose text matches a pattern. */
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
- const rendered = envelope['rendered'];
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' && _isDict(value)) {
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
- /** Return microcopy paths whose values match a placeholder pattern. */
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
- const microcopy = design['microcopy'];
163
- if (!_isDict(microcopy)) return [];
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
- _walk_microcopy(microcopy, '', violations);
212
+ _walk_placeholders(node, prefix, violations);
166
213
  return violations;
167
214
  }
168
215
 
169
- function _walk_microcopy(
170
- node: Record<string, Any>,
171
- prefix: string,
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(path);
188
- break;
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
- /** Count leaf string entries in `microcopy` (recursive). */
315
- function _count_microcopy(microcopy: Record<string, Any>): number {
316
- let total = 0;
317
- for (const value of Object.values(microcopy)) {
318
- if (_isDict(value)) {
319
- total += _count_microcopy(value);
320
- } else if (typeof value === 'string') {
321
- total += 1;
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 total;
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 skill name. */
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.',