@event4u/agent-config 9.10.0 → 9.11.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 (121) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +59 -0
  3. package/README.md +12 -15
  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/rules/design-fidelity.md +1 -1
  7. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -2
  8. package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
  9. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  10. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +30 -30
  11. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
  12. package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +20 -0
  13. package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +20 -1
  14. package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +179 -0
  15. package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +155 -4
  16. package/dist/agent-src/templates/scripts/work_engine/state.ts +21 -4
  17. package/dist/cli/initRouting.js +74 -11
  18. package/dist/cli/initRouting.js.map +1 -1
  19. package/dist/cli/main.js +14 -3
  20. package/dist/cli/main.js.map +1 -1
  21. package/dist/cli-delegate/chunk-4SV3XGFA.js +6910 -0
  22. package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
  23. package/dist/cli-delegate/chunk-7DSAMZFU.js +356 -0
  24. package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
  25. package/dist/cli-delegate/chunk-BJGXZNPM.js +266 -0
  26. package/dist/cli-delegate/chunk-FTAISGDQ.js +117 -0
  27. package/dist/cli-delegate/chunk-GME3Q44L.js +82 -0
  28. package/dist/cli-delegate/chunk-GRRPEPRN.js +336 -0
  29. package/dist/cli-delegate/chunk-HJ6DAJHR.js +502 -0
  30. package/dist/cli-delegate/chunk-IR7P7JWA.js +872 -0
  31. package/dist/cli-delegate/chunk-J55WI3RR.js +47 -0
  32. package/dist/cli-delegate/chunk-MCJYCQPF.js +35 -0
  33. package/dist/cli-delegate/chunk-NDJLNC6Q.js +169 -0
  34. package/dist/cli-delegate/chunk-OMIVVCG2.js +26 -0
  35. package/dist/cli-delegate/chunk-OY53J6II.js +7336 -0
  36. package/dist/cli-delegate/chunk-RDGRHAAB.js +259 -0
  37. package/dist/cli-delegate/chunk-TFG57JJM.js +1969 -0
  38. package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
  39. package/dist/cli-delegate/chunk-YMFO6FDN.js +10603 -0
  40. package/dist/cli-delegate/cmd_analyze_session.js +294 -0
  41. package/dist/cli-delegate/cmd_conformance.js +646 -0
  42. package/dist/cli-delegate/cmd_converge.js +326 -0
  43. package/dist/cli-delegate/cmd_doctor.js +144 -0
  44. package/dist/cli-delegate/cmd_explain.js +2508 -0
  45. package/dist/cli-delegate/cmd_export.js +296 -0
  46. package/dist/cli-delegate/cmd_fleet.js +332 -0
  47. package/dist/cli-delegate/cmd_memory_get.js +3878 -0
  48. package/dist/cli-delegate/cmd_migrate.js +11 -0
  49. package/dist/cli-delegate/cmd_preflight.js +270 -0
  50. package/dist/cli-delegate/cmd_prune.js +444 -0
  51. package/dist/cli-delegate/cmd_refresh.js +28 -0
  52. package/dist/cli-delegate/cmd_settings_check.js +574 -0
  53. package/dist/cli-delegate/cmd_settings_migrate.js +223 -0
  54. package/dist/cli-delegate/cmd_sync.js +264 -0
  55. package/dist/cli-delegate/cmd_uninstall.js +656 -0
  56. package/dist/cli-delegate/cmd_update.js +425 -0
  57. package/dist/cli-delegate/cmd_upgrade.js +444 -0
  58. package/dist/cli-delegate/cmd_validate.js +305 -0
  59. package/dist/cli-delegate/cmd_versions.js +370 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +14 -7
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +2 -2
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +12 -7
  66. package/dist/discovery/trust-report.md +1 -1
  67. package/dist/discovery/workspaces.json +4 -3
  68. package/dist/install/emit_host_rules_cli.js +2 -1
  69. package/dist/install/emit_host_rules_cli.js.map +1 -1
  70. package/dist/install/install.mjs +410 -378
  71. package/dist/install/rule_scope.js +5 -1
  72. package/dist/install/rule_scope.js.map +1 -1
  73. package/dist/install/toolDetection.js +40 -12
  74. package/dist/install/toolDetection.js.map +1 -1
  75. package/dist/install/wizard-plan.js +2 -2
  76. package/dist/install/wizard-plan.js.map +1 -1
  77. package/dist/mcp/registry-manifest.json +1 -1
  78. package/dist/mcp/server.json +2 -2
  79. package/dist/mcp/server.mjs +1614 -765
  80. package/dist/router.json +1 -1
  81. package/package.json +4 -2
  82. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +30 -30
  83. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
  84. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +20 -0
  85. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +20 -1
  86. package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +179 -0
  87. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +155 -4
  88. package/src/agent-src/templates/scripts/work_engine/state.ts +21 -4
  89. package/src/config/discovery/packs.yml +7 -0
  90. package/src/config/evaluator-budgets.json +1 -1
  91. package/src/config/pack-size-budget.json +27 -0
  92. package/src/install/emit_host_rules_cli.ts +2 -1
  93. package/src/install/rule_scope.ts +5 -1
  94. package/src/install/toolDetection.ts +39 -8
  95. package/src/install/wizard-plan.ts +2 -2
  96. package/src/scripts/_cli/cmd_conformance.ts +2 -1
  97. package/src/scripts/_cli/cmd_converge.ts +2 -1
  98. package/src/scripts/_cli/cmd_doctor.ts +214 -13
  99. package/src/scripts/_cli/cmd_export.ts +2 -1
  100. package/src/scripts/_cli/cmd_fleet.ts +2 -1
  101. package/src/scripts/_cli/cmd_migrate.ts +2 -1
  102. package/src/scripts/_cli/cmd_preflight.ts +2 -1
  103. package/src/scripts/_cli/cmd_refresh.ts +2 -1
  104. package/src/scripts/_cli/cmd_update.ts +2 -1
  105. package/src/scripts/_cli/cmd_upgrade.ts +2 -1
  106. package/src/scripts/_cli/cmd_versions.ts +2 -1
  107. package/src/scripts/_cli/detection_report.ts +480 -0
  108. package/src/scripts/_dispatch.bash +52 -14
  109. package/src/scripts/_lib/changelog_eras.ts +103 -9
  110. package/src/scripts/_lib/environment_detector.ts +441 -0
  111. package/src/scripts/_lib/package_root.ts +72 -0
  112. package/src/scripts/ai_council/config.ts +15 -7
  113. package/src/scripts/ai_council/modes.ts +68 -2
  114. package/src/scripts/ai_council/transport_resolver.ts +331 -0
  115. package/src/scripts/check_bundle_path_leakage.ts +10 -4
  116. package/src/scripts/check_dependency_floors.ts +153 -0
  117. package/src/scripts/check_pack_size.ts +188 -0
  118. package/src/scripts/council_cli.ts +6 -2
  119. package/src/scripts/evaluator_umbrella.sh +34 -1
  120. package/src/scripts/lint_eval_fixture_citations.ts +113 -0
  121. package/src/scripts/lint_ui_stack_bundles.ts +170 -0
