@event4u/agent-config 8.4.0 → 8.4.1

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/discovery/deprecation-report.md +1 -1
  5. package/dist/discovery/discovery-manifest.json +1 -1
  6. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  7. package/dist/discovery/discovery-manifest.summary.md +1 -1
  8. package/dist/discovery/orphan-report.md +1 -1
  9. package/dist/discovery/packs.json +1 -1
  10. package/dist/discovery/trust-report.md +1 -1
  11. package/dist/discovery/workspaces.json +1 -1
  12. package/dist/mcp/registry-manifest.json +1 -1
  13. package/package.json +4 -1
  14. package/src/agent-src/README.md +64 -0
  15. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  16. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  17. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  18. package/src/agent-src/templates/AGENTS.md +34 -0
  19. package/src/agent-src/templates/scripts/README.md +195 -0
  20. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  21. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  22. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  23. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  24. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  25. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  26. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  27. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  28. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  29. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  30. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  31. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  32. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  33. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  34. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  35. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  36. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  37. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  38. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  39. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  40. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  41. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  42. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  43. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  44. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  45. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  46. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  47. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  48. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  49. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  50. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  51. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  77. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  78. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  79. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  80. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  81. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  82. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  83. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  84. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  99. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  100. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  101. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  102. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  103. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  104. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  105. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  106. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  107. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  108. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  109. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  110. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  111. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  112. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  113. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  114. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  115. package/src/install/atomic.ts +111 -0
  116. package/src/install/conflict.ts +226 -0
  117. package/src/install/detect.ts +280 -0
  118. package/src/install/paths.ts +90 -0
  119. package/src/install/plan.ts +205 -0
  120. package/src/install/preflight.ts +210 -0
  121. package/src/install/selectedTools.ts +52 -0
  122. package/src/install/toolDetection.ts +120 -0
  123. package/src/install/txlog.ts +161 -0
  124. package/src/install/types.ts +175 -0
  125. package/src/install/wizard-plan.ts +216 -0
  126. package/src/scripts/prepack-check.mjs +108 -5
