Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .agents/skills/validate-sas-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: validate-sas-release
description: Validate Super Agent Skill changes before publication, using the existing local checks without Lovable generation credits.
---

Run `npm ci` when locked dependencies changed. Run `npm run check:project` from the repository root. This batches tests, types, dependency audit, build, lint regression checking, content validation and both generated-artifact checks. It writes actual exit codes and logs to `artifacts/validation/`; failures block release.

Use `npm run check:project -- --security` with the existing Supabase publishable environment to include RLS and anonymous-access checks. Never expose credentials or weaken allowlists to pass. With a local dev server running, add `--ui` to test signup at mobile and desktop sizes with intercepted auth; review its screenshots personally.

`npm run lint:strict` retains all ESLint errors. The normal lint command permits only the recorded pre-existing explicit-any debt per file; it blocks increases and all other errors. Never increase that baseline to make a change pass. Report outstanding debt explicitly.

Check version-specific trust evidence, query-preserving auth redirects and MCP outcome semantics when those flows change. No model evaluations, signup creation, agent requests, provisioning, paid operations or production writes are part of this batch. Inspect and validate any additive SQL separately. Publish only after checks pass and publication is user-authorized; verify that Lovable's commit matches the validated code before using direct deployment. Preserve approval, signature and evidence gates.
18 changes: 5 additions & 13 deletions .claude/skills/brand/scripts/extract-colors.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ function colorDistance(color1, color2) {
if (!rgb1 || !rgb2) return Infinity;

return Math.sqrt(
Math.pow(rgb1.r - rgb2.r, 2) +
Math.pow(rgb1.g - rgb2.g, 2) +
Math.pow(rgb1.b - rgb2.b, 2)
Math.pow(rgb1.r - rgb2.r, 2) + Math.pow(rgb1.g - rgb2.g, 2) + Math.pow(rgb1.b - rgb2.b, 2),
);
}