@@ -0,0 +1,247 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ agents/.ai-video.xml.example
4
+ ============================
5
+
6
+ Operator-facing provider configuration for the /video:* command cluster.
7
+
8
+ Copy this file to `agents/.ai-video.xml` and fill in the API keys for the
9
+ providers you actually use. The real file is gitignored and MUST NEVER be
10
+ committed.
11
+
12
+ Loader: scripts/media/lib/load-config.sh
13
+ Redaction: scripts/media/lib/redact.sh
14
+ Docs: agents/reference/ai-video/README.md
15
+ -->
16
+ <ai-video version="1">
17
+
18
+ <!--
19
+ Defaults: pick which provider a /video:* run uses when the command
20
+ does not pass the image-provider / video-provider flags. Must match
21
+ a provider id="..." declared below.
22
+
23
+ Each provider carries a <lifecycle> tag — experimental | stable |
24
+ deprecated | community — per docs/contracts/provider-lifecycle.md.
25
+ Day-one shipped tiers are conservative: every adapter is
26
+ `experimental` until a maintainer captures a real-API smoke trace.
27
+ The agent refuses to *default* to a non-stable provider silently;
28
+ it surfaces the tier and asks before running.
29
+ -->
30
+ <default-image-provider>openai-images</default-image-provider>
31
+ <default-video-provider>gemini-veo</default-video-provider>
32
+
33
+ <!-- ============================================================ -->
34
+ <!-- Image providers -->
35
+ <!-- ============================================================ -->
36
+
37
+ <provider id="openai-images" kind="image">
38
+ <lifecycle>stable</lifecycle>
39
+ <api-key>sk-REPLACE-ME</api-key>
40
+ <endpoint>https://api.openai.com/v1</endpoint>
41
+ <default-model>gpt-image-1</default-model>
42
+ <dry-run>true</dry-run>
43
+ <tuning>
44
+ <aspect>16:9</aspect>
45
+ <quality>high</quality>
46
+ <best-of-n>4</best-of-n>
47
+ </tuning>
48
+ </provider>
49
+
50
+ <!-- ============================================================ -->
51
+ <!-- Video providers -->
52
+ <!-- ============================================================ -->
53
+
54
+ <provider id="higgsfield" kind="image+video">
55
+ <lifecycle>stable</lifecycle>
56
+ <api-key>hf-REPLACE-ME</api-key>
57
+ <endpoint>https://api.higgsfield.ai/v1</endpoint>
58
+ <default-model>higgsfield-soul</default-model>
59
+ <dry-run>true</dry-run>
60
+ <tuning>
61
+ <aspect>16:9</aspect>
62
+ <preset>cinematic</preset>
63
+ <max-duration>5</max-duration>
64
+ <best-of-n>2</best-of-n>
65
+ </tuning>
66
+ </provider>
67
+
68
+ <provider id="gemini-veo" kind="video">
69
+ <lifecycle>stable</lifecycle>
70
+ <api-key>AIza-REPLACE-ME</api-key>
71
+ <endpoint>https://generativelanguage.googleapis.com/v1beta</endpoint>
72
+ <default-model>veo-3.0-generate-001</default-model>
73
+ <dry-run>true</dry-run>
74
+ <tuning>
75
+ <aspect>16:9</aspect>
76
+ <fps>24</fps>
77
+ <max-duration>8</max-duration>
78
+ <audio-native>true</audio-native>
79
+ </tuning>
80
+ </provider>
81
+
82
+ <provider id="kling" kind="video">
83
+ <lifecycle>stable</lifecycle>
84
+ <access-key>kling-access-REPLACE-ME</access-key>
85
+ <secret-key>kling-secret-REPLACE-ME</secret-key>
86
+ <endpoint>https://api.klingai.com/v1</endpoint>
87
+ <default-model>kling-v2-master</default-model>
88
+ <dry-run>true</dry-run>
89
+ <tuning>
90
+ <aspect>16:9</aspect>
91
+ <fps>30</fps>
92
+ <max-duration>10</max-duration>
93
+ <audio-native>false</audio-native>
94
+ </tuning>
95
+ </provider>
96
+
97
+ <provider id="sora" kind="video">
98
+ <lifecycle>stable</lifecycle>
99
+ <api-key>sora-REPLACE-ME</api-key>
100
+ <endpoint>https://api.openai.com/v1</endpoint>
101
+ <default-model>sora-2</default-model>
102
+ <dry-run>true</dry-run>
103
+ <tuning>
104
+ <aspect>16:9</aspect>
105
+ <fps>30</fps>
106
+ <max-duration>20</max-duration>
107
+ <audio-native>true</audio-native>
108
+ </tuning>
109
+ </provider>
110
+
111
+ <!-- ============================================================ -->
112
+ <!-- Multiplexer providers — one adapter, many models -->
113
+ <!-- Model selection: top-level `model_id` stdin key overrides -->
114
+ <!-- <default-model>. Per-model capabilities: -->
115
+ <!-- scripts/ai-video/lib/model-capabilities/<id>.json -->
116
+ <!-- Kill-switch: <enabled>false</enabled> refuses every network -->
117
+ <!-- subcommand without editing procedures that name the provider.-->
118
+ <!-- ============================================================ -->
119
+
120
+ <provider id="fal" kind="video">
121
+ <lifecycle>stable</lifecycle>
122
+ <enabled>true</enabled>
123
+ <api-key>fal-REPLACE-ME</api-key>
124
+ <endpoint>https://queue.fal.run</endpoint>
125
+ <default-model>fal-ai/ltx-2/text-to-video</default-model>
126
+ <dry-run>true</dry-run>
127
+ <tuning>
128
+ <aspect>16:9</aspect>
129
+ <max-duration>10</max-duration>
130
+ <audio-native>false</audio-native>
131
+ </tuning>
132
+ </provider>
133
+
134
+ <provider id="replicate" kind="video">
135
+ <lifecycle>stable</lifecycle>
136
+ <enabled>true</enabled>
137
+ <api-key>r8_REPLACE-ME</api-key>
138
+ <endpoint>https://api.replicate.com/v1</endpoint>
139
+ <default-model>kwaivgi/kling-v2.1-master</default-model>
140
+ <dry-run>true</dry-run>
141
+ <tuning>
142
+ <aspect>16:9</aspect>
143
+ <max-duration>10</max-duration>
144
+ <audio-native>false</audio-native>
145
+ </tuning>
146
+ </provider>
147
+
148
+ <!--
149
+ Local ComfyUI (ADR-060 — container-primary sandbox). No api-key:
150
+ the adapter talks to YOUR ComfyUI. A loopback endpoint is assumed
151
+ to be the shipped container posture (read-only image, cap-drop ALL,
152
+ network-none at generation time — run line in the adapter header).
153
+ A NON-loopback endpoint is the remote escape hatch: isolation is
154
+ the operator's responsibility and the block must carry
155
+ <unsandboxed>accepted</unsandboxed> or the adapter refuses (exit 6).
156
+ Models: wan22-ti2v-5b (default, Apache-2.0) | ltx-2 (audio-sync).
157
+ Shipped workflow templates only — operator workflow JSONs are
158
+ rejected by design. macOS: no CUDA — container path is
159
+ reference-only; use the remote escape hatch.
160
+ -->
161
+ <provider id="comfyui" kind="video">
162
+ <lifecycle>experimental</lifecycle>
163
+ <enabled>true</enabled>
164
+ <endpoint>http://127.0.0.1:8188</endpoint>
165
+ <default-model>wan22-ti2v-5b</default-model>
166
+ <dry-run>true</dry-run>
167
+ <tuning>
168
+ <aspect>16:9</aspect>
169
+ <max-duration>5</max-duration>
170
+ <audio-native>false</audio-native>
171
+ </tuning>
172
+ </provider>
173
+
174
+ <!-- ============================================================ -->
175
+ <!-- Audio adapters (music-video orchestration) -->
176
+ <!-- Contract: scripts/ai-video/lib/audio-adapter-contract.md -->
177
+ <!-- local-cli adapters wrap an OPERATOR-installed tool (pip) — -->
178
+ <!-- no api-key element needed; <enabled>false</enabled> opts a -->
179
+ <!-- configured-but-unwanted adapter out (otherwise a missing CLI -->
180
+ <!-- is a loud config failure, never a silent probe fallback). -->
181
+ <!-- ============================================================ -->
182
+
183
+ <provider id="allin1" kind="audio-analysis">
184
+ <lifecycle>experimental</lifecycle>
185
+ <enabled>true</enabled>
186
+ <dry-run>true</dry-run>
187
+ </provider>
188
+
189
+ <provider id="whisperx" kind="lyrics">
190
+ <lifecycle>experimental</lifecycle>
191
+ <enabled>true</enabled>
192
+ <dry-run>true</dry-run>
193
+ </provider>
194
+
195
+ <!-- ============================================================ -->
196
+ <!-- Lip-sync post-process (v2 contract, kind="lipsync") -->
197
+ <!-- Sparse budget: lib/model-capabilities/syncso.json -->
198
+ <!-- ============================================================ -->
199
+
200
+ <!--
201
+ Local (MIT) lip-sync — offline alternative to syncso. Wraps an
202
+ OPERATOR-installed MuseTalk (github.com/TMElyralab/MuseTalk); the
203
+ wrapped entry point is configurable via the MUSETALK_CMD env var.
204
+ No api-key. Same sparse lipsync_budget discipline as syncso
205
+ (lib/model-capabilities/musetalk.json) — local renders are free but
206
+ the quality argument for sparseness is unchanged.
207
+ -->
208
+ <provider id="musetalk" kind="lipsync">
209
+ <lifecycle>experimental</lifecycle>
210
+ <enabled>true</enabled>
211
+ <dry-run>true</dry-run>
212
+ </provider>
213
+
214
+ <provider id="syncso" kind="lipsync">
215
+ <lifecycle>experimental</lifecycle>
216
+ <enabled>true</enabled>
217
+ <api-key>sk-REPLACE-ME</api-key>
218
+ <endpoint>https://api.sync.so</endpoint>
219
+ <default-model>lipsync-2</default-model>
220
+ <dry-run>true</dry-run>
221
+ </provider>
222
+
223
+ <!-- ============================================================ -->
224
+ <!-- Extra slot — future providers without schema bump -->
225
+ <!-- Use kind="image" | "video" | "image+video" | "audio-analysis"-->
226
+ <!-- | "lyrics" | "lipsync". -->
227
+ <!-- ============================================================ -->
228
+
229
+ <extra>
230
+ <!--
231
+ <provider id="my-future-backend" kind="video">
232
+ <lifecycle>experimental</lifecycle>
233
+ <api-key>REPLACE-ME</api-key>
234
+ <endpoint>https://example.invalid/v1</endpoint>
235
+ <default-model>example-1</default-model>
236
+ <dry-run>true</dry-run>
237
+ <tuning>
238
+ <aspect>16:9</aspect>
239
+ <fps>24</fps>
240
+ <max-duration>6</max-duration>
241
+ <audio-native>false</audio-native>
242
+ </tuning>
243
+ </provider>
244
+ -->
245
+ </extra>
246
+
247
+ </ai-video>
@@ -20,7 +20,7 @@ applies_to_user_types:
20
20
  routes_to:
