` orphaned after accept/discard. Single regex with
- // `[^>]*?` (which spans newlines in JS) handles either form correctly.
- const joined = lines.slice(start).join('\n');
- // Match either `
]*?(\/?)>|<\/div\s*>/g;
- let depth = 0;
- let m;
- while ((m = tagRe.exec(joined)) !== null) {
- const isClose = m[0].startsWith('');
- const isSelfClose = !isClose && m[1] === '/';
- if (isClose) depth--;
- else if (!isSelfClose) depth++;
- if (depth <= 0) {
- // m.index is offset within `joined`; convert back to a file line.
- const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
- const candidateEnd = start + linesBefore;
- if (candidateEnd >= end) {
- end = candidateEnd;
- break;
- }
- }
- }
-
- return { start, end };
-}
-
-function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-function isVariantEndMarkerLine(line, id) {
- return new RegExp('impeccable-variants-end\\s+' + escapeRegExp(id) + '(?:\\s|--|\\*/|$)').test(line);
-}
-
-function hasVariantWrapperAttr(line, id) {
- const escaped = escapeRegExp(id);
- return new RegExp(`data-impeccable-variants\\s*=\\s*(?:"${escaped}"|'${escaped}'|\\{["']${escaped}["']\\})`).test(line);
-}
-
-/**
- * Join wrapper lines into a single string with `` to close on)
- * - Same-line `` blocks
- * - Multi-line `` blocks
- */
-function stripStyleAndJoin(lines, block) {
- const out = [];
- let inStyle = false;
- for (let i = block.start; i <= block.end; i++) {
- let line = lines[i];
-
- if (!inStyle) {
- // Strip any complete .
- const closeIdx = line.search(/<\/style\s*>/);
- if (closeIdx !== -1) {
- inStyle = false;
- out.push(line.slice(closeIdx).replace(/<\/style\s*>/, ''));
- }
- // else: skip line entirely
- }
- }
- return out.join('\n');
-}
-
-/**
- * Find the inner content of `
…` inside `text`,
- * handling nested same-tag elements via depth counting. `attrMatch` is a
- * regex source fragment that must appear inside the opener tag.
- * Returns the inner string (may be empty), or null if not found.
- */
-function extractInnerByAttr(text, attrMatch) {
- const openerRe = new RegExp('<([A-Za-z][A-Za-z0-9]*)\\b[^>]*' + attrMatch + '[^>]*>');
- const openMatch = text.match(openerRe);
- if (!openMatch) return null;
-
- const tagName = openMatch[1];
- const innerStart = openMatch.index + openMatch[0].length;
-
- // Match any opener or closer of this tag name after innerStart.
- // (Does not match self-closing
, which doesn't contribute to depth.)
- const tagRe = new RegExp('<(?:/)?' + tagName + '\\b[^>]*>', 'g');
- tagRe.lastIndex = innerStart;
-
- let depth = 1;
- let m;
- while ((m = tagRe.exec(text))) {
- const isClose = m[0].startsWith('');
- const isSelfClose = !isClose && /\/\s*>$/.test(m[0]);
- if (isClose) {
- depth--;
- if (depth === 0) return text.slice(innerStart, m.index);
- } else if (!isSelfClose) {
- depth++;
- }
- }
- return null;
-}
-
-/**
- * Extract the original element content from within the variant wrapper.
- * Returns an array of lines.
- */
-function extractOriginal(lines, block) {
- const text = stripStyleAndJoin(lines, block);
- const inner = extractInnerByAttr(text, 'data-impeccable-variant="original"');
- if (inner === null) return [];
- return inner.split('\n');
-}
-
-/**
- * Extract a specific variant's inner content (stripping the wrapper div).
- * Returns an array of lines, or null if not found.
- */
-function extractVariant(lines, block, variantNum) {
- const text = stripStyleAndJoin(lines, block);
- const inner = extractInnerByAttr(text, 'data-impeccable-variant="' + variantNum + '"');
- if (inner === null) return null;
- const result = inner.split('\n');
- // Collapse a lone empty leading/trailing line (common after string splice).
- while (result.length > 1 && result[0].trim() === '') result.shift();
- while (result.length > 1 && result[result.length - 1].trim() === '') result.pop();
- return result.length > 0 ? result : null;
-}
-
-/**
- * Extract the colocated ` — return the inner content.
- * 3. Multi-line: `` on a later line — return
- * the lines between them.
- */
-function extractCss(lines, block, id) {
- const styleAttr = 'data-impeccable-css="' + id + '"';
- let inStyle = false;
- const content = [];
-
- for (let i = block.start; i <= block.end; i++) {
- const line = lines[i];
-
- if (!inStyle && line.includes(styleAttr)) {
- // Self-closing: nothing to carbonize.
- if (/ anywhere on the line — JSX template-literal closes
- // (`}`) put the close mid-line, and we don't want to absorb the
- // template-literal punctuation as CSS content.
- const closeIdx = line.indexOf('');
- if (closeIdx !== -1) break;
- content.push(line);
- }
- }
-
- if (content.length === 0) return null;
- return stripJsxTemplateLines(content);
-}
-
-/**
- * Strip a JSX template-literal wrap (`{` … `}`) from CSS extracted out of a
- * `',
- )
- .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
- const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
- return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
- })
- .replace(/\bclassName\s*=/g, 'class=')
- .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
- const css = jsxStyleObjectToCss(body);
- return css ? ' style="' + escapeHtml(css) + '"' : '';
- });
- }
-
- function jsxStyleObjectToCss(body) {
- const declarations = [];
- const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
- let match;
- while ((match = re.exec(String(body || '')))) {
- const prop = jsxStylePropToCss(match[1]);
- const value = match[2] ?? match[3] ?? match[4] ?? '';
- if (!prop || value === '') continue;
- declarations.push(prop + ': ' + value);
- }
- return declarations.join('; ');
- }
-
- function jsxStylePropToCss(prop) {
- let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
- if (!out) return '';
- if (out.startsWith('--')) return out;
- return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
- }
-
- function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
- const map = new Map();
- if (!sourceOriginal || !liveOriginal) return map;
-
- const sourceNodes = collectTextNodes(sourceOriginal)
- .filter((node) => /\{[^{}]+\}/.test(node.nodeValue || ''));
- const liveTexts = collectTextNodes(liveOriginal)
- .map((node) => normalizePreviewText(node.nodeValue || ''))
- .filter(Boolean);
- let liveIndex = 0;
-
- for (const sourceNode of sourceNodes) {
- const sourceText = sourceNode.nodeValue || '';
- const tokens = sourceText.match(/\{[^{}]+\}/g) || [];
- if (tokens.length === 0) continue;
-
- const liveText = liveTexts[liveIndex++] || '';
- if (!liveText) continue;
-
- if (tokens.length === 1) {
- const token = tokens[0];
- const normalizedSource = normalizePreviewText(sourceText);
- if (normalizedSource === token) {
- map.set(token, liveText);
- continue;
- }
-
- const match = liveText.match(expressionTextMatcher(sourceText, [token]));
- if (match && match[1]) map.set(token, match[1].trim());
- continue;
- }
-
- if (normalizePreviewText(sourceText) === tokens.join(' ')) {
- for (const token of tokens) {
- const tokenLiveText = liveTexts[liveIndex - 1] || '';
- if (tokenLiveText) map.set(token, tokenLiveText);
- }
- }
- }
-
- return map;
- }
-
- function expressionTextMatcher(sourceText, tokens) {
- let pattern = '^';
- let cursor = 0;
- for (const token of tokens) {
- const index = sourceText.indexOf(token, cursor);
- if (index === -1) continue;
- pattern += escapeRegExp(sourceText.slice(cursor, index)).replace(/\s+/g, '\\s*');
- pattern += '(.*?)';
- cursor = index + token.length;
- }
- pattern += escapeRegExp(sourceText.slice(cursor)).replace(/\s+/g, '\\s*') + '$';
- return new RegExp(pattern);
- }
-
- function collectTextNodes(root) {
- if (!root) return [];
- const nodes = [];
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
- let node = walker.nextNode();
- while (node) {
- nodes.push(node);
- node = walker.nextNode();
- }
- return nodes;
- }
-
- function normalizePreviewText(value) {
- return String(value || '').replace(/\s+/g, ' ').trim();
- }
-
- function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
-
- async function selectVariant(next, checkpointReason) {
- if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- if (variantSelectionInFlight) return;
- if (next < 1 || next > arrivedVariants) return;
- if (next === visibleVariant) return;
-
- const previous = visibleVariant;
- variantSelectionInFlight = true;
- const selectionPromise = (async () => {
- visibleVariant = next;
- showOrUpdateCyclingBar();
- saveSession();
- const shown = await showVariantInDOM(currentSessionId, next); // calls refreshParamsPanel itself
- if (!shown) {
- visibleVariant = previous;
- await showVariantInDOM(currentSessionId, previous);
- showOrUpdateCyclingBar();
- saveSession();
- return;
- }
- updateSelectedElement();
- showOrUpdateCyclingBar();
- positionBar();
- saveSession();
- if (checkpointReason) queueCheckpoint(checkpointReason);
- })();
- variantSelectionPromise = selectionPromise;
- try {
- await selectionPromise;
- } finally {
- if (variantSelectionPromise === selectionPromise) variantSelectionPromise = null;
- variantSelectionInFlight = false;
- }
- }
-
- function cycleVariant(dir) {
- selectVariant(visibleVariant + dir, 'variant_changed');
- }
-
- function updateSelectedElement() {
- if (!currentSessionId) return;
- if (svelteComponentSession?.sessionId === currentSessionId) {
- const anchor = resolveSvelteComponentAnchor();
- if (anchor && !anchor.__impeccableFrozenAnchor) selectedElement = anchor;
- return;
- }
- const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
- if (!wrapper) return;
- const visEl = pickVariantContent(wrapper, visibleVariant);
- if (visEl) selectedElement = visEl;
- }
-
- function readVisibleVariantFromDOM(sessionId) {
- if (svelteComponentSession?.sessionId === sessionId && svelteComponentSession.mountedVariant > 0) {
- return svelteComponentSession.mountedVariant;
- }
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
- if (!wrapper) return 0;
- const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
- for (const variant of variants) {
- if (!isVariantShown(variant)) continue;
- const idx = parseInt(variant.dataset.impeccableVariant || '0', 10);
- if (idx > 0) return idx;
- }
- return 0;
- }
-
- // Resolve the element that represents the variant's visible content.
- // Contract: each variant div should contain exactly one top-level element
- // (the full replacement). In practice a model may ship loose siblings or
- // lead with close.',
- 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.',
- 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.',
- ],
- forbidden: [
- 'Do not use @scope for this styleMode.',
- 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.',
- 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.',
- ],
- };
- }
- return {
- mode: styleMode.mode,
- styleTag: styleMode.styleTag,
- strategy: 'scope-rule',
- rulePattern: '@scope ([data-impeccable-variant="N"]) { :scope > .variant-class { ... } }',
- selectorExamples: variantNumbers.map((n) => `@scope ([data-impeccable-variant="${n}"]) { :scope > .variant-class { ... } }`),
- requirements: [
- 'Use @scope blocks keyed to each [data-impeccable-variant="N"] wrapper.',
- 'Inside each @scope block, make :scope rules step into the replacement element with a descendant combinator.',
- 'Use the styleTag exactly; do not add framework-specific style attributes unless this object says to.',
- ],
- forbidden: [
- 'Do not use global [data-impeccable-variant="N"] selector prefixes for this styleMode.',
- 'Do not add is:inline to the style tag for this styleMode.',
- ],
- };
-}
-
-/**
- * Search project files for the query string (class name, ID, etc.)
- * Returns the first matching file path, or null.
- *
- * Only `node_modules`, `.git`, and `.impeccable` are skipped outright.
- * dist/build/out are left to the isGeneratedFile guard so the
- * `includeGenerated` second pass can still find the element there and report
- * `generatedMatch`.
- */
-function findFileWithQuery(query, cwd, genOpts = {}) {
- return findSourceFile({
- query,
- cwd,
- extensions: resolveLiveTemplateExtensions(cwd),
- fileFilter: (filePath) => genOpts.includeGenerated || !isGeneratedFile(filePath, genOpts),
- });
-}
-
-/**
- * Regex that matches a tag opener on a line. Allows the tag name to be
- * followed by whitespace, `>`, `/`, or end-of-line so that multi-line JSX
- * openers (e.g. `
`) are recognised.
- */
-const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
-
-/**
- * Find the element's start and end line in the file.
- *
- * `query` is a class name, attribute fragment (`class="..."`, `className="..."`,
- * `id="..."`), or a raw text snippet. Because a query can appear on a
- * continuation line of a multi-line tag (e.g. the `className="..."` row of a
- * `` JSX tag), we walk backward from the match
- * line to find the actual tag opener. When `tag` is provided, opener candidates
- * must match that tag name.
- */
-/**
- * Return the smallest leading-whitespace count across a set of lines,
- * ignoring blank lines (whose indent isn't load-bearing). Used to compute
- * the common base indent of a multi-line picked element so reindenting
- * under the wrapper preserves the relative depth between lines.
- */
-function minLeadingSpaces(lines) {
- let min = Infinity;
- for (const l of lines) {
- if (l.trim() === '') continue;
- const m = l.match(/^(\s*)/);
- if (m && m[1].length < min) min = m[1].length;
- }
- return min === Infinity ? 0 : min;
-}
-
-function findElement(lines, query, tag = null) {
- // Iterate all matches — the first substring hit isn't always the right one.
- for (let i = 0; i < lines.length; i++) {
- if (!lines[i].includes(query)) continue;
-
- const stripped = lines[i].trim();
- if (stripped.startsWith(').
- */
-export function extractMustacheExpressions(text) {
- const expressions = [];
- const seen = new Set();
- const lines = String(text || '').split('\n');
- for (const line of lines) {
- const trimmed = line.trim();
- if (trimmed.startsWith('\n`
- : '';
- return `${buildPropsScript(contract)}${propsComment}${originalWithProps.trim()}\n\n\n`;
-}
-
-function buildInsertVariantStub(variantNum) {
- return `${buildPropsScript([])}Insert variant ${variantNum}
\n\n\n`;
-}
-
-export function scaffoldSvelteComponentSession({
- id,
- count,
- sourceFile,
- sourceStartLine,
- sourceEndLine,
- originalLines,
- cwd = process.cwd(),
-}) {
- ensureRuntimeHelper(cwd);
- const dir = componentSessionDir(id, cwd);
- fs.mkdirSync(dir, { recursive: true });
-
- const originalMarkup = originalLines.join('\n');
- const contract = buildPropContract(extractMustacheExpressions(originalMarkup));
- const originalWithProps = substituteExprsWithProps(originalMarkup, contract);
-
- const manifest = {
- id,
- previewMode: 'svelte-component',
- sourceFile: sourceFile.split(path.sep).join('/'),
- sourceStartLine,
- sourceEndLine,
- count,
- propContract: contract,
- originalMarkup,
- componentDir: path.relative(cwd, dir).split(path.sep).join('/'),
- runtimeModule: `/${SVELTE_RUNTIME_FILE}`,
- };
-
- fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
-
- for (let n = 1; n <= count; n++) {
- const variantFile = path.join(dir, `v${n}.svelte`);
- if (!fs.existsSync(variantFile)) {
- fs.writeFileSync(variantFile, buildVariantStub(n, originalWithProps, contract), 'utf-8');
- }
- }
-
- return {
- manifest,
- manifestFile: path.relative(cwd, path.join(dir, 'manifest.json')).split(path.sep).join('/'),
- componentDir: manifest.componentDir,
- propContract: contract,
- };
-}
-
-export function scaffoldSvelteComponentInsertSession({
- id,
- count,
- sourceFile,
- insertLine,
- position,
- anchorStartLine,
- anchorEndLine,
- anchorLines,
- cwd = process.cwd(),
-}) {
- ensureRuntimeHelper(cwd);
- const dir = componentSessionDir(id, cwd);
- fs.mkdirSync(dir, { recursive: true });
-
- const anchorMarkup = (anchorLines || []).join('\n');
- const manifest = {
- id,
- mode: 'insert',
- previewMode: 'svelte-component',
- sourceFile: sourceFile.split(path.sep).join('/'),
- insertLine,
- position,
- anchorStartLine,
- anchorEndLine,
- originalMarkup: anchorMarkup,
- anchorMarkup,
- count,
- propContract: [],
- componentDir: path.relative(cwd, dir).split(path.sep).join('/'),
- runtimeModule: `/${SVELTE_RUNTIME_FILE}`,
- };
-
- fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
-
- for (let n = 1; n <= count; n++) {
- const variantFile = path.join(dir, `v${n}.svelte`);
- if (!fs.existsSync(variantFile)) {
- fs.writeFileSync(variantFile, buildInsertVariantStub(n), 'utf-8');
- }
- }
-
- return {
- manifest,
- manifestFile: path.relative(cwd, path.join(dir, 'manifest.json')).split(path.sep).join('/'),
- componentDir: manifest.componentDir,
- propContract: [],
- };
-}
-
-export function findSvelteComponentManifest(id, cwd = process.cwd()) {
- const direct = manifestPathForSession(id, cwd);
- if (fs.existsSync(direct)) {
- return readManifest(direct);
- }
- const root = path.join(cwd, SVELTE_COMPONENT_ROOT);
- if (!fs.existsSync(root)) return null;
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
- if (!entry.isDirectory()) continue;
- const candidate = path.join(root, entry.name, 'manifest.json');
- if (!fs.existsSync(candidate)) continue;
- try {
- const manifest = readManifest(candidate);
- if (manifest?.id === id) return { ...manifest, manifestPath: candidate };
- } catch { /* skip */ }
- }
- return null;
-}
-
-export function readManifest(manifestPath) {
- const data = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
- return {
- ...data,
- manifestPath,
- };
-}
-
-export function resolveSourceFile(sourceFile, cwd = process.cwd()) {
- if (!sourceFile || path.isAbsolute(sourceFile)) {
- throw new Error('Invalid svelte-component source file');
- }
- const full = path.resolve(cwd, sourceFile);
- const rel = path.relative(cwd, full);
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
- throw new Error('Svelte-component source file escapes project root');
- }
- if (!fs.existsSync(full)) {
- throw new Error('Svelte-component source file not found: ' + sourceFile);
- }
- return full;
-}
-
-function appendCssToSvelteStyle(lines, cssLines) {
- const closeIdx = findLastStyleCloseLine(lines);
- const prepared = ['', ...cssLines.map((line) => (line.trim() === '' ? '' : ' ' + line.trimStart()))];
- if (closeIdx === -1) {
- return [...lines, '', ''];
- }
- return [
- ...lines.slice(0, closeIdx),
- ...prepared,
- ...lines.slice(closeIdx),
- ];
-}
-
-function findLastStyleCloseLine(lines) {
- for (let i = lines.length - 1; i >= 0; i--) {
- if (/<\/style\s*>/.test(lines[i])) return i;
- }
- return -1;
-}
-
-function bakeParamValuesInCss(cssLines, paramValues) {
- if (!paramValues || Object.keys(paramValues).length === 0) return cssLines;
- return cssLines.map((line) => {
- let out = line;
- for (const [key, value] of Object.entries(paramValues)) {
- const varName = `--p-${key}`;
- out = out.replace(new RegExp(`var\\(${escapeRegExp(varName)}(?:,\\s*[^)]+)?\\)`, 'g'), String(value));
- }
- return out;
- });
-}
-
-function sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues = null, rootTag = 'div') {
- const css = String((cssLines || []).join('\n'));
- if (!/data-impeccable-variant|impeccable-variant-ready/.test(css)) return cssLines;
-
- const rules = parseCssRules(css);
- const output = [];
- for (const rule of rules) {
- appendSanitizedCssRule(output, rule, variantNum, paramValues, rootTag);
- }
- return output.join('\n')
- .split('\n')
- .map((line) => line.trimEnd())
- .filter((line) => line.trim() !== '');
-}
-
-function appendSanitizedCssRule(output, rule, variantNum, paramValues, rootTag) {
- const prelude = rule.prelude.trim();
- const body = rule.body.trim();
- if (!prelude || !body || /--impeccable-variant-ready\s*:/.test(body)) return;
-
- if (/^@scope\b/i.test(prelude)) {
- if (/data-impeccable-variant/.test(prelude) && !selectorHasVariant(prelude, variantNum)) return;
- const inner = parseCssRules(body);
- for (const innerRule of inner) {
- const rewrittenPrelude = rewriteAcceptedSvelteSelector(innerRule.prelude, variantNum, paramValues, rootTag, true);
- if (!rewrittenPrelude || /--impeccable-variant-ready\s*:/.test(innerRule.body)) continue;
- output.push(formatCssRule(rewrittenPrelude, innerRule.body.trim()));
- }
- return;
- }
-
- const rewrittenPrelude = rewriteAcceptedSvelteSelector(prelude, variantNum, paramValues, rootTag, false);
- if (!rewrittenPrelude) return;
- output.push(formatCssRule(rewrittenPrelude, body));
-}
-
-function parseCssRules(css) {
- const rules = [];
- const text = String(css || '');
- let i = 0;
- while (i < text.length) {
- while (i < text.length && /\s/.test(text[i])) i++;
- const preludeStart = i;
- while (i < text.length && text[i] !== '{') i++;
- if (i >= text.length) break;
- const prelude = text.slice(preludeStart, i).trim();
- i++;
- const bodyStart = i;
- let depth = 1;
- let quote = null;
- let comment = false;
- while (i < text.length && depth > 0) {
- const ch = text[i];
- const next = text[i + 1];
- if (comment) {
- if (ch === '*' && next === '/') {
- comment = false;
- i += 2;
- continue;
- }
- i++;
- continue;
- }
- if (quote) {
- if (ch === '\\') {
- i += 2;
- continue;
- }
- if (ch === quote) quote = null;
- i++;
- continue;
- }
- if (ch === '/' && next === '*') {
- comment = true;
- i += 2;
- continue;
- }
- if (ch === '"' || ch === "'") {
- quote = ch;
- i++;
- continue;
- }
- if (ch === '{') depth++;
- else if (ch === '}') depth--;
- i++;
- }
- const body = text.slice(bodyStart, Math.max(bodyStart, i - 1));
- if (prelude) rules.push({ prelude, body });
- }
- return rules;
-}
-
-function rewriteAcceptedSvelteSelector(prelude, variantNum, paramValues, rootTag, fromScope) {
- const selectors = splitSelectorList(prelude);
- const rewritten = [];
- for (const selector of selectors) {
- const next = rewriteAcceptedSvelteSelectorPart(selector, variantNum, paramValues, rootTag, fromScope);
- if (next) rewritten.push(next);
- }
- return rewritten.join(', ');
-}
-
-function rewriteAcceptedSvelteSelectorPart(selector, variantNum, paramValues, rootTag, fromScope) {
- let out = selector.trim();
- const hasVariant = /data-impeccable-variant/.test(out);
- if (hasVariant && !selectorHasVariant(out, variantNum)) return '';
- if (hasVariant) {
- out = out.replace(variantSelectorRegex(variantNum), '');
- out = out.replace(/\[data-impeccable-variant=(["']).*?\1\]/g, '');
- }
-
- const paramResult = rewriteParamSelectors(out, paramValues);
- if (!paramResult.keep) return '';
- out = paramResult.selector;
-
- out = out
- .replace(/:scope(?:\[[^\]]+\])?\s*>\s*/g, '')
- .replace(/:scope(?:\[[^\]]+\])?/g, rootTag || '')
- .replace(/\s+/g, ' ')
- .trim();
-
- out = out.replace(/^[>+~]\s*/, '').trim();
- if (!out && (hasVariant || fromScope)) return rootTag || ':global(*)';
- return out;
-}
-
-function rewriteParamSelectors(selector, paramValues) {
- let keep = true;
- const next = selector.replace(/\[data-p-([A-Za-z0-9_-]+)(?:=(["'])(.*?)\2)?\]/g, (_match, key, _quote, expected) => {
- if (!paramValues || !Object.prototype.hasOwnProperty.call(paramValues, key)) return '';
- const actual = paramValues[key];
- if (expected != null && String(actual) !== String(expected)) {
- keep = false;
- return '';
- }
- if (expected == null && (actual === false || actual == null || actual === 'false' || actual === 'off' || actual === '0')) {
- keep = false;
- return '';
- }
- return '';
- });
- return { keep, selector: next };
-}
-
-function splitSelectorList(prelude) {
- const selectors = [];
- let start = 0;
- let bracket = 0;
- let paren = 0;
- let quote = null;
- for (let i = 0; i < prelude.length; i++) {
- const ch = prelude[i];
- if (quote) {
- if (ch === '\\') i++;
- else if (ch === quote) quote = null;
- continue;
- }
- if (ch === '"' || ch === "'") {
- quote = ch;
- continue;
- }
- if (ch === '[') bracket++;
- else if (ch === ']') bracket = Math.max(0, bracket - 1);
- else if (ch === '(') paren++;
- else if (ch === ')') paren = Math.max(0, paren - 1);
- else if (ch === ',' && bracket === 0 && paren === 0) {
- selectors.push(prelude.slice(start, i));
- start = i + 1;
- }
- }
- selectors.push(prelude.slice(start));
- return selectors;
-}
-
-function selectorHasVariant(selector, variantNum) {
- return variantSelectorRegex(variantNum).test(selector);
-}
-
-function variantSelectorRegex(variantNum) {
- return new RegExp(`\\[data-impeccable-variant=(["'])${escapeRegExp(String(variantNum))}\\1\\]`, 'g');
-}
-
-function formatCssRule(selector, body) {
- return `${selector} { ${body.trim()} }`;
-}
-
-function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-export function inlineSvelteComponentAccept(manifest, variantNum, paramValues = null, cwd = process.cwd()) {
- const sourceFile = resolveSourceFile(manifest.sourceFile, cwd);
- const variantPath = path.join(cwd, manifest.componentDir, `v${variantNum}.svelte`);
- const resultBase = {
- file: manifest.sourceFile,
- sourceFile: manifest.sourceFile,
- previewMode: 'svelte-component',
- componentDir: manifest.componentDir,
- carbonize: false,
- };
- if (!fs.existsSync(variantPath)) {
- return { handled: false, error: `Variant ${variantNum} not found`, ...resultBase };
- }
-
- const { markup, cssLines } = parseSvelteComponentFile(fs.readFileSync(variantPath, 'utf-8'));
- if (manifest.mode === 'insert') {
- return inlineSvelteComponentInsertAccept({
- manifest,
- markup,
- cssLines,
- variantNum,
- paramValues,
- sourceFile,
- resultBase,
- cwd,
- });
- }
-
- const rootTag = matchOpeningTag(markup)?.tag || 'div';
- const contract = manifest.propContract || [];
- const mergedMarkup = mergeOriginalTopLevelAttrs(markup, manifest.originalMarkup || '');
- const restoredMarkup = substitutePropsWithExprs(mergedMarkup, contract)
- .split('\n')
- .map((line) => line.trimEnd());
-
- const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
- const sourceLines = sourceContent.split('\n');
- const start = Number(manifest.sourceStartLine) - 1;
- const end = Number(manifest.sourceEndLine) - 1;
- if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end >= sourceLines.length) {
- return { handled: false, error: 'Invalid source line range for ' + manifest.sourceFile, ...resultBase };
- }
-
- const indent = sourceLines[start].match(/^(\s*)/)?.[1] || '';
- const indentedMarkup = restoredMarkup.map((line) => {
- if (line.trim() === '') return '';
- return indent + line.trimStart();
- });
-
- let newLines = [
- ...sourceLines.slice(0, start),
- ...indentedMarkup,
- ...sourceLines.slice(end + 1),
- ];
-
- const sanitizedCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag);
- const bakedCss = bakeParamValuesInCss(sanitizedCss, paramValues);
- if (bakedCss.length > 0) {
- newLines = appendCssToSvelteStyle(newLines, bakedCss);
- }
-
- try {
- fs.writeFileSync(sourceFile, newLines.join('\n'), 'utf-8');
- } catch (err) {
- return { handled: false, error: 'Failed to write Svelte source: ' + err.message, ...resultBase };
- }
- removeSvelteComponentSession(manifest.id, cwd);
-
- return {
- handled: true,
- ...resultBase,
- };
-}
-
-function inlineSvelteComponentInsertAccept({
- manifest,
- markup,
- cssLines,
- variantNum,
- paramValues,
- sourceFile,
- resultBase,
- cwd,
-}) {
- if (!svelteMarkupHasVisibleContent(markup)) {
- return { handled: false, error: 'Accepted Svelte insert variant is empty', ...resultBase };
- }
- if (/\bdata-impeccable-[\w-]*\s*=/.test(markup)) {
- return { handled: false, error: 'Accepted Svelte insert variant contains preview-only data-impeccable attributes', ...resultBase };
- }
-
- const rootTag = matchOpeningTag(markup)?.tag || 'div';
- const restoredMarkup = String(markup || '')
- .split('\n')
- .map((line) => line.trimEnd());
- const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
- const sourceLines = sourceContent.split('\n');
- const insertIndex = Number(manifest.insertLine) - 1;
- if (!Number.isInteger(insertIndex) || insertIndex < 0 || insertIndex > sourceLines.length) {
- return { handled: false, error: 'Invalid insert line for ' + manifest.sourceFile, ...resultBase };
- }
-
- const nearbyLine = sourceLines[insertIndex] ?? sourceLines[insertIndex - 1] ?? '';
- const indent = nearbyLine.match(/^(\s*)/)?.[1] || '';
- const indentedMarkup = restoredMarkup.map((line) => {
- if (line.trim() === '') return '';
- return indent + line.trimStart();
- });
-
- let newLines = [
- ...sourceLines.slice(0, insertIndex),
- ...indentedMarkup,
- ...sourceLines.slice(insertIndex),
- ];
-
- const sanitizedCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag);
- const bakedCss = bakeParamValuesInCss(sanitizedCss, paramValues);
- if (bakedCss.length > 0) {
- newLines = appendCssToSvelteStyle(newLines, bakedCss);
- }
-
- try {
- fs.writeFileSync(sourceFile, newLines.join('\n'), 'utf-8');
- } catch (err) {
- return { handled: false, error: 'Failed to write Svelte source: ' + err.message, ...resultBase };
- }
- removeSvelteComponentSession(manifest.id, cwd);
-
- return {
- handled: true,
- ...resultBase,
- };
-}
-
-function svelteMarkupHasVisibleContent(markup) {
- const text = String(markup || '')
- .replace(/\n\n` + out;
- }
- }
-
- if (!out.includes(SVELTE_LAYOUT_MARKER_OPEN)) {
- const block = `${SVELTE_LAYOUT_MARKER_OPEN}\n\n${SVELTE_LAYOUT_MARKER_CLOSE}\n`;
- const renderMatch = out.match(/\{@render\s+children(?:\?\.)?\(\)\s*\}/);
- const slotMatch = out.match(//);
- const match = renderMatch || slotMatch;
- if (match) {
- out = out.slice(0, match.index) + block + out.slice(match.index);
- } else {
- out = out.replace(/\s*$/, '\n\n' + block);
- }
- }
-
- return out;
-}
-
-export function unpatchSvelteLayout(content) {
- let out = String(content || '');
- const blockRe = new RegExp(
- '([ \\t]*)' + escapeRegExp(SVELTE_LAYOUT_MARKER_OPEN)
- + '\\n\\n'
- + escapeRegExp(SVELTE_LAYOUT_MARKER_CLOSE)
- + '\\n?',
- 'g',
- );
- out = out.replace(blockRe, '$1');
- out = out.replace(new RegExp('^\\s*' + escapeRegExp(SVELTE_ROOT_IMPORT) + '\\s*\\n?', 'gm'), '');
- out = out.replace(/
-`;
-}
-
-function findSvelteKitAppHtml(cwd, config) {
- const files = Array.isArray(config?.files) ? config.files : ['src/app.html'];
- for (const rel of files) {
- if (rel.includes('*')) continue;
- const normalized = rel.split(path.sep).join('/');
- if (!normalized.endsWith('app.html')) continue;
- const abs = path.join(cwd, normalized);
- if (fs.existsSync(abs)) return normalized;
- }
- const fallback = 'src/app.html';
- return fs.existsSync(path.join(cwd, fallback)) ? fallback : null;
-}
-
-function findSvelteKitLayout(cwd) {
- const candidates = [
- 'src/routes/+layout.svelte',
- 'src/routes/(app)/+layout.svelte',
- ];
- for (const rel of candidates) {
- if (fs.existsSync(path.join(cwd, rel))) return rel;
- }
- return 'src/routes/+layout.svelte';
-}
-
-function defaultSvelteLayout() {
- return `\n\n{@render children?.()}\n`;
-}
-
-function packageHasSvelteKit(cwd) {
- const file = path.join(cwd, 'package.json');
- if (!fs.existsSync(file)) return false;
- try {
- const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
- const deps = {
- ...(pkg.dependencies || {}),
- ...(pkg.devDependencies || {}),
- ...(pkg.peerDependencies || {}),
- };
- return Boolean(deps['@sveltejs/kit'] || deps['@sveltejs/vite-plugin-svelte'] || deps.svelte);
- } catch {
- return false;
- }
-}
-
-function fileIncludes(file, text) {
- try {
- return fs.readFileSync(file, 'utf-8').includes(text);
- } catch {
- return false;
- }
-}
-
-function pruneEmptyDir(dir, stopDir) {
- let current = dir;
- while (current.startsWith(stopDir) && current !== stopDir) {
- try {
- if (fs.readdirSync(current).length > 0) return;
- fs.rmdirSync(current);
- current = path.dirname(current);
- } catch {
- return;
- }
- }
-}
-
-function escapeRegExp(value) {
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
diff --git a/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs b/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs
deleted file mode 100644
index c682914..0000000
--- a/.agents/skills/impeccable/scripts/live/tanstack-adapter.mjs
+++ /dev/null
@@ -1,280 +0,0 @@
-/**
- * TanStack Start live-mode adapter.
- *
- * TanStack Start is SSR: there is no static index.html to patch. The document
- * shell is a React component (`shellComponent`/`component`) defined in the root
- * route file, `src/routes/__root.tsx`, which renders `…{children}
- * `.
- *
- * A raw ``;
-}
-
-const server = http.createServer((req, res) => {
- if (req.method === 'GET' && req.url === '/') {
- const pending = nextFile();
- if (pending && fs.existsSync(pending)) {
- try { loadRound(fs.readFileSync(pending, 'utf8')); fs.rmSync(pending); } catch { /* keep current round */ }
- }
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
- res.end(page());
- return;
- }
- if (req.method === 'POST' && req.url === '/heartbeat') {
- res.writeHead(204); res.end();
- if (detachedKey) {
- const now = Date.now();
- if (!server.lastBeatWrite || now - server.lastBeatWrite > 4000) {
- server.lastBeatWrite = now;
- try {
- const state = JSON.parse(fs.readFileSync(stateFile(detachedKey), 'utf8'));
- state.lastBeat = now;
- fs.writeFileSync(stateFile(detachedKey), JSON.stringify(state));
- } catch { /* state file recreated on next beat */ }
- }
- }
- return;
- }
- if (req.method === 'GET' && req.url === '/next-status') {
- const pending = nextFile();
- res.writeHead(200, { 'content-type': 'application/json' });
- res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
- return;
- }
- const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
- if (imageMatch) {
- const abs = localImages[Number(imageMatch[1])];
- if (!abs) { res.writeHead(404); res.end(); return; }
- const type = abs.endsWith('.webp') ? 'image/webp'
- : abs.endsWith('.png') ? 'image/png'
- : abs.endsWith('.svg') ? 'image/svg+xml'
- : abs.endsWith('.gif') ? 'image/gif'
- : 'image/jpeg';
- res.writeHead(200, { 'content-type': type });
- fs.createReadStream(abs).pipe(res);
- return;
- }
- if (req.method === 'POST' && req.url === '/answer') {
- let body = '';
- req.on('data', (chunk) => { body += chunk; });
- req.on('end', () => {
- res.writeHead(200, { 'content-type': 'application/json' });
- res.end('{"ok":true}');
- let parsed = {};
- try { parsed = JSON.parse(body); } catch { /* empty steer */ }
- const chosen = options.find((o) => o.id === parsed.optionId);
- const answer = JSON.stringify({
- optionId: parsed.optionId ?? null,
- steer: parsed.steer ?? '',
- ...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
- });
- const isReroll = parsed.optionId === 'reroll';
- if (detachedKey) {
- fs.mkdirSync(QUESTION_DIR, { recursive: true });
- fs.writeFileSync(answerFile(detachedKey), answer + '\n');
- } else {
- printAnswer(answer);
- }
- // A re-roll in detached mode keeps the table open: the client shows a
- // loading hand and reloads when --update delivers the next round.
- if (!(isReroll && detachedKey)) setTimeout(() => process.exit(0), 150);
- });
- return;
- }
- res.writeHead(404); res.end();
-});
-
-server.listen(portArg, '127.0.0.1', () => {
- const { port } = server.address();
- const url = `http://127.0.0.1:${port}/`;
- if (hasFlag('detached-serve')) {
- fs.mkdirSync(QUESTION_DIR, { recursive: true });
- fs.writeFileSync(stateFile(arg('key')), JSON.stringify({ pid: process.pid, port, url }));
- } else {
- console.log(`QUESTION URL: ${url}`);
- console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
- }
- if (!hasFlag('no-open')) {
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
- try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
- }
- if (timeoutSec > 0) {
- setTimeout(() => {
- console.log('serve-question: timed out with no answer');
- process.exit(2);
- }, timeoutSec * 1000).unref?.();
- }
-});
diff --git a/.agents/skills/impeccable/scripts/surface-brief.mjs b/.agents/skills/impeccable/scripts/surface-brief.mjs
deleted file mode 100644
index 723f7c1..0000000
--- a/.agents/skills/impeccable/scripts/surface-brief.mjs
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env node
-import fs from 'node:fs';
-import path from 'node:path';
-import { fileURLToPath, pathToFileURL } from 'node:url';
-import { resolveProjectRoot } from './context.mjs';
-import {
- listSurfaceBriefs,
- resolveSurfaceBrief,
- surfaceBriefPathForTarget,
- writeSurfaceBrief,
-} from './lib/surface-briefs.mjs';
-
-function summary(brief, projectRoot) {
- return {
- slug: brief.slug,
- path: path.relative(projectRoot, brief.path).split(path.sep).join('/'),
- primaryTarget: brief.primaryTarget,
- relatedTargets: brief.relatedTargets,
- };
-}
-
-function main(argv) {
- const [command, target, bodyFile, ...relatedTargets] = argv;
- const projectRoot = resolveProjectRoot(process.cwd(), target ? { targetPath: target } : {});
- if (command === 'path') {
- const filePath = surfaceBriefPathForTarget(target, { projectRoot });
- if (!filePath) throw new Error('surface brief path requires a concrete target');
- process.stdout.write(`${path.relative(process.cwd(), filePath) || filePath}\n`);
- return;
- }
- if (command === 'list') {
- process.stdout.write(`${JSON.stringify(listSurfaceBriefs(projectRoot).map((brief) => summary(brief, projectRoot)), null, 2)}\n`);
- return;
- }
- if (command === 'read') {
- const result = resolveSurfaceBrief(projectRoot, target || null);
- if (result.brief) {
- process.stdout.write(result.brief.text);
- return;
- }
- if (result.candidates.length) process.stderr.write(`${JSON.stringify(result.candidates.map((brief) => summary(brief, projectRoot)), null, 2)}\n`);
- process.exit(2);
- }
- if (command === 'write') {
- if (!target || !bodyFile) throw new Error('usage: surface-brief.mjs write ');
- const filePath = writeSurfaceBrief({
- projectRoot,
- primaryTarget: target,
- relatedTargets,
- body: fs.readFileSync(bodyFile, 'utf-8'),
- });
- process.stdout.write(`${path.relative(process.cwd(), filePath) || filePath}\n`);
- return;
- }
- throw new Error('usage: surface-brief.mjs [target] [body-file] [related-target ...]');
-}
-
-function isMainModule() {
- if (!process.argv[1]) return false;
- try {
- return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(process.argv[1]);
- } catch {
- return import.meta.url === pathToFileURL(process.argv[1]).href;
- }
-}
-
-if (isMainModule()) {
- try {
- main(process.argv.slice(2));
- } catch (error) {
- process.stderr.write(`${error?.message || error}\n`);
- process.exit(1);
- }
-}
diff --git a/.agents/skills/improve-codebase-architecture/HTML-REPORT.md b/.agents/skills/improve-codebase-architecture/HTML-REPORT.md
deleted file mode 100644
index 17f6d2c..0000000
--- a/.agents/skills/improve-codebase-architecture/HTML-REPORT.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# HTML Report Format
-
-The architectural review is rendered as a single self-contained HTML file in the OS temp directory. Tailwind and Mermaid both come from CDNs. Mermaid handles graph-shaped diagrams reliably; hand-built divs and inline SVG handle the more editorial visuals (mass diagrams, cross-sections). Mix the two — don't lean on Mermaid for everything, it'll start to look generic.
-
-## Scaffold
-
-```html
-
-
-
-
- Architecture review — {{repo name}}
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-## Header
-
-Repo name, date, and a compact legend: solid box = module, dashed line = seam, red arrow = leakage, thick dark box = deep module. No introduction paragraph — straight into the candidates.
-
-## Candidate card
-
-The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/codebase-design` skill) without ceremony.
-
-Each candidate is one ``:
-
-- **Title** — short, names the deepening (e.g. "Collapse the Order intake pipeline").
-- **Badge row** — recommendation strength (`Strong` = emerald, `Worth exploring` = amber, `Speculative` = slate), plus a tag for the dependency category (`in-process`, `local-substitutable`, `ports & adapters`, `mock`).
-- **Files** — monospaced list, `font-mono text-sm`.
-- **Before / After diagram** — the centrepiece. Two columns, side by side. See patterns below.
-- **Problem** — one sentence. What hurts.
-- **Solution** — one sentence. What changes.
-- **Wins** — bullets, ≤6 words each. e.g. "Tests hit one interface", "Pricing logic stops leaking", "Delete 4 shallow wrappers".
-- **ADR callout** (if applicable) — one line in an amber-tinted box.
-
-No paragraphs of explanation. If the diagram needs a paragraph to be understood, redraw the diagram.
-
-## Diagram patterns
-
-Pick the pattern that fits the candidate. Mix them. Don't make every diagram look the same — variety is part of the point.
-
-### Mermaid graph (the workhorse for dependencies / call flow)
-
-Use a Mermaid `flowchart` or `graph` when the point is "X calls Y calls Z, and look at the mess." Wrap it in a Tailwind-styled card so it doesn't feel parachuted in. Style with classDef to colour leakage edges red and the deep module dark. Sequence diagrams work well for "before: 6 round-trips; after: 1."
-
-```html
-
-
- flowchart LR
- A[OrderHandler] --> B[OrderValidator]
- B --> C[OrderRepo]
- C -.leak.-> D[PricingClient]
- classDef leak stroke:#dc2626,stroke-width:2px;
- class C,D leak
-
-
-```
-
-### Hand-built boxes-and-arrows (when Mermaid's layout fights you)
-
-Modules as ``s with borders and labels. Arrows as inline SVG `
` or `` elements positioned absolutely over a relative container. Reach for this when you want the "after" diagram to feel like one thick-bordered deep module with greyed-out internals — Mermaid won't render that with the right weight.
-
-### Cross-section (good for layered shallowness)
-
-Stack horizontal bands (`h-12 border-l-4`) to show layers a call passes through. Before: 6 thin layers each doing nothing. After: 1 thick band labelled with the consolidated responsibility.
-
-### Mass diagram (good for "interface as wide as implementation")
-
-Two rectangles per module — one for interface surface area, one for implementation. Before: interface rectangle is nearly as tall as the implementation rectangle (shallow). After: interface rectangle is short, implementation rectangle is tall (deep).
-
-### Call-graph collapse
-
-Before: a tree of function calls rendered as nested boxes. After: the same tree collapsed into one box, with the now-internal calls shown faded inside it.
-
-## Style guidance
-
-- Lean editorial, not corporate-dashboard. Generous whitespace. Serif optional for headings (`font-serif` works well with stone/slate).
-- Colour sparingly: one accent (emerald or indigo) plus red for leakage and amber for warnings.
-- Keep diagrams ~320px tall so before/after sits comfortably side by side without scrolling.
-- Use `text-xs uppercase tracking-wider` for module labels inside diagrams — they should read as schematic, not as UI.
-- The only scripts are the Tailwind CDN and the Mermaid ESM import. The report is otherwise static — no app code, no interactivity beyond Mermaid's own rendering.
-
-## Top recommendation section
-
-One larger card. Candidate name, one sentence on why, anchor link to its card. That's it.
-
-## Tone
-
-Plain English, concise — but the architectural nouns and verbs come straight from the `/codebase-design` skill. Concision is not an excuse to drift.
-
-**Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality.
-
-**Never substitute:** component, service, unit (for module) · API, signature (for interface) · boundary (for seam) · layer, wrapper (for module, when you mean module).
-
-**Phrasings that fit the style:**
-
-- "Order intake module is shallow — interface nearly matches the implementation."
-- "Pricing leaks across the seam."
-- "Deepen: one interface, one place to test."
-- "Two adapters justify the seam: HTTP in prod, in-memory in tests."
-
-**Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"* — those terms aren't in the glossary and don't earn their place.
-
-No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/codebase-design` glossary, reach for one that is before inventing a new one.
diff --git a/.agents/skills/improve-codebase-architecture/SKILL.md b/.agents/skills/improve-codebase-architecture/SKILL.md
deleted file mode 100644
index b56969e..0000000
--- a/.agents/skills/improve-codebase-architecture/SKILL.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-name: improve-codebase-architecture
-description: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
-disable-model-invocation: true
----
-
-# Improve Codebase Architecture
-
-Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
-
-This command is _informed_ by the project's domain model and built on a shared design vocabulary:
-
-- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion — don't drift into "component," "service," "API," or "boundary."
-- The domain language in `CONTEXT.md` gives names to good seams; ADRs in `docs/adr/` record decisions this command should not re-litigate.
-
-## Process
-
-### 1. Explore
-
-**Scope before you scan — YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look:
-
-- If the user named a direction — a module, a subsystem, a pain point — take it, and skip the inference below.
-- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots — the files and areas that keep coming up — and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net.
-
-Read the project's domain glossary (`CONTEXT.md`) and any ADRs in the area you're touching first.
-
-Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
-
-- Where does understanding one concept require bouncing between many small modules?
-- Where are modules **shallow** — interface nearly as complex as the implementation?
-- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
-- Where do tightly-coupled modules leak across their seams?
-- Which parts of the codebase are untested, or hard to test through their current interface?
-
-Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
-
-### 2. Present candidates as an HTML report
-
-Write a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `/architecture-review-.html` so each run gets a fresh file. Open it for the user — `xdg-open ` on Linux, `open ` on macOS, `start ` on Windows — and tell them the absolute path.
-
-The report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.
-
-For each candidate, render a card with:
-
-- **Files** — which files/modules are involved
-- **Problem** — why the current architecture is causing friction
-- **Solution** — plain English description of what would change
-- **Benefits** — explained in terms of locality and leverage, and how tests would improve
-- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening
-- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge
-
-End the report with a **Top recommendation** section: which candidate you'd tackle first and why.
-
-**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
-
-**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
-
-See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance.
-
-Do NOT propose interfaces yet. After the file is written, ask the user: "Which of these would you like to explore?"
-
-### 3. Grilling loop
-
-Once the user picks a candidate, run the `/grilling` skill to walk the decision tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
-
-Side effects happen inline as decisions crystallize — run the `/domain-modeling` skill to keep the domain model current as you go:
-
-- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist.
-- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
-- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones.
-- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.
diff --git a/.agents/skills/improve-codebase-architecture/agents/openai.yaml b/.agents/skills/improve-codebase-architecture/agents/openai.yaml
deleted file mode 100644
index 706fdca..0000000
--- a/.agents/skills/improve-codebase-architecture/agents/openai.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-interface:
- display_name: "Improve Codebase Architecture"
- short_description: "Find and grill architecture improvements"
-policy:
- allow_implicit_invocation: false
diff --git a/.agents/skills/stop-slop/CHANGELOG.md b/.agents/skills/stop-slop/CHANGELOG.md
deleted file mode 100644
index 64d9f54..0000000
--- a/.agents/skills/stop-slop/CHANGELOG.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Changelog
-
-## 2026-01-13
-
-### Added
-
-**Phrases (references/phrases.md)**
-- Throat-clearing: "Here's what I find interesting", "Here's the problem though"
-- Performative emphasis: "creeps in", "I promise", "They exist, I promise"
-- Telling instead of showing: "This is genuinely hard", "This is what leadership actually looks like"
-
-**Structures (references/structures.md)**
-- Binary contrasts: "Not X. But Y.", "It's not this. It's that.", "stops being X and starts being Y"
-- Rhythm patterns: staccato fragmentation, dashes for dramatic pause, hedging as reassurance
-- Word patterns: absolute words (always, never, everyone, etc.), AI-overused intensifiers (deeply, truly, fundamentally, inherently, simply, literally, inevitably)
-
-## 2026-01-12
-
-- Restructured skill following Claude Code best practices (PR #1)
-- Split into SKILL.md and references/ folder
-
-## 2025-01-12
-
-- Initial release
diff --git a/.agents/skills/stop-slop/LICENSE b/.agents/skills/stop-slop/LICENSE
deleted file mode 100644
index e0ede9b..0000000
--- a/.agents/skills/stop-slop/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Hardik Pandya
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/.agents/skills/stop-slop/README.md b/.agents/skills/stop-slop/README.md
deleted file mode 100644
index 3c98256..0000000
--- a/.agents/skills/stop-slop/README.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Stop Slop
-
-A skill for removing AI tells from prose.
-
-
-
-## What this is
-
-AI writing has patterns. Predictable phrases, structures, rhythms. This skill teaches Claude (or any LLM) to catch and remove them.
-
-## Skill Structure
-
-```
-stop-slop/
-├── SKILL.md # Core instructions
-├── references/
-│ ├── phrases.md # Phrases to remove
-│ ├── structures.md # Structural patterns to avoid
-│ └── examples.md # Before/after transformations
-├── README.md
-└── LICENSE
-```
-
-## Quick start
-
-**Claude Code:** Add this folder as a skill.
-
-**Claude Projects:** Upload `SKILL.md` and reference files to project knowledge.
-
-**Custom instructions:** Copy core rules from `SKILL.md`.
-
-**API calls:** Include `SKILL.md` in your system prompt. Reference files load on demand.
-
-## What it catches
-
-**Banned phrases** - Throat-clearing openers, emphasis crutches, business jargon, all adverbs, vague declaratives, meta-commentary. See `references/phrases.md`.
-
-**Structural clichés** - Binary contrasts, negative listings, dramatic fragmentation, rhetorical setups, false agency, narrator-from-a-distance voice, passive voice. See `references/structures.md`.
-
-**Sentence-level rules** - No Wh- sentence starters, no em dashes, no staccato fragmentation, no lazy extremes, active voice required.
-
-## Scoring
-
-Rate 1-10 on each dimension:
-
-| Dimension | Question |
-|-----------|----------|
-| Directness | Statements or announcements? |
-| Rhythm | Varied or metronomic? |
-| Trust | Respects reader intelligence? |
-| Authenticity | Sounds human? |
-| Density | Anything cuttable? |
-
-Below 35/50: revise.
-
-## Author
-
-[Hardik Pandya](https://hvpandya.com)
-
-## License
-
-MIT. Use freely, share widely.
diff --git a/.agents/skills/stop-slop/SKILL.md b/.agents/skills/stop-slop/SKILL.md
deleted file mode 100644
index 83f2032..0000000
--- a/.agents/skills/stop-slop/SKILL.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-name: stop-slop
-description: Remove AI writing patterns from prose. Use when drafting, editing, or reviewing text to eliminate predictable AI tells.
-metadata:
- trigger: Writing prose, editing drafts, reviewing content for AI patterns
- author: Hardik Pandya (https://hvpandya.com)
----
-
-# Stop Slop
-
-Eliminate predictable AI writing patterns from prose.
-
-## Core Rules
-
-1. **Cut filler phrases.** Remove throat-clearing openers, emphasis crutches, and all adverbs. See [references/phrases.md](references/phrases.md).
-
-2. **Break formulaic structures.** Avoid binary contrasts, negative listings, dramatic fragmentation, rhetorical setups, false agency. See [references/structures.md](references/structures.md).
-
-3. **Use active voice.** Every sentence needs a human subject doing something. No passive constructions. No inanimate objects performing human actions ("the complaint becomes a fix").
-
-4. **Be specific.** No vague declaratives ("The reasons are structural"). Name the specific thing. No lazy extremes ("every," "always," "never") doing vague work.
-
-5. **Put the reader in the room.** No narrator-from-a-distance voice. "You" beats "People." Specifics beat abstractions.
-
-6. **Vary rhythm.** Mix sentence lengths. Two items beat three. End paragraphs differently. No em dashes.
-
-7. **Trust readers.** State facts directly. Skip softening, justification, hand-holding.
-
-8. **Cut quotables.** If it sounds like a pull-quote, rewrite it.
-
-## Quick Checks
-
-Before delivering prose:
-
-- Any adverbs? Kill them.
-- Any passive voice? Find the actor, make them the subject.
-- Inanimate thing doing a human verb ("the decision emerges")? Name the person.
-- Sentence starts with a Wh- word? Restructure it.
-- Any "here's what/this/that" throat-clearing? Cut to the point.
-- Any "not X, it's Y" contrasts? State Y directly.
-- Three consecutive sentences match length? Break one.
-- Paragraph ends with punchy one-liner? Vary it.
-- Em-dash anywhere? Remove it.
-- Vague declarative ("The implications are significant")? Name the specific implication.
-- Narrator-from-a-distance ("Nobody designed this")? Put the reader in the scene.
-- Meta-joiners ("The rest of this essay...")? Delete. Let the essay move.
-
-## Scoring
-
-Rate 1-10 on each dimension:
-
-| Dimension | Question |
-|-----------|----------|
-| Directness | Statements or announcements? |
-| Rhythm | Varied or metronomic? |
-| Trust | Respects reader intelligence? |
-| Authenticity | Sounds human? |
-| Density | Anything cuttable? |
-
-Below 35/50: revise.
-
-## Examples
-
-See [references/examples.md](references/examples.md) for before/after transformations.
-
-## License
-
-MIT
diff --git a/.agents/skills/stop-slop/references/examples.md b/.agents/skills/stop-slop/references/examples.md
deleted file mode 100644
index 0376b6d..0000000
--- a/.agents/skills/stop-slop/references/examples.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Before/After Examples
-
-## Example 1: Throat-Clearing + Binary Contrast
-
-**Before:**
-> "Here's the thing: building products is hard. Not because the technology is complex. Because people are complex. Let that sink in."
-
-**After:**
-> "Building products is hard. Technology is manageable. People aren't."
-
-**Changes:** Removed opener, binary contrast structure, and emphasis crutch. Direct statements.
-
----
-
-## Example 2: Filler + Unnecessary Reassurance
-
-**Before:**
-> "It turns out that most teams struggle with alignment. The uncomfortable truth is that nobody wants to admit they're confused. And that's okay."
-
-**After:**
-> "Teams struggle with alignment. Nobody admits confusion."
-
-**Changes:** Cut hedging ("most"), removed throat-clearing phrases, deleted permission-granting ending.
-
----
-
-## Example 3: Business Jargon Stack
-
-**Before:**
-> "In today's fast-paced landscape, we need to lean into discomfort and navigate uncertainty with clarity. This matters because your competition isn't waiting."
-
-**After:**
-> "Move faster. Your competition is."
-
-**Changes:** Eliminated jargon entirely. Core message in six words.
-
----
-
-## Example 4: Dramatic Fragmentation
-
-**Before:**
-> "Speed. Quality. Cost. You can only pick two. That's it. That's the tradeoff."
-
-**After:**
-> "Speed, quality, cost—pick two."
-
-**Changes:** Single sentence. No performative emphasis.
-
----
-
-## Example 5: Rhetorical Setup
-
-**Before:**
-> "What if I told you that the best teams don't optimize for productivity? Here's what I mean: they optimize for learning. Think about it."
-
-**After:**
-> "The best teams optimize for learning, not productivity."
-
-**Changes:** Direct claim. No rhetorical scaffolding.
diff --git a/.agents/skills/stop-slop/references/phrases.md b/.agents/skills/stop-slop/references/phrases.md
deleted file mode 100644
index f9234fe..0000000
--- a/.agents/skills/stop-slop/references/phrases.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# Phrases to Remove
-
-## Throat-Clearing Openers
-
-Remove these announcement phrases. State the content directly.
-
-- "Here's the thing:"
-- "Here's what [X]"
-- "Here's this [X]"
-- "Here's that [X]"
-- "Here's why [X]"
-- "The uncomfortable truth is"
-- "It turns out"
-- "The real [X] is"
-- "Let me be clear"
-- "The truth is,"
-- "I'll say it again:"
-- "I'm going to be honest"
-- "Can we talk about"
-- "Here's what I find interesting"
-- "Here's the problem though"
-
-Any "here's what/this/that" construction is throat-clearing before the point. Cut it and state the point.
-
-## Emphasis Crutches
-
-These add no meaning. Delete them.
-
-- "Full stop." / "Period."
-- "Let that sink in."
-- "This matters because"
-- "Make no mistake"
-- "Here's why that matters"
-
-## Business Jargon
-
-Replace with plain language.
-
-| Avoid | Use instead |
-|-------|-------------|
-| Navigate (challenges) | Handle, address |
-| Unpack (analysis) | Explain, examine |
-| Lean into | Accept, embrace |
-| Landscape (context) | Situation, field |
-| Game-changer | Significant, important |
-| Double down | Commit, increase |
-| Deep dive | Analysis, examination |
-| Take a step back | Reconsider |
-| Moving forward | Next, from now |
-| Circle back | Return to, revisit |
-| On the same page | Aligned, agreed |
-
-## Adverbs
-
-Kill all adverbs. No -ly words. No softeners, no intensifiers, no hedges.
-
-Specific offenders:
-
-- "really"
-- "just"
-- "literally"
-- "genuinely"
-- "honestly"
-- "simply"
-- "actually"
-- "deeply"
-- "truly"
-- "fundamentally"
-- "inherently"
-- "inevitably"
-- "interestingly"
-- "importantly"
-- "crucially"
-
-Also cut these filler phrases:
-
-- "At its core"
-- "In today's [X]"
-- "It's worth noting"
-- "At the end of the day"
-- "When it comes to"
-- "In a world where"
-- "The reality is"
-
-## Meta-Commentary
-
-Remove self-referential asides. The essay should move, not announce its own structure.
-
-- "Hint:"
-- "Plot twist:" / "Spoiler:"
-- "You already know this, but"
-- "But that's another post"
-- "X is a feature, not a bug"
-- "Dressed up as"
-- "The rest of this essay explains..."
-- "Let me walk you through..."
-- "In this section, we'll..."
-- "As we'll see..."
-- "I want to explore..."
-
-## Performative Emphasis
-
-False intimacy or manufactured sincerity:
-
-- "creeps in"
-- "I promise"
-- "They exist, I promise"
-
-## Telling Instead of Showing
-
-Announcing difficulty or significance rather than demonstrating it:
-
-- "This is genuinely hard"
-- "This is what leadership actually looks like"
-- "This is what X actually looks like"
-- "actually matters"
-
-## Vague Declaratives
-
-Sentences that announce importance without naming the specific thing. Kill these.
-
-- "The reasons are structural"
-- "The implications are significant"
-- "This is the deepest problem"
-- "The stakes are high"
-- "The consequences are real"
-
-If a sentence says something is important/deep/structural without showing the specific thing, cut it or replace it with the specific thing.
diff --git a/.agents/skills/stop-slop/references/structures.md b/.agents/skills/stop-slop/references/structures.md
deleted file mode 100644
index bbcc359..0000000
--- a/.agents/skills/stop-slop/references/structures.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# Structures to Avoid
-
-## Binary Contrasts
-
-These create false drama. State the point directly.
-
-| Pattern | Problem |
-|---------|---------|
-| "Not because X. Because Y." / "Not because X, but because Y." | Telegraphed reversal |
-| "[X] isn't the problem. [Y] is." | Formulaic reframe |
-| "The answer isn't X. It's Y." | Predictable pivot |
-| "It feels like X. It's actually Y." | Setup/reveal cliche |
-| "The question isn't X. It's Y." | Rhetorical misdirection |
-| "Not X. But Y." / "not X, it's Y" / "isn't X, it's Y" | Mechanical contrast |
-| "It's not this. It's that." | Same formula, different words |
-| "stops being X and starts being Y" | False transformation arc |
-| "doesn't mean X, but actually Y" | Negation-then-assertion crutch |
-| "is about X but not Y" | False distinction |
-| "not just X but also Y" | Additive hedge |
-
-**Instead:** State Y directly. "The problem is Y." "Y matters here." Drop the negation entirely.
-
-## Negative Listing
-
-Listing what something is *not* before revealing what it *is*. A rhetorical striptease.
-
-| Pattern | Problem |
-|---------|---------|
-| "Not a X... Not a Y... A Z." | Dramatic buildup through negation |
-| "It wasn't X. It wasn't Y. It was Z." | Same structure, past tense |
-
-**Instead:** State Z. The reader doesn't need the runway.
-
-## Dramatic Fragmentation
-
-Sentence fragments for emphasis read as manufactured profundity.
-
-| Pattern | Problem |
-|---------|---------|
-| "[Noun]. That's it. That's the [thing]." | Performative simplicity |
-| "X. And Y. And Z." | Staccato drama |
-| "This unlocks something. [Word]." | Artificial revelation |
-
-**Instead:** Complete sentences. Trust content over presentation.
-
-## Rhetorical Setups
-
-These announce insight rather than deliver it.
-
-| Pattern | Problem |
-|---------|---------|
-| "What if [reframe]?" | Socratic posturing |
-| "Here's what I mean:" | Redundant preview |
-| "Think about it:" | Condescending prompt |
-| "And that's okay." | Unnecessary permission |
-
-**Instead:** Make the point. Let readers draw conclusions.
-
-## Formulaic Constructions
-
-| Pattern | Problem |
-|---------|---------|
-| "By the time X, I was Y." | Narrative template |
-| "X that isn't Y" | Indirect. Say "X is broken" |
-
-## False Agency
-
-Giving inanimate things human verbs. Complaints don't "become" fixes. Bets don't "live or die." Decisions don't "emerge." A person does something to make those things happen. AI loves this because it avoids naming the actor.
-
-| Pattern | Problem |
-|---------|---------|
-| "a complaint becomes a fix" | The complaint did nothing. Someone fixed it. |
-| "a bet lives or dies in days" | Bets don't have lifespans. Someone kills the project or ships it. |
-| "the decision emerges" | Decisions don't emerge. Someone decides. |
-| "the culture shifts" | Cultures don't shift on their own. People change behavior. |
-| "the conversation moves toward" | Conversations don't move. Someone steers. |
-| "the data tells us" | Data sits there. Someone reads it and draws a conclusion. |
-| "the market rewards" | Markets don't reward. Buyers pay for things. |
-
-**Instead:** Name the human. "The team fixed it that week" beats "the complaint becomes a fix." If no specific person fits, use "you" to put the reader in the seat.
-
-## Narrator-from-a-Distance
-
-Floating above the scene instead of putting the reader in it.
-
-| Pattern | Problem |
-|---------|---------|
-| "Nobody designed this." | Disembodied observation |
-| "This happens because..." | Lecturer voice |
-| "This is why..." | Same |
-| "People tend to..." | Armchair sociologist |
-
-**Instead:** Put the reader in the room. "You don't sit down one day and decide to..." beats "Nobody designed this."
-
-## Passive Voice
-
-Every sentence needs a subject doing something. Passive voice hides the actor and drains energy.
-
-| Pattern | Fix |
-|---------|-----|
-| "X was created" | Name who created it |
-| "It is believed that" | Name who believes it |
-| "Mistakes were made" | Name who made them |
-| "The decision was reached" | Name who decided |
-
-**Instead:** Find the actor. Put them at the front of the sentence.
-
-## Sentence Starters to Avoid
-
-| Pattern | Fix |
-|---------|-----|
-| Sentences starting with What, When, Where, Which, Who, Why, How | Restructure. Lead with the subject or the verb. |
-| Paragraphs starting with "So" | Start with content |
-| Sentences starting with "Look," | Remove |
-
-Wh- openers become a crutch. "What makes this hard is..." becomes "The constraint is..." or better, name the specific constraint.
-
-## Rhythm Patterns
-
-| Pattern | Fix |
-|---------|-----|
-| Three-item lists | Use two items or one |
-| Questions answered immediately | Let questions breathe or cut them |
-| Every paragraph ends punchily | Vary endings |
-| Em-dashes | Remove. Use commas or periods. No em dashes at all. |
-| Staccato fragmentation | Don't stack short punchy sentences |
-| "Not always. Not perfectly." | Hedging disguised as reassurance |
-
-## Word Patterns
-
-| Pattern | Problem |
-|---------|---------|
-| Lazy extremes (every, always, never, everyone, everybody, nobody) | False authority. Use specifics instead of sweeping claims. |
-| All adverbs (-ly words, "really," "just," "literally," "genuinely," "honestly," "simply," "actually") | Empty emphasis. See phrases.md for full list. |
diff --git a/.agents/skills/webapp-testing/LICENSE.txt b/.agents/skills/webapp-testing/LICENSE.txt
deleted file mode 100644
index 7a4a3ea..0000000
--- a/.agents/skills/webapp-testing/LICENSE.txt
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
\ No newline at end of file
diff --git a/.agents/skills/webapp-testing/SKILL.md b/.agents/skills/webapp-testing/SKILL.md
deleted file mode 100644
index 4726215..0000000
--- a/.agents/skills/webapp-testing/SKILL.md
+++ /dev/null
@@ -1,96 +0,0 @@
----
-name: webapp-testing
-description: Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
-license: Complete terms in LICENSE.txt
----
-
-# Web Application Testing
-
-To test local web applications, write native Python Playwright scripts.
-
-**Helper Scripts Available**:
-- `scripts/with_server.py` - Manages server lifecycle (supports multiple servers)
-
-**Always run scripts with `--help` first** to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window.
-
-## Decision Tree: Choosing Your Approach
-
-```
-User task → Is it static HTML?
- ├─ Yes → Read HTML file directly to identify selectors
- │ ├─ Success → Write Playwright script using selectors
- │ └─ Fails/Incomplete → Treat as dynamic (below)
- │
- └─ No (dynamic webapp) → Is the server already running?
- ├─ No → Run: python scripts/with_server.py --help
- │ Then use the helper + write simplified Playwright script
- │
- └─ Yes → Reconnaissance-then-action:
- 1. Navigate and wait for networkidle
- 2. Take screenshot or inspect DOM
- 3. Identify selectors from rendered state
- 4. Execute actions with discovered selectors
-```
-
-## Example: Using with_server.py
-
-To start a server, run `--help` first, then use the helper:
-
-**Single server:**
-```bash
-python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
-```
-
-**Multiple servers (e.g., backend + frontend):**
-```bash
-python scripts/with_server.py \
- --server "cd backend && python server.py" --port 3000 \
- --server "cd frontend && npm run dev" --port 5173 \
- -- python your_automation.py
-```
-
-To create an automation script, include only Playwright logic (servers are managed automatically):
-```python
-from playwright.sync_api import sync_playwright
-
-with sync_playwright() as p:
- browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
- page = browser.new_page()
- page.goto('http://localhost:5173') # Server already running and ready
- page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
- # ... your automation logic
- browser.close()
-```
-
-## Reconnaissance-Then-Action Pattern
-
-1. **Inspect rendered DOM**:
- ```python
- page.screenshot(path='/tmp/inspect.png', full_page=True)
- content = page.content()
- page.locator('button').all()
- ```
-
-2. **Identify selectors** from inspection results
-
-3. **Execute actions** using discovered selectors
-
-## Common Pitfall
-
-❌ **Don't** inspect the DOM before waiting for `networkidle` on dynamic apps
-✅ **Do** wait for `page.wait_for_load_state('networkidle')` before inspection
-
-## Best Practices
-
-- **Use bundled scripts as black boxes** - To accomplish a task, consider whether one of the scripts available in `scripts/` can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use `--help` to see usage, then invoke directly.
-- Use `sync_playwright()` for synchronous scripts
-- Always close the browser when done
-- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs
-- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()`
-
-## Reference Files
-
-- **examples/** - Examples showing common patterns:
- - `element_discovery.py` - Discovering buttons, links, and inputs on a page
- - `static_html_automation.py` - Using file:// URLs for local HTML
- - `console_logging.py` - Capturing console logs during automation
\ No newline at end of file
diff --git a/.agents/skills/webapp-testing/examples/console_logging.py b/.agents/skills/webapp-testing/examples/console_logging.py
deleted file mode 100644
index 9329b5e..0000000
--- a/.agents/skills/webapp-testing/examples/console_logging.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from playwright.sync_api import sync_playwright
-
-# Example: Capturing console logs during browser automation
-
-url = 'http://localhost:5173' # Replace with your URL
-
-console_logs = []
-
-with sync_playwright() as p:
- browser = p.chromium.launch(headless=True)
- page = browser.new_page(viewport={'width': 1920, 'height': 1080})
-
- # Set up console log capture
- def handle_console_message(msg):
- console_logs.append(f"[{msg.type}] {msg.text}")
- print(f"Console: [{msg.type}] {msg.text}")
-
- page.on("console", handle_console_message)
-
- # Navigate to page
- page.goto(url)
- page.wait_for_load_state('networkidle')
-
- # Interact with the page (triggers console logs)
- page.click('text=Dashboard')
- page.wait_for_timeout(1000)
-
- browser.close()
-
-# Save console logs to file
-with open('/mnt/user-data/outputs/console.log', 'w') as f:
- f.write('\n'.join(console_logs))
-
-print(f"\nCaptured {len(console_logs)} console messages")
-print(f"Logs saved to: /mnt/user-data/outputs/console.log")
\ No newline at end of file
diff --git a/.agents/skills/webapp-testing/examples/element_discovery.py b/.agents/skills/webapp-testing/examples/element_discovery.py
deleted file mode 100644
index 917ba72..0000000
--- a/.agents/skills/webapp-testing/examples/element_discovery.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from playwright.sync_api import sync_playwright
-
-# Example: Discovering buttons and other elements on a page
-
-with sync_playwright() as p:
- browser = p.chromium.launch(headless=True)
- page = browser.new_page()
-
- # Navigate to page and wait for it to fully load
- page.goto('http://localhost:5173')
- page.wait_for_load_state('networkidle')
-
- # Discover all buttons on the page
- buttons = page.locator('button').all()
- print(f"Found {len(buttons)} buttons:")
- for i, button in enumerate(buttons):
- text = button.inner_text() if button.is_visible() else "[hidden]"
- print(f" [{i}] {text}")
-
- # Discover links
- links = page.locator('a[href]').all()
- print(f"\nFound {len(links)} links:")
- for link in links[:5]: # Show first 5
- text = link.inner_text().strip()
- href = link.get_attribute('href')
- print(f" - {text} -> {href}")
-
- # Discover input fields
- inputs = page.locator('input, textarea, select').all()
- print(f"\nFound {len(inputs)} input fields:")
- for input_elem in inputs:
- name = input_elem.get_attribute('name') or input_elem.get_attribute('id') or "[unnamed]"
- input_type = input_elem.get_attribute('type') or 'text'
- print(f" - {name} ({input_type})")
-
- # Take screenshot for visual reference
- page.screenshot(path='/tmp/page_discovery.png', full_page=True)
- print("\nScreenshot saved to /tmp/page_discovery.png")
-
- browser.close()
\ No newline at end of file
diff --git a/.agents/skills/webapp-testing/examples/static_html_automation.py b/.agents/skills/webapp-testing/examples/static_html_automation.py
deleted file mode 100644
index 90bbedc..0000000
--- a/.agents/skills/webapp-testing/examples/static_html_automation.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from playwright.sync_api import sync_playwright
-import os
-
-# Example: Automating interaction with static HTML files using file:// URLs
-
-html_file_path = os.path.abspath('path/to/your/file.html')
-file_url = f'file://{html_file_path}'
-
-with sync_playwright() as p:
- browser = p.chromium.launch(headless=True)
- page = browser.new_page(viewport={'width': 1920, 'height': 1080})
-
- # Navigate to local HTML file
- page.goto(file_url)
-
- # Take screenshot
- page.screenshot(path='/mnt/user-data/outputs/static_page.png', full_page=True)
-
- # Interact with elements
- page.click('text=Click Me')
- page.fill('#name', 'John Doe')
- page.fill('#email', 'john@example.com')
-
- # Submit form
- page.click('button[type="submit"]')
- page.wait_for_timeout(500)
-
- # Take final screenshot
- page.screenshot(path='/mnt/user-data/outputs/after_submit.png', full_page=True)
-
- browser.close()
-
-print("Static HTML automation completed!")
\ No newline at end of file
diff --git a/.agents/skills/webapp-testing/scripts/with_server.py b/.agents/skills/webapp-testing/scripts/with_server.py
deleted file mode 100644
index 431f2eb..0000000
--- a/.agents/skills/webapp-testing/scripts/with_server.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-"""
-Start one or more servers, wait for them to be ready, run a command, then clean up.
-
-Usage:
- # Single server
- python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py
- python scripts/with_server.py --server "npm start" --port 3000 -- python test.py
-
- # Multiple servers
- python scripts/with_server.py \
- --server "cd backend && python server.py" --port 3000 \
- --server "cd frontend && npm run dev" --port 5173 \
- -- python test.py
-"""
-
-import subprocess
-import socket
-import time
-import sys
-import argparse
-
-def is_server_ready(port, timeout=30):
- """Wait for server to be ready by polling the port."""
- start_time = time.time()
- while time.time() - start_time < timeout:
- try:
- with socket.create_connection(('localhost', port), timeout=1):
- return True
- except (socket.error, ConnectionRefusedError):
- time.sleep(0.5)
- return False
-
-
-def main():
- parser = argparse.ArgumentParser(description='Run command with one or more servers')
- parser.add_argument('--server', action='append', dest='servers', required=True, help='Server command (can be repeated)')
- parser.add_argument('--port', action='append', dest='ports', type=int, required=True, help='Port for each server (must match --server count)')
- parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)')
- parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready')
-
- args = parser.parse_args()
-
- # Remove the '--' separator if present
- if args.command and args.command[0] == '--':
- args.command = args.command[1:]
-
- if not args.command:
- print("Error: No command specified to run")
- sys.exit(1)
-
- # Parse server configurations
- if len(args.servers) != len(args.ports):
- print("Error: Number of --server and --port arguments must match")
- sys.exit(1)
-
- servers = []
- for cmd, port in zip(args.servers, args.ports):
- servers.append({'cmd': cmd, 'port': port})
-
- server_processes = []
-
- try:
- # Start all servers
- for i, server in enumerate(servers):
- print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")
-
- # Use shell=True to support commands with cd and &&
- process = subprocess.Popen(
- server['cmd'],
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE
- )
- server_processes.append(process)
-
- # Wait for this server to be ready
- print(f"Waiting for server on port {server['port']}...")
- if not is_server_ready(server['port'], timeout=args.timeout):
- raise RuntimeError(f"Server failed to start on port {server['port']} within {args.timeout}s")
-
- print(f"Server ready on port {server['port']}")
-
- print(f"\nAll {len(servers)} server(s) ready")
-
- # Run the command
- print(f"Running: {' '.join(args.command)}\n")
- result = subprocess.run(args.command)
- sys.exit(result.returncode)
-
- finally:
- # Clean up all servers
- print(f"\nStopping {len(server_processes)} server(s)...")
- for i, process in enumerate(server_processes):
- try:
- process.terminate()
- process.wait(timeout=5)
- except subprocess.TimeoutExpired:
- process.kill()
- process.wait()
- print(f"Server {i+1} stopped")
- print("All servers stopped")
-
-
-if __name__ == '__main__':
- main()
\ No newline at end of file