@miller-tech/uap 1.64.3 → 1.65.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 (41) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +15 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/design.d.ts +9 -0
  5. package/dist/cli/design.d.ts.map +1 -0
  6. package/dist/cli/design.js +187 -0
  7. package/dist/cli/design.js.map +1 -0
  8. package/dist/cli/react.d.ts.map +1 -1
  9. package/dist/cli/react.js +4 -0
  10. package/dist/cli/react.js.map +1 -1
  11. package/dist/cli/setup.d.ts.map +1 -1
  12. package/dist/cli/setup.js +36 -0
  13. package/dist/cli/setup.js.map +1 -1
  14. package/dist/coordination/reactor.d.ts.map +1 -1
  15. package/dist/coordination/reactor.js +25 -2
  16. package/dist/coordination/reactor.js.map +1 -1
  17. package/dist/design/gate.d.ts +16 -0
  18. package/dist/design/gate.d.ts.map +1 -0
  19. package/dist/design/gate.js +79 -0
  20. package/dist/design/gate.js.map +1 -0
  21. package/dist/design/interrogate.d.ts +17 -0
  22. package/dist/design/interrogate.d.ts.map +1 -0
  23. package/dist/design/interrogate.js +258 -0
  24. package/dist/design/interrogate.js.map +1 -0
  25. package/dist/design/reactor-inject.d.ts +8 -0
  26. package/dist/design/reactor-inject.d.ts.map +1 -0
  27. package/dist/design/reactor-inject.js +37 -0
  28. package/dist/design/reactor-inject.js.map +1 -0
  29. package/dist/design/tokens.d.ts +62 -0
  30. package/dist/design/tokens.d.ts.map +1 -0
  31. package/dist/design/tokens.js +185 -0
  32. package/dist/design/tokens.js.map +1 -0
  33. package/dist/mcp-router/tools/react.d.ts.map +1 -1
  34. package/dist/mcp-router/tools/react.js +2 -0
  35. package/dist/mcp-router/tools/react.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  38. package/src/policies/enforcers/design_token_gate.py +145 -0
  39. package/src/policies/schemas/policies/design-token-gate.md +38 -0
  40. package/tools/agents/scripts/anthropic_proxy.py +22 -6
  41. package/tools/agents/tests/test_turn_count_breaker_periodic.py +64 -0