21
21
  - "guideline:design-fidelity-mechanics"
22
22
  workspaces: [engineering]
23
- packs: [frontend-design]
23
+ packs: [engineering-base, frontend-design]
24
24
  ---
25
25
 
26
26
  # Design Fidelity
@@ -31,7 +31,7 @@ Use this skill when:
31
31
  Do NOT use when:
32
32
  - `directive_set="ui-trivial"` — the trivial path bypasses audit by precondition (≤1 file, ≤5 lines, no new component, no new state, no new dep)
33
33
  - `directive_set="backend"` — no UI surface to inventory
34
- - The audit findings already exist in `state.ui_audit` for this state-file (cached) — re-run only if `package.json` or `composer.json` mtime changed
34
+ - The audit findings already exist in `state.ui_audit` for this state-file — the gate round-trips through SUCCESS once `audit_path` is set, and does **not** re-run for the life of that state-file (see Gotchas for what that does and does not cover)
35
35
 
36
36
  ## Resource-first context gate (design fidelity)
37
37
 
@@ -243,7 +243,20 @@ tool produces, so the import path is uniform.
243
243
  - Don't assume a Radix-only `package.json` means shadcn — shadcn requires `components.json` at repo root.
244
244
  - `state.ui_audit.shadcn_inventory.version` is often missing; the shadcn CLI does not always pin itself in `package.json`. Record `null` rather than guessing.