Expand Down Expand Up @@ -235,12 +233,9 @@ function main() {
const jsonOutput = args.includes("--json");
const showPalette = args.includes("--palette");
const brandFileIdx = args.indexOf("--brand-file");
const brandFile =
brandFileIdx !== -1 ? args[brandFileIdx + 1] : DEFAULT_GUIDELINES_PATH;
const brandFile = brandFileIdx !== -1 ? args[brandFileIdx + 1] : DEFAULT_GUIDELINES_PATH;
const brandFileValue = brandFileIdx !== -1 ? args[brandFileIdx + 1] : null;
const imagePath = args.find(
(a) => !a.startsWith("--") && a !== brandFileValue
);
const imagePath = args.find((a) => !a.startsWith("--") && a !== brandFileValue);

// Load brand palette
const brandPalette = parseBrandColors(brandFile);
Expand Down Expand Up @@ -269,9 +264,7 @@ function main() {
}

// Resolve image path
const resolvedPath = path.isAbsolute(imagePath)
? imagePath
: path.join(process.cwd(), imagePath);
const resolvedPath = path.isAbsolute(imagePath) ? imagePath : path.join(process.cwd(), imagePath);

if (!fs.existsSync(resolvedPath)) {
console.error(`Image not found: ${resolvedPath}`);
Expand All @@ -297,8 +290,7 @@ function main() {
],
complianceCheck: {
threshold: 50,
description:
"Colors within distance 50 (RGB space) are considered brand-compliant",
description: "Colors within distance 50 (RGB space) are considered brand-compliant",
brandColors: brandPalette.all,
},
};
Expand Down
75 changes: 25 additions & 50 deletions .claude/skills/brand/scripts/inject-brand-context.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@ function extractColorsFromTable(content) {
};

// Find color tables
const primaryMatch = content.match(
/### Primary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i
);
const secondaryMatch = content.match(
/### Secondary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i
);
const neutralMatch = content.match(
/### Neutral[\s\S]*?\|[\s\S]*?(?=###|$)/i
);
const semanticMatch = content.match(
/### Semantic[\s\S]*?\|[\s\S]*?(?=###|$)/i
);
const primaryMatch = content.match(/### Primary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i);
const secondaryMatch = content.match(/### Secondary Colors[\s\S]*?\|[\s\S]*?(?=###|$)/i);
const neutralMatch = content.match(/### Neutral[\s\S]*?\|[\s\S]*?(?=###|$)/i);
const semanticMatch = content.match(/### Semantic[\s\S]*?\|[\s\S]*?(?=###|$)/i);

if (primaryMatch) colors.primary = extractHexColors(primaryMatch[0]);
if (secondaryMatch) colors.secondary = extractHexColors(secondaryMatch[0]);
Expand Down Expand Up @@ -103,25 +95,21 @@ function extractVoice(content) {
};

// Extract personality traits from table
const personalityMatch = content.match(
/### Brand Personality[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
);
const personalityMatch = content.match(/### Brand Personality[\s\S]*?\|[\s\S]*?(?=###|##|$)/i);
if (personalityMatch) {
const traits = personalityMatch[0].match(
/\*\*([^*]+)\*\*\s*\|\s*([^|]+)/g
);
const traits = personalityMatch[0].match(/\*\*([^*]+)\*\*\s*\|\s*([^|]+)/g);
if (traits) {
voice.traits = traits.map((t) => {
const match = t.match(/\*\*([^*]+)\*\*/);
return match ? match[1].trim() : "";
}).filter(Boolean);
voice.traits = traits
.map((t) => {
const match = t.match(/\*\*([^*]+)\*\*/);
return match ? match[1].trim() : "";
})
.filter(Boolean);
}
}

// Extract prohibited terms
const prohibitedMatch = content.match(
/### Prohibited[\s\S]*?(?=###|##|$)/i
);
const prohibitedMatch = content.match(/### Prohibited[\s\S]*?(?=###|##|$)/i);
if (prohibitedMatch) {
const terms = prohibitedMatch[0].match(/\|\s*([^|]+)\s*\|/g);
if (terms) {
Expand All @@ -132,15 +120,11 @@ function extractVoice(content) {
}

// Fallback: look for Forbidden Phrases
const forbiddenMatch = content.match(
/### Forbidden Phrases[\s\S]*?(?=###|##|$)/i
);
const forbiddenMatch = content.match(/### Forbidden Phrases[\s\S]*?(?=###|##|$)/i);
if (forbiddenMatch && voice.prohibited.length === 0) {
const items = forbiddenMatch[0].match(/-\s*["']?([^"'\n(]+)/g);
if (items) {
voice.prohibited = items
.map((item) => item.replace(/^-\s*["']?/, "").trim())
.filter(Boolean);
voice.prohibited = items.map((item) => item.replace(/^-\s*["']?/, "").trim()).filter(Boolean);
}
}

Expand All @@ -155,13 +139,9 @@ function extractVoice(content) {
function extractCoreAttributes(content) {
const attributes = [];

const attributesMatch = content.match(
/### Core Attributes[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
);
const attributesMatch = content.match(/### Core Attributes[\s\S]*?\|[\s\S]*?(?=###|##|$)/i);
if (attributesMatch) {
const rows = attributesMatch[0].match(
/\|\s*\*\*([^*]+)\*\*\s*\|\s*([^|]+)\|/g
);
const rows = attributesMatch[0].match(/\|\s*\*\*([^*]+)\*\*\s*\|\s*([^|]+)\|/g);
if (rows) {
rows.forEach((row) => {
const match = row.match(/\*\*([^*]+)\*\*\s*\|\s*([^|]+)/);
Expand Down Expand Up @@ -191,34 +171,31 @@ function extractImageStyle(content) {
};

// Extract base prompt template (content between ``` blocks after "Base Prompt Template")
const basePromptMatch = content.match(
/### Base Prompt Template[\s\S]*?```\n?([\s\S]*?)```/i
);
const basePromptMatch = content.match(/### Base Prompt Template[\s\S]*?```\n?([\s\S]*?)```/i);
if (basePromptMatch) {
imageStyle.basePrompt = basePromptMatch[1].trim().replace(/\n/g, " ");
}

// Extract style keywords from table
const keywordsMatch = content.match(
/### Style Keywords[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
);
const keywordsMatch = content.match(/### Style Keywords[\s\S]*?\|[\s\S]*?(?=###|##|$)/i);
if (keywordsMatch) {
const keywordRows = keywordsMatch[0].match(/\|\s*\*\*[^*]+\*\*\s*\|\s*([^|]+)\|/g);
if (keywordRows) {
keywordRows.forEach((row) => {
const match = row.match(/\|\s*\*\*[^*]+\*\*\s*\|\s*([^|]+)\|/);
if (match) {
const keywords = match[1].split(",").map((k) => k.trim()).filter(Boolean);
const keywords = match[1]
.split(",")
.map((k) => k.trim())
.filter(Boolean);
imageStyle.keywords.push(...keywords);
}
});
}
}

// Extract visual mood descriptors (bullet points)
const moodMatch = content.match(
/### Visual Mood Descriptors[\s\S]*?(?=###|##|$)/i
);
const moodMatch = content.match(/### Visual Mood Descriptors[\s\S]*?(?=###|##|$)/i);
if (moodMatch) {
const moodItems = moodMatch[0].match(/-\s*([^\n]+)/g);
if (moodItems) {
Expand All @@ -227,9 +204,7 @@ function extractImageStyle(content) {
}

// Extract visual don'ts from table
const dontsMatch = content.match(
/### Visual Don'ts[\s\S]*?\|[\s\S]*?(?=###|##|$)/i
);
const dontsMatch = content.match(/### Visual Don'ts[\s\S]*?\|[\s\S]*?(?=###|##|$)/i);
if (dontsMatch) {
const dontRows = dontsMatch[0].match(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/g);
if (dontRows) {
Expand Down
Loading
Loading