@@ -0,0 +1,474 @@
1
+ /**
2
+ * `review` step — stack-dispatched design-review pass.
3
+ *
4
+ * TypeScript twin of `directives/ui/review.py` (ADR-094 py2ts). Public API
5
+ * names stay snake_case to mirror the Python module 1:1.
6
+ *
7
+ * The review step compares the rendered components from `apply` against the
8
+ * locked design brief and produces a structured findings list. It does not
9
+ * apply fixes — that is the polish step's job. Review's single output is
10
+ * `state.ui_review` carrying `findings` and `review_clean`.
11
+ */
12
+ import {
13
+ type Any,
14
+ type DeliveryState,
15
+ Outcome,
16
+ StepResult,
17
+ agent_directive,
18
+ } from '../../delivery_state.js';
19
+
20
+ /** Map `state.stack.frontend` → agent-directive skill name. */
21
+ export const STACK_DIRECTIVES: Record<string, string> = {
22
+ 'blade-livewire-flux': 'ui-design-review-blade-livewire-flux',
23
+ 'react-shadcn': 'ui-design-review-react-shadcn',
24
+ vue: 'ui-design-review-vue',
25
+ plain: 'ui-design-review-plain',
26
+ };
27
+
28
+ /** Fallback directive when `state.stack` is missing or malformed. */
29
+ export const DEFAULT_DIRECTIVE = 'ui-design-review-plain';
30
+
31
+ /** R4 a11y severity ranking — mirrors axe-core's impact levels. */
32
+ export const SEVERITY_ORDER: Record<string, number> = {
33
+ minor: 0,
34
+ moderate: 1,
35
+ serious: 2,
36
+ critical: 3,
37
+ };
38
+
39
+ /** R4 a11y default severity floor. */
40
+ export const DEFAULT_SEVERITY_FLOOR = 'moderate';
41
+
42
+ export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
43
+ {
44
+ code: 'review_envelope_missing',
45
+ trigger: 'state.ui_review unset / empty — review skill has not run yet',
46
+ resolution:
47
+ 'agent directive `ui-design-review-<stack>` → ' +
48
+ 'skill compares rendered components against state.ui_design ' +
49
+ 'and writes `findings` + `review_clean` back',
50
+ },
51
+ {
52
+ code: 'review_findings_missing',
53
+ trigger: 'state.ui_review populated but `findings` key absent',
54
+ resolution:
55
+ 'agent re-runs the review skill with the same ' +
56
+ 'directive; review only succeeds once findings is a list',
57
+ },
58
+ {
59
+ code: 'review_clean_missing',
60
+ trigger:
61
+ 'state.ui_review.findings is set but review_clean ' +
62
+ 'is missing or not a bool — polish needs an explicit flag',
63
+ resolution:
64
+ 'agent sets state.ui_review.review_clean to ' +
65
+ 'True or False before returning the envelope; review does ' +
66
+ 'not infer it from findings count',
67
+ },
68
+ {
69
+ code: 'review_a11y_pending',
70
+ trigger:
71
+ 'state.ui_audit declared an `a11y_baseline` but ' +
72
+ 'state.ui_review.a11y is missing — the review skill ran but ' +
73
+ 'did not produce an a11y envelope',
74
+ resolution:
75
+ 'agent re-runs the review skill so it captures ' +
76
+ 'axe-core (or equivalent) findings into ' +
77
+ '`state.ui_review.a11y.violations`; the gate then filters ' +
78
+ 'against the baseline and the severity floor',
79
+ },
80
+ {
81
+ code: 'preview_render_failed',
82
+ trigger:
83
+ 'state.ui_review.preview.render_ok is False — the ' +
84
+ 'stack-specific review skill tried to render the changed ' +
85
+ 'components and the headless browser reported an error',
86
+ resolution:
87
+ 'user picks Retry (re-run the review skill so it ' +
88
+ 'renders again), Skip (write `state.ui_review.preview.skipped = ' +
89
+ 'true` so the gate stops asking this run), or Abort',
90
+ },
91
+ {
92
+ code: 'preview_render_required',
93
+ trigger:
94
+ 'the resolved stack is render-capable (has a rendering ' +
95
+ 'review skill) but state.ui_review.preview carries no `render_ok` — ' +
96
+ 'render evidence is required, not optional, so a skill cannot claim ' +
97
+ 'success without rendering',
98
+ resolution:
99
+ 'user picks Render (re-run the review skill so it ' +
100
+ 'drives the headless browser and writes ' +
101
+ '`{render_ok, screenshot_path, dom_dump_path}`), Skip (this env ' +
102
+ 'cannot render: set `state.ui_review.preview.skipped = true` plus a ' +
103
+ '`skip_reason`), or Abort',
104
+ },
105
+ ];
106
+
107
+ function _isDict(value: Any): value is Record<string, Any> {
108
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
109
+ }
110
+
111
+ function _pyTruthy(value: Any): boolean {
112
+ if (value === null || value === undefined) return false;
113
+ if (typeof value === 'string') return value.length > 0;
114
+ if (typeof value === 'number') return value !== 0;
115
+ if (typeof value === 'boolean') return value;
116
+ if (Array.isArray(value)) return value.length > 0;
117
+ if (typeof value === 'object') return Object.keys(value).length > 0;
118
+ return true;
119
+ }
120
+
121
+ /** Apply the design-review gate to `state.ui_review`. */
122
+ export function run(state: DeliveryState): StepResult {
123
+ const review = state.ui_review;
124
+ if (!_is_populated(review)) {
125
+ return _delegate_to_review_skill(state);
126
+ }
127
+
128
+ const r = review as Record<string, Any>;
129
+ if (!('findings' in r) || !Array.isArray(r['findings'])) {
130
+ return _halt_findings_missing(state);
131
+ }
132
+
133
+ const findings = r['findings'] as Any[];
134
+ if (typeof r['review_clean'] !== 'boolean') {
135
+ return _halt_clean_missing(state, findings.length);
136
+ }
137
+
138
+ const a11y_halt = _apply_a11y_gate(state, r);
139
+ if (a11y_halt !== null) {
140
+ return a11y_halt;
141
+ }
142
+
143
+ const preview_halt = _apply_preview_gate(state, r);
144
+ if (preview_halt !== null) {
145
+ return preview_halt;
146
+ }
147
+
148
+ return new StepResult({ outcome: Outcome.SUCCESS });
149
+ }
150
+
151
+ /** True when `review` is a dict with at least one own key. */
152
+ function _is_populated(review: Any): boolean {
153
+ return _isDict(review) && Object.keys(review).length > 0;
154
+ }
155
+
156
+ /** Pick the agent directive for the project's frontend stack. */
157
+ function _resolve_directive(state: DeliveryState): string {
158
+ const stack = _pyTruthy(state.stack) ? state.stack : {};
159
+ if (_isDict(stack)) {
160
+ const frontend = stack['frontend'];
161
+ if (typeof frontend === 'string' && frontend in STACK_DIRECTIVES) {
162
+ return STACK_DIRECTIVES[frontend] as string;
163
+ }
164
+ }
165
+ return DEFAULT_DIRECTIVE;
166
+ }
167
+
168
+ /** Return the frontend stack label, defaulting to `plain`. */
169
+ function _stack_label(state: DeliveryState): string {
170
+ const stack = _pyTruthy(state.stack) ? state.stack : {};
171
+ if (_isDict(stack)) {
172
+ const frontend = stack['frontend'];
173
+ if (typeof frontend === 'string' && frontend !== '') {
174
+ return frontend;
175
+ }
176
+ }
177
+ return 'plain';
178
+ }
179
+
180
+ /** First-pass halt — emit the stack-specific review directive. */
181
+ function _delegate_to_review_skill(state: DeliveryState): StepResult {
182
+ const directive = _resolve_directive(state);
183
+ const stack_label = _stack_label(state);
184
+ return new StepResult({
185
+ outcome: Outcome.BLOCKED,
186
+ questions: [
187
+ agent_directive(directive),
188
+ `> Stack: \`${stack_label}\`. Reviewing rendered components ` +
189
+ 'against the locked design brief.',
190
+ '> The review pass compares `state.ticket.ui_apply.rendered` ' +
191
+ 'against `state.ui_design` (microcopy, states, a11y, layout) ' +
192
+ 'and produces a structured `findings` list.',
193
+ '> Ground chart-type and contrast findings in the adopted ' +
194
+ "corpus (design-intelligence § 'Grounding the review/polish " +
195
+ "a11y gate') — cite the corpus row, don't eyeball.",
196
+ '> 1. Continue — run the review and write ' +
197
+ '`{findings: [...], review_clean: bool}` into ' +
198
+ '`state.ui_review`',
199
+ '> 2. Abort — drop this UI request',
200
+ ],
201
+ message: `UI review pending; delegating to \`${directive}\` for stack \`${stack_label}\`.`,
202
+ });
203
+ }
204
+
205
+ /** BLOCKED halt — envelope present but `findings` slot is unset. */
206
+ function _halt_findings_missing(state: DeliveryState): StepResult {
207
+ const directive = _resolve_directive(state);
208
+ return new StepResult({
209
+ outcome: Outcome.BLOCKED,
210
+ questions: [
211
+ agent_directive(directive),
212
+ '> Review envelope is partial: `findings` list is missing.',
213
+ '> Re-run the review skill so `state.ui_review.findings` ' +
214
+ 'is a list (empty when nothing is wrong).',
215
+ ],
216
+ message: 'UI review envelope incomplete; `findings` missing.',
217
+ });
218
+ }
219
+
220
+ /** BLOCKED halt — `review_clean` is missing or not a bool. */
221
+ function _halt_clean_missing(state: DeliveryState, findings_count: number): StepResult {
222
+ const directive = _resolve_directive(state);
223
+ return new StepResult({
224
+ outcome: Outcome.BLOCKED,
225
+ questions: [
226
+ agent_directive(directive),
227
+ '> Review envelope is incomplete: `review_clean` is missing ' +
228
+ 'or not a boolean.',
229
+ `> Findings count: ${findings_count}. Set ` +
230
+ '`state.ui_review.review_clean` to `True` (no further ' +
231
+ 'polish needed) or `False` (polish loop should run).',
232
+ ],
233
+ message: 'UI review envelope incomplete; `review_clean` must be a bool.',
234
+ });
235
+ }
236
+
237
+ /** R4 Phase 1: enforce a11y gate after the basic shape gates pass. */
238
+ function _apply_a11y_gate(state: DeliveryState, review: Record<string, Any>): StepResult | null {
239
+ const audit = state.ui_audit;
240
+ const has_baseline = _isDict(audit) && 'a11y_baseline' in audit;
241
+ const a11y = review['a11y'];
242
+
243
+ if (a11y === null || a11y === undefined) {
244
+ if (has_baseline) {
245
+ return _halt_a11y_pending(state);
246
+ }
247
+ return null;
248
+ }
249
+
250
+ const a11yDict = a11y as Record<string, Any>;
251
+ const violations = _pyTruthy(a11yDict['violations']) ? (a11yDict['violations'] as Any[]) : [];
252
+ const baseline = has_baseline ? ((audit as Record<string, Any>)['a11y_baseline'] as Any[]) : [];
253
+ const accepted = _pyTruthy(a11yDict['accepted_violations'])
254
+ ? (a11yDict['accepted_violations'] as Any[])
255
+ : [];
256
+ const floor = _pyTruthy(a11yDict['severity_floor'])
257
+ ? (a11yDict['severity_floor'] as string)
258
+ : DEFAULT_SEVERITY_FLOOR;
259
+
260
+ let new_violations = _filter_known(violations, baseline);
261
+ new_violations = _filter_known(new_violations, accepted);
262
+ const actionable = new_violations.filter((v) => _at_or_above_floor(v, floor));
263
+
264
+ if (actionable.length === 0) {
265
+ return null;
266
+ }
267
+
268
+ _synthesize_a11y_findings(review['findings'] as Any[], actionable);
269
+ review['review_clean'] = false;
270
+ return null;
271
+ }
272
+
273
+ /** Drop violations whose `(rule, selector)` matches `known`. */
274
+ function _filter_known(violations: Any[], known: Any[]): Any[] {
275
+ if (!_pyTruthy(known)) {
276
+ return [...violations];
277
+ }
278
+ const keys = new Set<string>();
279
+ for (const entry of known) {
280
+ if (_isDict(entry)) {
281
+ keys.add(_keyOf(entry['rule'], entry['selector']));
282
+ }
283
+ }
284
+ if (keys.size === 0) {
285
+ return [...violations];
286
+ }
287
+ return violations.filter(
288
+ (v) => !(_isDict(v) && keys.has(_keyOf(v['rule'], v['selector']))),
289
+ );
290
+ }
291
+
292
+ /**
293
+ * Stable, collision-free key for a `(rule, selector)` tuple of arbitrary JSON
294
+ * scalars, mirroring Python tuple-equality in a JS `Set`.
295
+ */
296
+ function _keyOf(rule: Any, selector: Any): string {
297
+ return `${_tag(rule)}${_tag(selector)}`;
298
+ }
299
+
300
+ function _tag(v: Any): string {
301
+ if (v === null || v === undefined) return 'n:';
302
+ if (typeof v === 'boolean') return `b:${v ? '1' : '0'}`;
303
+ if (typeof v === 'number') return `f:${v}`;
304
+ if (typeof v === 'string') return `s:${v}`;
305
+ return `o:${JSON.stringify(v)}`;
306
+ }
307
+
308
+ /** `True` when `violation.severity` is at or above `floor`. */
309
+ function _at_or_above_floor(violation: Any, floor: string): boolean {
310
+ if (!_isDict(violation)) {
311
+ return false;
312
+ }
313
+ const severity = violation['severity'];
314
+ const sevKey = typeof severity === 'string' ? severity : '';
315
+ const sev_rank =
316
+ sevKey in SEVERITY_ORDER
317
+ ? (SEVERITY_ORDER[sevKey] as number)
318
+ : (SEVERITY_ORDER[DEFAULT_SEVERITY_FLOOR] as number);
319
+ const floor_rank =
320
+ floor in SEVERITY_ORDER
321
+ ? (SEVERITY_ORDER[floor] as number)
322
+ : (SEVERITY_ORDER[DEFAULT_SEVERITY_FLOOR] as number);
323
+ return sev_rank >= floor_rank;
324
+ }
325
+
326
+ /** Append `a11y_violation` findings, deduped by `(rule, selector)`. */
327
+ function _synthesize_a11y_findings(findings: Any[], actionable: Any[]): void {
328
+ const existing = new Set<string>();
329
+ for (const f of findings) {
330
+ if (_isDict(f) && f['kind'] === 'a11y_violation') {
331
+ existing.add(_keyOf(f['rule'], f['selector']));
332
+ }
333
+ }
334
+ for (const v of actionable) {
335
+ if (!_isDict(v)) {
336
+ continue;
337
+ }
338
+ const key = _keyOf(v['rule'], v['selector']);
339
+ if (existing.has(key)) {
340
+ continue;
341
+ }
342
+ findings.push({
343
+ kind: 'a11y_violation',
344
+ rule: v['rule'] ?? null,
345
+ selector: v['selector'] ?? null,
346
+ severity: v['severity'] ?? null,
347
+ });
348
+ existing.add(key);
349
+ }
350
+ }
351
+
352
+ /** BLOCKED halt — audit declared a baseline but review has no a11y. */
353
+ function _halt_a11y_pending(state: DeliveryState): StepResult {
354
+ const directive = _resolve_directive(state);
355
+ return new StepResult({
356
+ outcome: Outcome.BLOCKED,
357
+ questions: [
358
+ agent_directive(directive),
359
+ '> Review envelope is incomplete: the audit declared an ' +
360
+ '`a11y_baseline` but `state.ui_review.a11y` is missing.',
361
+ '> Re-run the review skill so it captures axe-core (or ' +
362
+ 'equivalent) findings into ' +
363
+ '`state.ui_review.a11y.violations`. The gate filters ' +
364
+ 'against the baseline and the severity floor ' +
365
+ `(default \`${DEFAULT_SEVERITY_FLOOR}\`).`,
366
+ ],
367
+ message:
368
+ 'UI review envelope incomplete; `a11y` envelope missing ' +
369
+ '(audit declared a baseline).',
370
+ });
371
+ }
372
+
373
+ /** R4 Phase 3: validate the visual-preview envelope written by the skill. */
374
+ function _apply_preview_gate(state: DeliveryState, review: Record<string, Any>): StepResult | null {
375
+ const raw_preview = review['preview'];
376
+ const preview: Record<string, Any> = _isDict(raw_preview) ? raw_preview : {};
377
+ if (_pyTruthy(preview['skipped'])) {
378
+ return null;
379
+ }
380
+ const render_ok = preview['render_ok'];
381
+ if (render_ok === false) {
382
+ return _halt_preview_failed(state, preview);
383
+ }
384
+ if (render_ok === true) {
385
+ return null;
386
+ }
387
+ // render_ok missing / None — absence is only tolerated for stacks that
388
+ // have no rendering review skill. A render-capable stack must render or
389
+ // explicitly skip; silent success is forbidden.
390
+ if (_is_render_capable(state)) {
391
+ return _halt_preview_required(state);
392
+ }
393
+ return null;
394
+ }
395
+
396
+ /**
397
+ * True when the resolved frontend stack has a rendering review skill.
398
+ *
399
+ * Render-capability is keyed off {@link STACK_DIRECTIVES} — every entry
400
+ * maps to a stack-specific `ui-design-review-<stack>` skill that drives a
401
+ * headless browser (Playwright + axe-core) and writes the `preview`
402
+ * envelope. A missing / unknown frontend falls back to the generic
403
+ * directive but is **not** treated as render-capable: the gate stays a
404
+ * silent no-op for it, so non-rendering stacks/envs are unaffected.
405
+ */
406
+ function _is_render_capable(state: DeliveryState): boolean {
407
+ const stack = _pyTruthy(state.stack) ? state.stack : {};
408
+ if (_isDict(stack)) {
409
+ const frontend = stack['frontend'];
410
+ if (typeof frontend === 'string' && frontend in STACK_DIRECTIVES) {
411
+ return true;
412
+ }
413
+ }
414
+ return false;
415
+ }
416
+
417
+ /** BLOCKED halt — render reported failure; user picks the next step. */
418
+ function _halt_preview_failed(state: DeliveryState, preview: Record<string, Any>): StepResult {
419
+ const directive = _resolve_directive(state);
420
+ const error = preview['error'];
421
+ const error_line =
422
+ typeof error === 'string' && error !== ''
423
+ ? `> Render error: \`${error}\`.`
424
+ : '> Render error: `(none reported)`.';
425
+ return new StepResult({
426
+ outcome: Outcome.BLOCKED,
427
+ questions: [
428
+ agent_directive(directive),
429
+ '> Visual preview failed: ' +
430
+ '`state.ui_review.preview.render_ok` is `False`.',
431
+ error_line,
432
+ '> 1. Retry — re-run the review skill so it renders again ' +
433
+ 'and writes a fresh `preview` envelope',
434
+ '> 2. Skip — set `state.ui_review.preview.skipped = true` ' +
435
+ 'so this run ships without a screenshot artifact',
436
+ '> 3. Abort — drop this UI request',
437
+ ],
438
+ message: 'UI preview render failed; awaiting user decision.',
439
+ });
440
+ }
441
+
442
+ /**
443
+ * BLOCKED halt — render-capable stack produced no render evidence.
444
+ *
445
+ * A skill that never rendered cannot pass the gate by simply omitting
446
+ * `render_ok`. The agent must render (Option 1) or record an explicit,
447
+ * reasoned skip (Option 2) — the only way a render-capable stack ships
448
+ * without a screenshot artifact.
449
+ */
450
+ function _halt_preview_required(state: DeliveryState): StepResult {
451
+ const directive = _resolve_directive(state);
452
+ const stack_label = _stack_label(state);
453
+ return new StepResult({
454
+ outcome: Outcome.BLOCKED,
455
+ questions: [
456
+ agent_directive(directive),
457
+ `> Stack \`${stack_label}\` is render-capable but ` +
458
+ '`state.ui_review.preview.render_ok` is absent — render evidence ' +
459
+ 'is required, not optional.',
460
+ '> 1. Render — re-run the review skill so it drives the ' +
461
+ 'headless browser (Playwright + axe-core) and writes ' +
462
+ '`{render_ok, screenshot_path, dom_dump_path}` into ' +
463
+ '`state.ui_review.preview`',
464
+ '> 2. Skip — this env cannot render: set ' +
465
+ '`state.ui_review.preview.skipped = true` plus a ' +
466
+ '`state.ui_review.preview.skip_reason` (e.g. no Playwright ' +
467
+ 'runner) so the gate honours an explicit, reasoned skip',
468
+ '> 3. Abort — drop this UI request',
469
+ ],
470
+ message:
471
+ `UI render evidence required for render-capable stack ` +
472
+ `\`${stack_label}\`; awaiting render or explicit skip.`,
473
+ });
474
+ }