245
245
  - Greenfield is detected, not assumed — a project with one Blade layout and no components is still greenfield only if tokens AND design system markers AND components are all empty.
246
- - Re-running the skill on a stale state-file: cache by `(composer.json mtime, package.json mtime)`; if either changed, re-audit and overwrite.
246
+ - **There is no mtime cache.** An earlier revision told you to cache by
247
+ `(composer.json mtime, package.json mtime)` and re-audit when either changed.
248
+ No code implemented it, and the key was the wrong one anyway: `apply` adds
249
+ components without touching a manifest, so a manifest-keyed cache never
250
+ invalidates on the change that matters. What the engine does is simpler and
251
+ stricter — `audit.ts` returns SUCCESS unconditionally once `audit_path` is
252
+ `high_confidence` or `ambiguous`, so the inventory is computed **once per
253
+ state-file** and never refreshed.
254
+ - **What that means in practice.** A single request is one pass, so the
255
+ inventory is fresh where it matters. The bounded gap is a run that reuses one
256
+ state-file across several component additions: components written earlier in
257
+ that run are absent from the inventory the later ones are checked against.
258
+ Adding a second component in the same run → re-read the component directories
259
+ directly rather than trusting `state.ui_audit.components_found`.
247
260
 
248
261
  ## Anti-slop cross-reference