@@ -0,0 +1,62 @@
1
+ export interface TypographyToken {
2
+ fontFamily?: string;
3
+ fontSize?: string;
4
+ fontWeight?: string | number;
5
+ lineHeight?: string | number;
6
+ letterSpacing?: string;
7
+ fontFeature?: string;
8
+ fontVariation?: string;
9
+ }
10
+ export interface DesignTokens {
11
+ version?: string;
12
+ name?: string;
13
+ description?: string;
14
+ colors?: Record<string, string>;
15
+ typography?: Record<string, TypographyToken>;
16
+ rounded?: Record<string, string | number>;
17
+ spacing?: Record<string, string | number>;
18
+ components?: Record<string, Record<string, string>>;
19
+ }
20
+ export interface ParsedDesign {
21
+ tokens: DesignTokens;
22
+ /** Markdown body after the front matter. */
23
+ prose: string;
24
+ /** Section heading -> body text, keyed by lowercased `##` title. */
25
+ sections: Record<string, string>;
26
+ }
27
+ /** Flat, JSON-serializable allow-list consumed by the Python gate. */
28
+ export interface TokenAllowList {
29
+ name: string;
30
+ colors: string[];
31
+ spacing: string[];
32
+ radii: string[];
33
+ fontSizes: string[];
34
+ fontFamilies: string[];
35
+ generatedFrom: string;
36
+ }
37
+ /** Locate a DESIGN.md in `dir` (project root). Returns absolute path or null. */
38
+ export declare function findDesignFile(dir: string): string | null;
39
+ /** Split a DESIGN.md into YAML front matter + markdown body. */
40
+ export declare function parseDesignMd(content: string): ParsedDesign;
41
+ /** Resolve a `{path.to.token}` reference against the token tree; returns the
42
+ * raw string for non-references unchanged. */
43
+ export declare function resolveRef(tokens: DesignTokens, value: unknown): string;
44
+ /** Normalize a CSS color for set-membership comparison: lowercase, trim, and
45
+ * expand `#rgb`/`#rgba` shorthand to the 6/8-digit form. */
46
+ export declare function normalizeColor(c: string): string;
47
+ /** Derive the flat allow-list from a parsed DESIGN.md. */
48
+ export declare function buildAllowList(parsed: ParsedDesign, designPath: string, projectDir: string): TokenAllowList;
49
+ export declare function allowListPath(projectDir: string): string;
50
+ /** Write `.uap/design-tokens.json` (the gate's source of truth). */
51
+ export declare function writeAllowList(projectDir: string, allow: TokenAllowList): string;
52
+ /** Load + parse the project DESIGN.md, or null if none exists. */
53
+ export declare function loadDesign(projectDir: string): {
54
+ parsed: ParsedDesign;
55
+ path: string;
56
+ } | null;
57
+ /**
58
+ * Compact guidance string injected by the reactor when UI/UX work is detected.
59
+ * Keeps the agent on-token for new UI without re-reading the whole DESIGN.md.
60
+ */
61
+ export declare function summarizeForReactor(parsed: ParsedDesign, maxChars?: number): string;
62
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/design/tokens.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAID,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMzD;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CA2B3D;AAED;8CAC8C;AAC9C,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAcvE;AAED;4DAC4D;AAC5D,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhD;AAQD,0DAA0D;AAC1D,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,cAAc,CA2ChB;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAKhF;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAI5F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,SAAM,GAAG,MAAM,CA0BhF"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * DESIGN.md token model.
3
+ *
4
+ * Parses a project's DESIGN.md (the Google Labs DESIGN.md format: YAML token
5
+ * front matter + markdown prose) into a structured token set, resolves
6
+ * `{token.refs}`, and derives a flat allow-list that the (pure-stdlib) Python
7
+ * design-token gate consumes from `.uap/design-tokens.json`.
8
+ *
9
+ * Spec: https://github.com/google-labs-code/design.md (docs/spec.md).
10
+ */
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
+ import { join, dirname, relative } from 'path';
13
+ import yaml from 'js-yaml';
14
+ const DESIGN_FILENAMES = ['DESIGN.md', 'design.md', 'Design.md'];
15
+ /** Locate a DESIGN.md in `dir` (project root). Returns absolute path or null. */
16
+ export function findDesignFile(dir) {
17
+ for (const name of DESIGN_FILENAMES) {
18
+ const p = join(dir, name);
19
+ if (existsSync(p))
20
+ return p;
21
+ }
22
+ return null;
23
+ }
24
+ /** Split a DESIGN.md into YAML front matter + markdown body. */
25
+ export function parseDesignMd(content) {
26
+ let tokens = {};
27
+ let prose = content;
28
+ // Front matter must open and close with a line that is exactly `---`.
29
+ const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
30
+ if (fm) {
31
+ try {
32
+ tokens = yaml.load(fm[1]) ?? {};
33
+ }
34
+ catch {
35
+ tokens = {};
36
+ }
37
+ prose = content.slice(fm[0].length);
38
+ }
39
+ const sections = {};
40
+ // Split on `##` headings (not `###`).
41
+ const parts = prose.split(/^##\s+(?!#)/m);
42
+ for (let i = 1; i < parts.length; i++) {
43
+ const block = parts[i];
44
+ const nl = block.indexOf('\n');
45
+ const heading = (nl === -1 ? block : block.slice(0, nl)).trim().toLowerCase();
46
+ const body = nl === -1 ? '' : block.slice(nl + 1).trim();
47
+ sections[heading] = body;
48
+ }
49
+ return { tokens, prose: prose.trim(), sections };
50
+ }
51
+ /** Resolve a `{path.to.token}` reference against the token tree; returns the
52
+ * raw string for non-references unchanged. */
53
+ export function resolveRef(tokens, value) {
54
+ if (typeof value !== 'string')
55
+ return String(value ?? '');
56
+ const m = value.match(/^\{([^}]+)\}$/);
57
+ if (!m)
58
+ return value;
59
+ const path = m[1].split('.');
60
+ let cur = tokens;
61
+ for (const key of path) {
62
+ if (cur && typeof cur === 'object' && key in cur) {
63
+ cur = cur[key];
64
+ }
65
+ else {
66
+ return value; // unresolved — leave as-is for the linter to flag
67
+ }
68
+ }
69
+ return typeof cur === 'string' ? cur : value;
70
+ }
71
+ /** Normalize a CSS color for set-membership comparison: lowercase, trim, and
72
+ * expand `#rgb`/`#rgba` shorthand to the 6/8-digit form. */
73
+ export function normalizeColor(c) {
74
+ let s = c.trim().toLowerCase();
75
+ const short = s.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/);
76
+ if (short) {
77
+ const [, r, g, b, a] = short;
78
+ s = `#${r}${r}${g}${g}${b}${b}${a ? a + a : ''}`;
79
+ }
80
+ return s.replace(/\s+/g, '');
81
+ }
82
+ function dimStr(v) {
83
+ if (v === undefined || v === null)
84
+ return null;
85
+ if (typeof v === 'number')
86
+ return `${v}px`;
87
+ return String(v).trim();
88
+ }
89
+ /** Derive the flat allow-list from a parsed DESIGN.md. */
90
+ export function buildAllowList(parsed, designPath, projectDir) {
91
+ const t = parsed.tokens;
92
+ const colors = new Set();
93
+ const spacing = new Set();
94
+ const radii = new Set();
95
+ const fontSizes = new Set();
96
+ const fontFamilies = new Set();
97
+ for (const v of Object.values(t.colors ?? {})) {
98
+ const resolved = resolveRef(t, v);
99
+ if (resolved)
100
+ colors.add(normalizeColor(resolved));
101
+ }
102
+ for (const v of Object.values(t.spacing ?? {})) {
103
+ const d = dimStr(v);
104
+ if (d)
105
+ spacing.add(d);
106
+ }
107
+ for (const v of Object.values(t.rounded ?? {})) {
108
+ const d = dimStr(v);
109
+ if (d)
110
+ radii.add(d);
111
+ }
112
+ for (const ty of Object.values(t.typography ?? {})) {
113
+ if (ty?.fontSize)
114
+ fontSizes.add(dimStr(ty.fontSize) ?? '');
115
+ if (ty?.fontFamily)
116
+ fontFamilies.add(String(ty.fontFamily).trim().toLowerCase());
117
+ }
118
+ // Component tokens may inline literal colors/dimensions too.
119
+ for (const comp of Object.values(t.components ?? {})) {
120
+ for (const v of Object.values(comp ?? {})) {
121
+ const resolved = resolveRef(t, v);
122
+ if (/^#|^rgb|^hsl|^oklch|^oklab|^lab|^lch|^hwb|^color-mix/i.test(resolved.trim())) {
123
+ colors.add(normalizeColor(resolved));
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ name: t.name ?? 'Untitled',
129
+ colors: [...colors].filter(Boolean).sort(),
130
+ spacing: [...spacing].filter(Boolean).sort(),
131
+ radii: [...radii].filter(Boolean).sort(),
132
+ fontSizes: [...fontSizes].filter(Boolean).sort(),
133
+ fontFamilies: [...fontFamilies].filter(Boolean).sort(),
134
+ generatedFrom: relative(projectDir, designPath) || 'DESIGN.md',
135
+ };
136
+ }
137
+ export function allowListPath(projectDir) {
138
+ return join(projectDir, '.uap', 'design-tokens.json');
139
+ }
140
+ /** Write `.uap/design-tokens.json` (the gate's source of truth). */
141
+ export function writeAllowList(projectDir, allow) {
142
+ const out = allowListPath(projectDir);
143
+ mkdirSync(dirname(out), { recursive: true });
144
+ writeFileSync(out, JSON.stringify(allow, null, 2) + '\n');
145
+ return out;
146
+ }
147
+ /** Load + parse the project DESIGN.md, or null if none exists. */
148
+ export function loadDesign(projectDir) {
149
+ const p = findDesignFile(projectDir);
150
+ if (!p)
151
+ return null;
152
+ return { parsed: parseDesignMd(readFileSync(p, 'utf-8')), path: p };
153
+ }
154
+ /**
155
+ * Compact guidance string injected by the reactor when UI/UX work is detected.
156
+ * Keeps the agent on-token for new UI without re-reading the whole DESIGN.md.
157
+ */
158
+ export function summarizeForReactor(parsed, maxChars = 900) {
159
+ const t = parsed.tokens;
160
+ const lines = [];
161
+ lines.push(`Apply the project design system "${t.name ?? 'DESIGN.md'}". Use ONLY these tokens for new UI:`);
162
+ const colorRoles = Object.entries(t.colors ?? {})
163
+ .slice(0, 8)
164
+ .map(([k, v]) => `${k}=${resolveRef(t, v)}`);
165
+ if (colorRoles.length)
166
+ lines.push(` colors: ${colorRoles.join(', ')}`);
167
+ const fams = [...new Set(Object.values(t.typography ?? {}).map((x) => x.fontFamily).filter(Boolean))];
168
+ if (fams.length)
169
+ lines.push(` fonts: ${fams.join(', ')}`);
170
+ const sp = Object.values(t.spacing ?? {}).map((v) => (typeof v === 'number' ? `${v}px` : v));
171
+ if (sp.length)
172
+ lines.push(` spacing: ${sp.join(', ')}`);
173
+ const donts = parsed.sections["do's and don'ts"] || parsed.sections['dos and donts'];
174
+ if (donts) {
175
+ const firstDont = donts.split('\n').find((l) => /don'?t|avoid|never/i.test(l));
176
+ if (firstDont)
177
+ lines.push(` rule: ${firstDont.replace(/^[-*]\s*/, '').trim()}`);
178
+ }
179
+ lines.push(' Hardcoding off-token colors/spacing in UI files is BLOCKED by the design gate.');
180
+ let out = lines.join('\n');
181
+ if (out.length > maxChars)
182
+ out = out.slice(0, maxChars - 1) + '…';
183
+ return out;
184
+ }
185
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/design/tokens.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBjC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAqB5D"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuBjC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAuB5D"}
@@ -41,6 +41,8 @@ export function handleReact(args) {
41
41
  promptText: args.promptText,
42
42
  changedFiles: args.changedFiles,
43
43
  surfaced: args.surfaced,
44
+ // Locate the project DESIGN.md for design-system injection.
45
+ cwd: process.cwd(),
44
46
  };
45
47
  const opts = {
46
48
  injectThreshold: args.injectThreshold,
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,IAAI,cAAc,GAG1B,MAAM,+BAA+B,CAAC;AAavC,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,OAAO;IACb,WAAW,EAAE;;yVAE0U;IACvV,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;YAC5E,YAAY,EAAE;gBACZ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,iCAAiC;aAC/C;YACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yCAAyC,EAAE;YACjF,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,uEAAuE;aACrF;SACF;QACD,QAAQ,EAAE,CAAC,YAAY,CAAC;KACzB;CACF,CAAC;AAQF,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,GAAG,GAAmB;YAC1B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,aAAa;YAClC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;QACF,MAAM,IAAI,GAAmB;YAC3B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;IACzD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../../src/mcp-router/tools/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,OAAO,IAAI,cAAc,GAG1B,MAAM,+BAA+B,CAAC;AAavC,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,OAAO;IACb,WAAW,EAAE;;yVAE0U;IACvV,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+BAA+B,EAAE;YAC5E,YAAY,EAAE;gBACZ,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,iCAAiC;aAC/C;YACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yCAAyC,EAAE;YACjF,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,WAAW,EAAE,uEAAuE;aACrF;SACF;QACD,QAAQ,EAAE,CAAC,YAAY,CAAC;KACzB;CACF,CAAC;AAQF,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC;QACxD,CAAC;QACD,MAAM,GAAG,GAAmB;YAC1B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,aAAa;YAClC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,4DAA4D;YAC5D,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;SACnB,CAAC;QACF,MAAM,IAAI,GAAmB;YAC3B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;IACzD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.64.3",
3
+ "version": "1.65.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """design-token gate: UI edits must stay on the project's DESIGN.md token system.
3
+
4
+ A Write/Edit/MultiEdit to a UI file (.css/.scss/.tsx/.jsx/.vue/.svelte/.html/...)
5
+ whose proposed content introduces a hardcoded color or spacing value NOT present
6
+ in the project's design tokens is **blocked**. The agent must use a DESIGN.md
7
+ token (or a CSS var), add the value to DESIGN.md (`uap design sync`), or bypass.
8
+
9
+ The token allow-list is generated by `uap design interrogate|sync` into
10
+ `.uap/design-tokens.json` (so this enforcer needs no YAML parser). If that file
11
+ is absent, the gate is INACTIVE and fails open — DESIGN.md is opt-in per project.
12
+
13
+ Mirrors src/design/gate.ts. Escape hatch: UAP_DESIGN_GATE_OFF=1.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).parent))
24
+ from _common import emit, parse_cli, repo_root, worktree_root # noqa: E402
25
+
26
+ PATH_WRITE_OPS = {"Write", "Edit", "MultiEdit", "write", "edit", "multiedit"}
27
+ UI_EXT = {".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".vue", ".svelte", ".html", ".astro"}
28
+ SPACING_IGNORE = {"0px", "1px", "2px", "3px", "4px"}
29
+
30
+ HEX_RE = re.compile(r"#[0-9a-fA-F]{3,8}\b")
31
+ FUNC_COLOR_RE = re.compile(r"\b(?:rgba?|hsla?|hwb|oklch|oklab|lch|lab)\([^)]*\)", re.IGNORECASE)
32
+ PX_RE = re.compile(r"\b\d{1,4}px\b")
33
+ VAR_RE = re.compile(r"var\(--[^)]*\)")
34
+ # A DESIGN.md token reference is a dotted path in braces (e.g. {colors.primary}).
35
+ # Must NOT match CSS declaration blocks like `{color:#fff;}` (they contain
36
+ # colons/semicolons), or the scanner would strip whole rules before checking.
37
+ REF_RE = re.compile(r"\{[\w.-]+\}")
38
+
39
+
40
+ def _norm_color(c: str) -> str:
41
+ s = c.strip().lower()
42
+ m = re.match(r"^#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$", s)
43
+ if m:
44
+ r, g, b, a = m.groups()
45
+ s = f"#{r}{r}{g}{g}{b}{b}{(a + a) if a else ''}"
46
+ return re.sub(r"\s+", "", s)
47
+
48
+
49
+ def _allow_list() -> dict | None:
50
+ for root in (worktree_root(), repo_root()):
51
+ p = root / ".uap" / "design-tokens.json"
52
+ if p.exists():
53
+ try:
54
+ return json.loads(p.read_text())
55
+ except Exception: # noqa: BLE001
56
+ return None
57
+ return None
58
+
59
+
60
+ def _target_path(args: dict) -> str:
61
+ for key in ("file_path", "path", "notebook_path", "filePath", "target"):
62
+ if args.get(key):
63
+ return str(args[key])
64
+ return ""
65
+
66
+
67
+ def _proposed_content(op: str, args: dict) -> str:
68
+ # Write: full content. Edit: the inserted text. MultiEdit: all inserts.
69
+ if args.get("content"):
70
+ return str(args["content"])
71
+ if args.get("new_string"):
72
+ return str(args["new_string"])
73
+ edits = args.get("edits")
74
+ if isinstance(edits, list):
75
+ return "\n".join(str(e.get("new_string", "")) for e in edits if isinstance(e, dict))
76
+ return ""
77
+
78
+
79
+ def _scan(content: str, allow: dict) -> tuple[list[str], list[str]]:
80
+ allow_colors = {_norm_color(c) for c in allow.get("colors", [])}
81
+ allow_spacing = set(allow.get("spacing", [])) | set(allow.get("radii", [])) | set(allow.get("fontSizes", []))
82
+ off_colors: list[str] = []
83
+ off_spacing: list[str] = []
84
+
85
+ for raw in content.split("\n"):
86
+ t = raw.strip()
87
+ if t.startswith("//") or t.startswith("*") or t.startswith("/*"):
88
+ continue
89
+ line = REF_RE.sub("", VAR_RE.sub("", raw))
90
+ for m in HEX_RE.finditer(line):
91
+ if _norm_color(m.group(0)) not in allow_colors:
92
+ off_colors.append(m.group(0))
93
+ for m in FUNC_COLOR_RE.finditer(line):
94
+ if re.sub(r"\s+", "", m.group(0).lower()) not in allow_colors:
95
+ off_colors.append(m.group(0))
96
+ for m in PX_RE.finditer(line):
97
+ px = m.group(0)
98
+ if px not in SPACING_IGNORE and px not in allow_spacing:
99
+ off_spacing.append(px)
100
+
101
+ return off_colors, off_spacing
102
+
103
+
104
+ def main() -> None:
105
+ op, args = parse_cli()
106
+
107
+ if os.environ.get("UAP_DESIGN_GATE_OFF") == "1":
108
+ emit(True, "UAP_DESIGN_GATE_OFF override set")
109
+ if op not in PATH_WRITE_OPS:
110
+ emit(True, "not a file-write operation")
111
+
112
+ target = _target_path(args)
113
+ if Path(target).suffix.lower() not in UI_EXT:
114
+ emit(True, "not a UI file")
115
+
116
+ allow = _allow_list()
117
+ if not allow:
118
+ emit(True, "no DESIGN.md token allow-list (.uap/design-tokens.json) — gate inactive")
119
+
120
+ content = _proposed_content(op, args)
121
+ if not content:
122
+ emit(True, "no proposed content to scan")
123
+
124
+ off_colors, off_spacing = _scan(content, allow)
125
+ if not off_colors and not off_spacing:
126
+ emit(True, "on-token UI edit")
127
+
128
+ name = allow.get("name", "DESIGN.md")
129
+ palette = ", ".join(allow.get("colors", [])[:5])
130
+ spacing_scale = ", ".join(allow.get("spacing", []))
131
+ lines = [f'design-token gate: this UI edit introduces values not in the "{name}" design system.']
132
+ if off_colors:
133
+ uniq = list(dict.fromkeys(off_colors))[:6]
134
+ lines.append(f" off-token colors: {', '.join(uniq)}")
135
+ lines.append(f" → use a DESIGN.md color token ({palette}{', …' if len(allow.get('colors', [])) > 5 else ''}) or a CSS var.")
136
+ if off_spacing:
137
+ uniq = list(dict.fromkeys(off_spacing))[:6]
138
+ lines.append(f" off-scale spacing: {', '.join(uniq)}")
139
+ lines.append(f" → use a spacing token ({spacing_scale or 'define spacing in DESIGN.md'}).")
140
+ lines.append(" Add the value to DESIGN.md (then `uap design sync`), reference a token, or bypass with UAP_DESIGN_GATE_OFF=1.")
141
+ emit(False, "\n".join(lines))
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,38 @@
1
+ # design-token-gate
2
+
3
+ **Category**: quality
4
+ **Level**: REQUIRED
5
+ **Enforcement Stage**: pre-exec
6
+ **Tags**: design, ui, ux, tokens, design-system
7
+
8
+ ## Rule
9
+
10
+ A `Write`, `Edit`, or `MultiEdit` to a UI file (`.css`, `.scss`, `.tsx`, `.jsx`,
11
+ `.vue`, `.svelte`, `.html`, `.astro`) whose proposed content hardcodes a color
12
+ or spacing value that is NOT part of the project's DESIGN.md token system is
13
+ **blocked**. The agent must use an existing design token (or CSS var), add the
14
+ value to `DESIGN.md` and re-sync, or bypass explicitly.
15
+
16
+ On-token (allowed): values present in the token allow-list, `var(--…)` usages,
17
+ `{token.ref}` references, and structural hairlines (`0–4px`). The gate is
18
+ **inactive** (fails open) when a project has no `DESIGN.md` / token allow-list.
19
+
20
+ ## Why
21
+
22
+ DESIGN.md gives agents a persistent, structured understanding of a project's
23
+ visual identity. Without enforcement, new UI drifts off-system — ad-hoc hex
24
+ colors and off-scale spacing accumulate until the design system is fiction. The
25
+ gate keeps new UI on-token so the design system stays the real source of truth.
26
+
27
+ ## Enforcement
28
+
29
+ Python enforcer `design_token_gate.py` reads `.uap/design-tokens.json` (generated
30
+ by `uap design interrogate|sync` from the project DESIGN.md — no YAML parser
31
+ needed) and scans the proposed content for off-token colors/spacing. Mirrors
32
+ `src/design/gate.ts`. Escape hatch: `UAP_DESIGN_GATE_OFF=1`.
33
+
34
+ ```rules
35
+ - title: "New UI must use DESIGN.md design tokens, not hardcoded colors/spacing"
36
+ keywords: [css, color, theme, ui, ux, button, component, style, tailwind, design]
37
+ antiPatterns: ["#hex", "rgb(", "hardcoded color", "off-scale spacing"]
38
+ ```
@@ -1005,6 +1005,14 @@ class SessionMonitor:
1005
1005
  # back into a structured tool_use, which would continue the very loop the
1006
1006
  # breaker is ending. Reset to False at the start of every request.
1007
1007
  suppress_text_tool_extraction: bool = False
1008
+ # Tool-turn count at which the TURN-COUNT FINALIZE BREAKER last fired. The
1009
+ # count is derived from the (only-growing) conversation, so without this the
1010
+ # breaker would re-fire on EVERY turn once the ceiling is first crossed —
1011
+ # permanently stripping tools and stalling a legitimately long agentic task.
1012
+ # Gating on (last + ceiling) makes it a PERIODIC nudge (80, 160, 240, ...)
1013
+ # with tools restored in between, so long tasks complete while a true runaway
1014
+ # still gets bounded (and the contamination/prune/cycle breakers catch faster).
1015
+ last_hard_finalize_turn_count: int = 0
1008
1016
  finalize_continuation_count: int = 0
1009
1017
  finalize_hard_stop_count: int = 0 # monotonic, not reset by fresh user text
1010
1018
  finalize_synthetic_tool_id: str = ""
@@ -4095,7 +4103,14 @@ def build_openai_request(
4095
4103
  # genuine runaway trips it; see PROXY_HARD_FINALIZE_TURNS.
4096
4104
  if PROXY_HARD_FINALIZE_TURNS > 0:
4097
4105
  _agent_tool_turns = _count_agent_tool_turns(anthropic_body)
4098
- if _agent_tool_turns >= PROXY_HARD_FINALIZE_TURNS:
4106
+ # PERIODIC, not permanent: fire once each time the count crosses
4107
+ # another `ceiling` worth of tool turns past the last firing. Without
4108
+ # the `last + ceiling` gate this fires on EVERY turn past the first
4109
+ # crossing (the count only grows), permanently denying tools and
4110
+ # stalling a long-but-legitimate task. Between fires tools are restored
4111
+ # so the agent keeps making progress.
4112
+ if _agent_tool_turns >= monitor.last_hard_finalize_turn_count + PROXY_HARD_FINALIZE_TURNS:
4113
+ monitor.last_hard_finalize_turn_count = _agent_tool_turns
4099
4114
  openai_body.pop("tool_choice", None)
4100
4115
  openai_body.pop("tools", None)
4101
4116
  openai_body.pop("grammar", None)
@@ -4103,11 +4118,12 @@ def build_openai_request(
4103
4118
  msgs.append({
4104
4119
  "role": "user",
4105
4120
  "content": (
4106
- f"You have made {_agent_tool_turns} tool calls without "
4107
- "converging on a final answer. STOP now. No tools are "
4108
- "available this turn. Reply with a brief plain-text summary "
4109
- "of what you accomplished and what remains. Do NOT emit any "
4110
- "tool call or tool-call-like syntax."
4121
+ f"You have made {_agent_tool_turns} tool calls. Pause for a "
4122
+ "progress checkpoint: in a brief plain-text summary, state "
4123
+ "what is done and the single most important next step. No "
4124
+ "tools are available this turn do NOT emit any tool call "
4125
+ "or tool-call-like syntax. If the task is complete, say so; "
4126
+ "otherwise you will continue on the next turn."
4111
4127
  ),
4112
4128
  })
4113
4129
  openai_body["messages"] = msgs
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """The TURN-COUNT FINALIZE BREAKER must fire PERIODICALLY, not on every turn once
3
+ the ceiling is first crossed.
4
+
5
+ `_count_agent_tool_turns` is derived from the (only-growing) conversation, so the
6
+ naive `count >= ceiling` check re-fires on every request past the first crossing —
7
+ permanently stripping tools and stalling a legitimately long agentic task (observed
8
+ live: msgs 206→208→…→214, breaker every turn, then the client gives up). Gating on
9
+ `count >= last_hard_finalize_turn_count + ceiling` makes it fire at ceiling, 2x,
10
+ 3x… with tools restored in between.
11
+ """
12
+
13
+ import importlib.util
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+
18
+ def _load():
19
+ p = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
20
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", p)
21
+ m = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(m)
23
+ return m
24
+
25
+
26
+ proxy = _load()
27
+ CEILING = 80
28
+
29
+
30
+ def _fire_turns(counts):
31
+ """Replay the breaker's gating decision (as in build_openai_request) over a
32
+ sequence of cumulative tool-turn counts; return the counts at which it fired."""
33
+ m = proxy.SessionMonitor(context_window=100000)
34
+ fired = []
35
+ for count in counts:
36
+ if count >= m.last_hard_finalize_turn_count + CEILING:
37
+ m.last_hard_finalize_turn_count = count
38
+ fired.append(count)
39
+ return fired
40
+
41
+
42
+ class TestTurnCountBreakerPeriodic(unittest.TestCase):
43
+ def test_monitor_has_the_field_defaulting_zero(self):
44
+ m = proxy.SessionMonitor(context_window=100000)
45
+ self.assertEqual(m.last_hard_finalize_turn_count, 0)
46
+
47
+ def test_fires_periodically_not_every_turn(self):
48
+ # cumulative tool turns 1..244 (grows by 1 each request)
49
+ fired = _fire_turns(range(1, 245))
50
+ # periodic at the ceiling multiples — NOT every turn past 80
51
+ self.assertEqual(fired, [80, 160, 240])
52
+
53
+ def test_does_not_refire_between_crossings(self):
54
+ # turns 80..159 -> fires once at 80, then silent until 160
55
+ fired = _fire_turns(range(80, 160))
56
+ self.assertEqual(fired, [80])
57
+
58
+ def test_first_fire_at_ceiling(self):
59
+ self.assertEqual(_fire_turns(range(1, 81)), [80])
60
+ self.assertEqual(_fire_turns(range(1, 80)), []) # never reaches ceiling
61
+
62
+
63
+ if __name__ == "__main__":
64
+ unittest.main()