@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,506 @@
1
+ /**
2
+ * `audit` step — mandatory pre-step for the UI directive set.
3
+ *
4
+ * TypeScript twin of `directives/ui/audit.py` (ADR-200 py2ts). Public API
5
+ * names stay snake_case to mirror the Python module 1:1.
6
+ *
7
+ * Routes on `state.ui_audit` shape: first-pass delegation, greenfield decision,
8
+ * shadcn version mismatch, ambiguous candidate pick, or high-confidence pass.
9
+ * The deterministic checks live here so "no design without audit findings" is
10
+ * enforceable from code, not norms. Mirrors the `state.ui_audit` gate the
11
+ * `ui-audit-gate` rule defends.
12
+ */
13
+ import {
14
+ type Any,
15
+ type DeliveryState,
16
+ Outcome,
17
+ StepResult,
18
+ agent_directive,
19
+ } from '../../delivery_state.js';
20
+
21
+ /** Similarity threshold for a "strong reusable match". */
22
+ export const STRONG_SIMILARITY = 0.7;
23
+
24
+ /** Top-2 within this gap counts as ambiguous regardless of confidence. */
25
+ export const TIE_GAP = 0.05;
26
+
27
+ /** Major version the `react-shadcn-ui` skill body declares as tested against. */
28
+ export const TESTED_AGAINST_SHADCN_MAJOR = 2;
29
+
30
+ export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
31
+ {
32
+ code: 'audit_missing',
33
+ trigger: 'state.ui_audit is None or empty — skill has not run yet',
34
+ resolution:
35
+ 'agent directive `existing-ui-audit` → skill writes ' +
36
+ 'findings into state.ui_audit',
37
+ },
38
+ {
39
+ code: 'greenfield_undecided',
40
+ trigger:
41
+ 'state.ui_audit.greenfield is True but greenfield_decision ' +
42
+ 'is unset — user has not picked a scaffolding direction',
43
+ resolution:
44
+ 'user picks scaffold / bare / external_reference; ' +
45
+ 'agent records the choice in state.ui_audit.greenfield_decision',
46
+ },
47
+ {
48
+ code: 'shadcn_version_mismatch',
49
+ trigger:
50
+ 'state.ui_audit.shadcn_inventory.version major differs from ' +
51
+ 'TESTED_AGAINST_SHADCN_MAJOR — react-shadcn-ui skill was ' +
52
+ 'tested against a different major',
53
+ resolution:
54
+ 'user accepts cautious composition or aborts; ' +
55
+ 'agent records the choice in ' +
56
+ 'state.ui_audit.version_mismatch_decision',
57
+ },
58
+ {
59
+ code: 'audit_ambiguous',
60
+ trigger:
61
+ 'confidence band is medium, OR inventory has multiple matches ' +
62
+ 'with similar similarity scores, OR no match clears ' +
63
+ 'STRONG_SIMILARITY',
64
+ resolution:
65
+ "user picks a candidate to extend (or 'build new'); " +
66
+ 'agent records the choice in state.ui_audit.audit_path ' +
67
+ 'and state.ui_audit.candidate_pick',
68
+ },
69
+ ];
70
+
71
+ function _isDict(value: Any): value is Record<string, Any> {
72
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
73
+ }
74
+
75
+ function _pyTruthy(value: Any): boolean {
76
+ if (value === null || value === undefined) return false;
77
+ if (typeof value === 'string') return value.length > 0;
78
+ if (typeof value === 'number') return value !== 0;
79
+ if (typeof value === 'boolean') return value;
80
+ if (Array.isArray(value)) return value.length > 0;
81
+ if (typeof value === 'object') return Object.keys(value).length > 0;
82
+ return true;
83
+ }
84
+
85
+ function pyStr(value: Any): string {
86
+ if (value === null || value === undefined) return 'None';
87
+ if (value === true) return 'True';
88
+ if (value === false) return 'False';
89
+ return String(value);
90
+ }
91
+
92
+ function _pyRStrip(s: string): string {
93
+ return s.replace(/\s+$/u, '');
94
+ }
95
+
96
+ /** Apply the audit gate to `state.ui_audit`. */
97
+ export function run(state: DeliveryState): StepResult {
98
+ const audit = state.ui_audit;
99
+ if (!_is_populated(audit)) {
100
+ return _delegate_to_audit_skill(state);
101
+ }
102
+
103
+ const a = audit as Record<string, Any>;
104
+ if (a['greenfield'] === true && !_pyTruthy(a['greenfield_decision'])) {
105
+ return _halt_greenfield(state, a);
106
+ }
107
+
108
+ // Greenfield with a recorded decision skips the candidate-pick halt and
109
+ // lands on SUCCESS.
110
+ if (a['greenfield'] === true) {
111
+ if (!_pyTruthy(a['audit_path'])) {
112
+ a['audit_path'] = 'greenfield';
113
+ }
114
+ return new StepResult({ outcome: Outcome.SUCCESS });
115
+ }
116
+
117
+ const mismatch = _detect_shadcn_version_mismatch(a);
118
+ if (mismatch !== null && !_pyTruthy(a['version_mismatch_decision'])) {
119
+ return _halt_shadcn_version_mismatch(state, mismatch);
120
+ }
121
+
122
+ // Idempotent re-entry: an already-decided path round-trips through SUCCESS.
123
+ if (a['audit_path'] === 'high_confidence' || a['audit_path'] === 'ambiguous') {
124
+ return new StepResult({ outcome: Outcome.SUCCESS });
125
+ }
126
+
127
+ const decision = _decide_path(state, a);
128
+ if (decision === 'high_confidence') {
129
+ a['audit_path'] = 'high_confidence';
130
+ return new StepResult({ outcome: Outcome.SUCCESS });
131
+ }
132
+
133
+ return _halt_ambiguous(state, a);
134
+ }
135
+
136
+ /** True when `audit` carries actionable findings. */
137
+ function _is_populated(audit: Any): boolean {
138
+ if (!_isDict(audit)) return false;
139
+ if (Object.keys(audit).length === 0) return false;
140
+ return ['components_found', 'components', 'greenfield'].some((key) => key in audit);
141
+ }
142
+
143
+ /** Render a one-line preview of the input being audited. */
144
+ function _preview_input(state: DeliveryState): string {
145
+ const data = (_isDict(state.ticket) ? state.ticket : {}) as Record<string, Any>;
146
+ const raw = data['raw'];
147
+ let text: string;
148
+ if (typeof raw === 'string' && raw.trim() !== '') {
149
+ text = raw.split(/\s+/u).filter((x) => x.length > 0).join(' ');
150
+ } else {
151
+ const title = data['title'];
152
+ if (typeof title === 'string') {
153
+ text = title;
154
+ } else {
155
+ const id = data['id'];
156
+ text = _pyTruthy(id) ? pyStr(id) : '(no title)';
157
+ }
158
+ }
159
+ if ([...text].length <= 80) {
160
+ return text;
161
+ }
162
+ return _pyRStrip([...text].slice(0, 79).join('')) + '…';
163
+ }
164
+
165
+ /** Halt with an agent directive so the orchestrator runs `existing-ui-audit`. */
166
+ function _delegate_to_audit_skill(state: DeliveryState): StepResult {
167
+ const preview = _preview_input(state);
168
+ return new StepResult({
169
+ outcome: Outcome.BLOCKED,
170
+ questions: [
171
+ agent_directive('existing-ui-audit'),
172
+ `> Input: ${preview}`,
173
+ '> No UI audit findings yet — running `existing-ui-audit` ' +
174
+ 'to inventory components, design system, tokens, and ' +
175
+ 'candidate matches before design.',
176
+ '> 1. Continue — let the skill produce the audit',
177
+ '> 2. Abort — drop this UI request',
178
+ ],
179
+ message: 'UI audit findings missing; delegating to existing-ui-audit skill.',
180
+ });
181
+ }
182
+
183
+ /** BLOCKED halt — greenfield project needs an explicit scaffolding pick. */
184
+ function _halt_greenfield(state: DeliveryState, audit: Record<string, Any>): StepResult {
185
+ void audit;
186
+ const preview = _preview_input(state);
187
+ const questions: string[] = [
188
+ `> Input: ${preview}`,
189
+ '> No existing UI surface detected — this looks like greenfield.',
190
+ '> 1. Scaffold — minimal token set + base component primitive folder',
191
+ '> 2. Bare — proceed with Tailwind defaults, no scaffolding',
192
+ '> 3. External reference — point me at a design-system URL or file',
193
+ '',
194
+ '**Recommendation: 1 — Scaffold tokens + primitives** ' +
195
+ '— even one extra screen benefits from a shared base; the ' +
196
+ 'scaffold cost is ~10 min and saves re-doing every primitive ' +
197
+ 'on screen 2. Caveat: flip to 2 if this is a demo or ' +
198
+ 'single-page prototype that will not grow.',
199
+ ];
200
+ return new StepResult({
201
+ outcome: Outcome.BLOCKED,
202
+ questions,
203
+ message:
204
+ 'UI audit detected greenfield; halting for scaffolding ' +
205
+ 'direction (scaffold / bare / external_reference).',
206
+ });
207
+ }
208
+
209
+ /** Return `"high_confidence"` or `"ambiguous"` for a populated audit. */
210
+ function _decide_path(state: DeliveryState, audit: Record<string, Any>): string {
211
+ const band = _confidence_band(state);
212
+ if (band !== 'high') {
213
+ return 'ambiguous';
214
+ }
215
+
216
+ const matches = _matches(audit);
217
+ if (matches.length === 0) {
218
+ return 'ambiguous';
219
+ }
220
+
221
+ const scored = matches.map((m) => _similarity_of(m)).sort((x, y) => y - x);
222
+ const top = scored[0] as number;
223
+ if (top < STRONG_SIMILARITY) {
224
+ return 'ambiguous';
225
+ }
226
+ if (scored.length >= 2 && top - (scored[1] as number) < TIE_GAP) {
227
+ return 'ambiguous';
228
+ }
229
+ return 'high_confidence';
230
+ }
231
+
232
+ /** Return the scored confidence band, or `"high"` when not applicable. */
233
+ function _confidence_band(state: DeliveryState): string {
234
+ const data = (_isDict(state.ticket) ? state.ticket : {}) as Record<string, Any>;
235
+ const confidence = data['confidence'];
236
+ if (_isDict(confidence)) {
237
+ const band = confidence['band'];
238
+ if (typeof band === 'string' && band !== '') {
239
+ return band;
240
+ }
241
+ }
242
+ const input_kind = data['input_kind'];
243
+ if (input_kind === 'diff' || input_kind === 'file') {
244
+ return 'high';
245
+ }
246
+ return 'medium';
247
+ }
248
+
249
+ /** Return the inventory list, preferring `components_found`. */
250
+ function _matches(audit: Record<string, Any>): Record<string, Any>[] {
251
+ for (const key of ['components_found', 'components']) {
252
+ const value = audit[key];
253
+ if (Array.isArray(value) && value.length > 0) {
254
+ return value.filter((m): m is Record<string, Any> => _isDict(m));
255
+ }
256
+ }
257
+ return [];
258
+ }
259
+
260
+ /** Read a similarity score from a match entry; default to 0.0. */
261
+ function _similarity_of(match: Record<string, Any>): number {
262
+ const raw = match['similarity'];
263
+ const f = _pyFloat(raw);
264
+ return f === null ? 0.0 : f;
265
+ }
266
+
267
+ /**
268
+ * Python `float(x)` for the JSON shapes that reach a similarity value.
269
+ * Returns the float, or `null` to mirror `except (TypeError, ValueError)`.
270
+ */
271
+ function _pyFloat(value: Any): number | null {
272
+ if (typeof value === 'boolean') {
273
+ return value ? 1.0 : 0.0;
274
+ }
275
+ if (typeof value === 'number') {
276
+ return value;
277
+ }
278
+ if (typeof value === 'string') {
279
+ const t = value.trim();
280
+ if (t === '') return null;
281
+ // Python float() accepts decimals, exponents, inf, nan; the inputs here
282
+ // are plain decimals — match the common numeric form.
283
+ if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(t)) {
284
+ return Number(t);
285
+ }
286
+ const lower = t.toLowerCase().replace(/^[+-]/, '');
287
+ if (lower === 'inf' || lower === 'infinity') {
288
+ return t.startsWith('-') ? -Infinity : Infinity;
289
+ }
290
+ if (lower === 'nan') {
291
+ return NaN;
292
+ }
293
+ return null;
294
+ }
295
+ return null;
296
+ }
297
+
298
+ /** Return mismatch info when the inventory diverges by a major. */
299
+ function _detect_shadcn_version_mismatch(audit: Record<string, Any>): Record<string, Any> | null {
300
+ const inventory = audit['shadcn_inventory'];
301
+ if (!_isDict(inventory)) {
302
+ return null;
303
+ }
304
+ const raw_version = inventory['version'];
305
+ if (typeof raw_version !== 'string' || raw_version.trim() === '') {
306
+ return null;
307
+ }
308
+ const head = _pyRStripWs(_pyLStripChar(raw_version, 'v').split('.', 1)[0] as string);
309
+ const installed_major = _pyIntStrict(head);
310
+ if (installed_major === null) {
311
+ return null;
312
+ }
313
+ if (installed_major === TESTED_AGAINST_SHADCN_MAJOR) {
314
+ return null;
315
+ }
316
+ return {
317
+ installed_version: raw_version,
318
+ installed_major,
319
+ tested_major: TESTED_AGAINST_SHADCN_MAJOR,
320
+ };
321
+ }
322
+
323
+ /** Python `str.lstrip(ch)`. */
324
+ function _pyLStripChar(s: string, ch: string): string {
325
+ let start = 0;
326
+ while (start < s.length && s[start] === ch) start += 1;
327
+ return s.slice(start);
328
+ }
329
+
330
+ /** Python `str.strip()` (no-arg) applied to the parsed head. */
331
+ function _pyRStripWs(s: string): string {
332
+ return s.replace(/^\s+/u, '').replace(/\s+$/u, '');
333
+ }
334
+
335
+ /**
336
+ * Python `str.split(sep, 1)` for a single-character separator: at most one
337
+ * split, returning the first segment via `[0]`.
338
+ */
339
+ // (inlined via String.prototype.split(sep, limit) won't replicate Python's
340
+ // maxsplit semantics for `[0]`; but `[0]` only needs the prefix before the
341
+ // first separator, which `split` then index 0 yields identically.)
342
+
343
+ /**
344
+ * Python `int(s)` on an already-trimmed integer string. Returns the int, or
345
+ * `null` to mirror `except ValueError`.
346
+ */
347
+ function _pyIntStrict(s: string): number | null {
348
+ if (/^[+-]?[0-9]+$/.test(s)) {
349
+ return Number(s);
350
+ }
351
+ return null;
352
+ }
353
+
354
+ /** BLOCKED soft-halt — user accepts cautious composition or aborts. */
355
+ function _halt_shadcn_version_mismatch(
356
+ state: DeliveryState,
357
+ mismatch: Record<string, Any>,
358
+ ): StepResult {
359
+ const preview = _preview_input(state);
360
+ const installed = mismatch['installed_version'];
361
+ const tested = mismatch['tested_major'];
362
+ const installed_major = mismatch['installed_major'];
363
+ return new StepResult({
364
+ outcome: Outcome.BLOCKED,
365
+ questions: [
366
+ `> Input: ${preview}`,
367
+ `> shadcn skill tested against v${pyStr(tested)}.x; project uses ` +
368
+ `\`${pyStr(installed)}\` (major v${pyStr(installed_major)}).`,
369
+ '> 1. Proceed with cautious composition — skill applies ' +
370
+ 'general patterns, agent verifies primitive APIs against ' +
371
+ 'the installed version',
372
+ '> 2. Abort — update the `react-shadcn-ui` skill to the ' +
373
+ 'installed major before continuing',
374
+ '',
375
+ '**Recommendation: 1 — Proceed with caution** ' +
376
+ '— most shadcn primitive APIs are stable across ' +
377
+ "majors; the skill's structural guidance still applies. " +
378
+ 'Caveat: flip to 2 if the design brief leans on a ' +
379
+ 'primitive whose API changed (Form, Sheet, Dialog have ' +
380
+ 'had breaking renames in past majors).',
381
+ ],
382
+ message:
383
+ `shadcn version mismatch (skill v${pyStr(tested)}.x vs project ` +
384
+ `${pyStr(installed)}); halting for cautious-composition decision.`,
385
+ });
386
+ }
387
+
388
+ /** BLOCKED halt — user picks an existing candidate or 'build new'. */
389
+ function _halt_ambiguous(state: DeliveryState, audit: Record<string, Any>): StepResult {
390
+ const preview = _preview_input(state);
391
+ const matches = _matches(audit);
392
+ const scored = _stableSortByKeyDesc(matches, _similarity_of).slice(0, 3);
393
+
394
+ const lines: string[] = [
395
+ `> Input: ${preview}`,
396
+ '> Audit findings are ambiguous — pick the candidate to ' +
397
+ 'extend, or build new:',
398
+ ];
399
+ let idx = 1;
400
+ for (const match of scored) {
401
+ const name = _pyTruthy(match['name'])
402
+ ? match['name']
403
+ : _pyTruthy(match['path'])
404
+ ? match['path']
405
+ : '(unnamed)';
406
+ const sim = _similarity_of(match);
407
+ const path = _pyTruthy(match['path']) ? match['path'] : '';
408
+ const suffix = path ? ` — \`${pyStr(path)}\`` : '';
409
+ lines.push(`> ${idx}. Extend \`${pyStr(name)}\` (similarity ${_pyFixed(sim, 2)})${suffix}`);
410
+ idx += 1;
411
+ }
412
+ const next_idx = scored.length + 1;
413
+ lines.push(`> ${next_idx}. Build new — none of the above is close enough`);
414
+
415
+ let rec: string;
416
+ if (scored.length > 0) {
417
+ const top = scored[0] as Record<string, Any>;
418
+ const top_name = _pyTruthy(top['name'])
419
+ ? top['name']
420
+ : _pyTruthy(top['path'])
421
+ ? top['path']
422
+ : 'candidate 1';
423
+ const top_sim = _similarity_of(top);
424
+ rec =
425
+ `**Recommendation: 1 — Extend \`${pyStr(top_name)}\`** — ` +
426
+ `similarity ${_pyFixed(top_sim, 2)} is the strongest match in the ` +
427
+ 'inventory; reuse beats new code unless the contract ' +
428
+ `diverges. Caveat: flip to ${next_idx} if the existing ` +
429
+ 'component cannot host the new behavior cleanly.';
430
+ } else {
431
+ rec =
432
+ `**Recommendation: ${next_idx} — Build new** — ` +
433
+ 'no inventory match cleared the strong-similarity bar. ' +
434
+ 'Caveat: flip to an extend option only if a near-miss ' +
435
+ 'is a better fit than starting from scratch.';
436
+ }
437
+ lines.push('', rec);
438
+
439
+ return new StepResult({
440
+ outcome: Outcome.BLOCKED,
441
+ questions: lines,
442
+ message:
443
+ 'UI audit findings ambiguous; halting for candidate pick ' +
444
+ '(extend existing / build new).',
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Python `sorted(matches, key=_similarity_of, reverse=True)` — stable sort,
450
+ * descending by key, ties keep original order. JS `Array.prototype.sort` is
451
+ * stable; for `reverse=True` Python keeps equal-key elements in original
452
+ * order, so a stable comparator that only compares keys (never swaps ties)
453
+ * reproduces it exactly.
454
+ */
455
+ function _stableSortByKeyDesc(
456
+ items: Record<string, Any>[],
457
+ key: (m: Record<string, Any>) => number,
458
+ ): Record<string, Any>[] {
459
+ const keyed = items.map((m) => ({ m, k: key(m) }));
460
+ keyed.sort((a, b) => (a.k < b.k ? 1 : a.k > b.k ? -1 : 0));
461
+ return keyed.map((e) => e.m);
462
+ }
463
+
464
+ /**
465
+ * Python `format(x, '.2f')` — fixed-point, 2 decimals, round-half-to-even on
466
+ * the exact IEEE value. Mirrors `telemetry/report_renderer._pyFixed`.
467
+ */
468
+ function _pyFixed(value: number, ndigits: number): string {
469
+ if (!Number.isFinite(value)) {
470
+ return String(value);
471
+ }
472
+ const sign = value < 0 ? '-' : '';
473
+ const abs = Math.abs(value);
474
+ const str = abs.toPrecision(17);
475
+ if (str.includes('e') || str.includes('E')) {
476
+ return sign + abs.toFixed(ndigits);
477
+ }
478
+ const dot = str.indexOf('.');
479
+ const intPart = dot === -1 ? str : str.slice(0, dot);
480
+ let fracPart = dot === -1 ? '' : str.slice(dot + 1);
481
+ while (fracPart.length <= ndigits) {
482
+ fracPart += '0';
483
+ }
484
+ const keepFrac = fracPart.slice(0, ndigits);
485
+ const deciderStr = fracPart.slice(ndigits);
486
+ let scaledInt = BigInt(intPart + keepFrac || '0');
487
+ const firstDecider = deciderStr.charAt(0);
488
+ const restNonZero = /[1-9]/u.test(deciderStr.slice(1));
489
+ let roundUp = false;
490
+ if (firstDecider > '5' || (firstDecider === '5' && restNonZero)) {
491
+ roundUp = true;
492
+ } else if (firstDecider === '5' && !restNonZero) {
493
+ roundUp = scaledInt % 2n === 1n;
494
+ }
495
+ if (roundUp) {
496
+ scaledInt += 1n;
497
+ }
498
+ let digits = scaledInt.toString();
499
+ while (digits.length <= ndigits) {
500
+ digits = `0${digits}`;
501
+ }
502
+ const outInt = ndigits === 0 ? digits : digits.slice(0, digits.length - ndigits);
503
+ const outFrac = ndigits === 0 ? '' : digits.slice(digits.length - ndigits);
504
+ const zeroValue = /^0*$/u.test(digits);
505
+ return `${zeroValue ? '' : sign}${outInt}${ndigits > 0 ? `.${outFrac}` : ''}`;
506
+ }