249
262
 
@@ -63,6 +63,16 @@ where the corpus reports an evidence gap or the audit already pins a
63
63
  project pattern. Stack-specific choices come from the dispatched
64
64
  implementation skill, not from here.
65
65
 
66
+ > **When the corpus is not installed.** `design-intelligence` ships in the
67
+ > `frontend-design` pack; this skill ships in `engineering-base`. A consumer
68
+ > who installed only `laravel` or only `react` therefore has this skill and
69
+ > **not** the corpus. Fall back to the heuristics here and say so in the
70
+ > result — "selected from heuristics; `frontend-design` not installed, so no
71
+ > corpus grounding". The evidence-gap fallback above is about a corpus that
72
+ > answered "nothing here"; this is about a corpus that is absent. Never
73
+ > present an ungrounded pick as grounded, and never record a missing pack as
74
+ > an evidence gap.
75
+
66
76
  ## Component Architecture
67
77
 
68
78
  ### Page structure (universal shape)
@@ -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.11.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
+ UNSUPPORTED_LANE,
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,36 @@ 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 (stack_label === UNSUPPORTED_LANE) {
136
+ return new StepResult({
137
+ outcome: Outcome.BLOCKED,
138
+ questions: unsupported_stack_questions('apply'),
139
+ message: 'UI apply refused: frontend framework not modelled by any lane.',
140
+ });
141
+ }
143
142
  return new StepResult({
144
143
  outcome: Outcome.BLOCKED,
145
144
  questions: [
146
145
  agent_directive(directive),
147
146
  `> Stack: \`${stack_label}\`. Implementing the locked design brief.`,
147
+ bundle_line(stack_label, 'build'),
148
148
  '> Microcopy is locked — every button label, empty-state ' +
149
149
  'message, and validation message must come verbatim from ' +
150
150
  '`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
+ UNSUPPORTED_LANE,
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,14 @@ function _delegate_to_polish_skill(
184
195
  ceiling: number,
185
196
  ): StepResult {
186
197
  const directive = _resolve_directive(state);
198
+ const unsupported_label = _stack_label(state);
199
+ if (unsupported_label === UNSUPPORTED_LANE) {
200
+ return new StepResult({
201
+ outcome: Outcome.BLOCKED,
202
+ questions: unsupported_stack_questions('polish'),
203
+ message: 'UI polish refused: frontend framework not modelled by any lane.',
204
+ });
205
+ }
187
206
  const stack_label = _stack_label(state);
188
207
  const next_round = rounds + 1;
189
208
  let findings_line =
@@ -203,6 +222,7 @@ function _delegate_to_polish_skill(
203
222
  questions: [
204
223
  agent_directive(directive),
205
224
  `> Stack: \`${stack_label}\`. Polish round ${next_round} of ${ceiling}.`,
225
+ bundle_line(stack_label, 'build'),
206
226
  findings_line,
207
227
  '> Fix chart-type / contrast findings against the adopted ' +
208
228
  "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
+ UNSUPPORTED_LANE,
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,20 @@ 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 (stack_label === UNSUPPORTED_LANE) {
196
+ return new StepResult({
197
+ outcome: Outcome.BLOCKED,
198
+ questions: unsupported_stack_questions('review'),
199
+ message: 'UI review refused: frontend framework not modelled by any lane.',
200
+ });
201
+ }
184
202
  return new StepResult({
185
203
  outcome: Outcome.BLOCKED,
186
204
  questions: [
187
205
  agent_directive(directive),
188
206
  `> Stack: \`${stack_label}\`. Reviewing rendered components ` +
189
207
  'against the locked design brief.',
208
+ bundle_line(stack_label, 'review'),
190
209
  '> The review pass compares `state.ticket.ui_apply.rendered` ' +
191
210
  'against `state.ui_design` (microcopy, states, a11y, layout) ' +
192
211
  'and produces a structured `findings` list.',