diff --git a/.agents/skills/impeccable/scripts/context.mjs b/.agents/skills/impeccable/scripts/context.mjs
index cb16553c2..3afb81b99 100644
--- a/.agents/skills/impeccable/scripts/context.mjs
+++ b/.agents/skills/impeccable/scripts/context.mjs
@@ -200,7 +200,20 @@ export function resolveTargetSelection(cwd = process.cwd(), options = {}) {
function resolveProject(cwd = process.cwd(), options = {}) {
const absCwd = path.resolve(cwd);
const targetDir = resolveTargetDir(absCwd, options);
+ const hasExplicitTarget = hasTargetOption(options) && targetDir !== absCwd;
+ const targetGitRoot = hasExplicitTarget ? findGitBoundaryRoot(targetDir) : null;
let repoRoot = findMonorepoRoot(targetDir);
+ if (!repoRoot && targetGitRoot) {
+ const cwdGitRoot = findGitBoundaryRoot(absCwd);
+ if (targetGitRoot !== cwdGitRoot) {
+ return {
+ targetDir,
+ projectRoot: nearestTargetContextRoot(targetGitRoot, targetDir) || targetGitRoot,
+ repoRoot: targetGitRoot,
+ isMonorepo: false,
+ };
+ }
+ }
if (!repoRoot && targetDir !== absCwd) {
const cwdRepoRoot = findMonorepoRoot(absCwd);
if (cwdRepoRoot && isPathInside(targetDir, cwdRepoRoot)) {
@@ -208,6 +221,18 @@ function resolveProject(cwd = process.cwd(), options = {}) {
}
}
if (!repoRoot) {
+ const targetIsExternal = hasTargetOption(options)
+ && targetDir !== absCwd
+ && !isPathInside(targetDir, absCwd);
+ if (targetIsExternal) {
+ const targetRepoRoot = targetGitRoot || targetDir;
+ return {
+ targetDir,
+ projectRoot: nearestTargetContextRoot(targetRepoRoot, targetDir) || targetRepoRoot,
+ repoRoot: targetRepoRoot,
+ isMonorepo: false,
+ };
+ }
return {
targetDir,
projectRoot: nearestTargetContextRoot(absCwd, targetDir) || absCwd,
@@ -223,6 +248,18 @@ function resolveProject(cwd = process.cwd(), options = {}) {
};
}
+function findGitBoundaryRoot(startDir) {
+ let dir = path.resolve(startDir);
+ const homeDir = path.resolve(os.homedir());
+ while (true) {
+ if (dir === homeDir) return null;
+ if (hasGitBoundary(dir)) return dir;
+ const parent = path.dirname(dir);
+ if (parent === dir) return null;
+ dir = parent;
+ }
+}
+
function isPathInside(candidate, root) {
const rel = path.relative(root, candidate);
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
@@ -1313,6 +1350,39 @@ function hookEnabledAt(root) {
const STOP_REVIEW_PROVIDERS = new Set(['claude-code', 'codex', 'agents', 'grok']);
+// Harness project settings are discovered by walking up from the resolved
+// project root. Its hook manifest can live at an enclosing git root, so
+// checking only projectRoot produces a false MANUAL_DETECTOR_REQUIRED
+// directive. Starting from projectRoot also prevents an explicit target from
+// borrowing an unrelated manifest near the caller. The walk itself is the
+// authority: do not append repoRoot afterward, because resolveProject can
+// retain an outer workspace root for a target inside an independent nested
+// Git repository.
+function hookManifestSearchRoots(ctx) {
+ const roots = [];
+ const seen = new Set();
+ const add = (root) => {
+ if (!root) return;
+ const resolved = path.resolve(root);
+ if (seen.has(resolved)) return;
+ seen.add(resolved);
+ roots.push(resolved);
+ };
+
+ let current = path.resolve(ctx.projectRoot || process.cwd());
+ const home = path.resolve(os.homedir());
+ while (true) {
+ if (current === home) break;
+ add(current);
+ if (hasGitBoundary(current)) break;
+ const parent = path.dirname(current);
+ if (parent === current) break;
+ current = parent;
+ }
+
+ return roots;
+}
+
function automaticHookMode(ctx) {
if (ctx.platform === 'ios' || ctx.platform === 'android' || ctx.platform === 'adaptive') {
return 'none';
@@ -1320,8 +1390,10 @@ function automaticHookMode(ctx) {
const activeRoot = path.resolve(ctx.projectRoot || process.cwd());
if (!hookEnabledAt(activeRoot)) return 'none';
const manifests = HOOK_MANIFESTS_BY_PROVIDER[IMPECCABLE_PROVIDER_ID] || [];
- const roots = [...new Set([process.cwd(), ctx.projectRoot, ctx.repoRoot].filter(Boolean).map((root) => path.resolve(root)))];
- for (const root of roots) {
+ for (const root of hookManifestSearchRoots(ctx)) {
+ // A manifest can live above the resolved product. Honor the hook lifecycle
+ // config beside that manifest before treating it as active coverage.
+ if (!hookEnabledAt(root)) continue;
for (const rel of manifests) {
const raw = readJson(path.join(root, rel));
if (raw?.hooks && valueHasHookMarker(raw.hooks)) {
diff --git a/.agents/skills/impeccable/scripts/detect-csp.mjs b/.agents/skills/impeccable/scripts/detect-csp.mjs
index a13505d23..1a5664b4d 100644
--- a/.agents/skills/impeccable/scripts/detect-csp.mjs
+++ b/.agents/skills/impeccable/scripts/detect-csp.mjs
@@ -18,8 +18,9 @@
* Covers:
* - Inline Next.js headers() with CSP string
* - Nuxt routeRules / nitro.routeRules CSP headers
- * - "middleware": CSP set dynamically in middleware.{ts,js}.
- * Detected but not auto-patched in v1.
+ * - "middleware": CSP set dynamically in middleware.{ts,js,mjs} or
+ * Next.js 16's proxy.{ts,js,mjs} convention. Detected
+ * but not auto-patched in v1.
* - "meta-tag": in
* layout files. Detected but not auto-patched in v1.
* - null: no CSP signals found; no patch needed.
@@ -77,9 +78,57 @@ const NUXT_ROUTE_RULES_SIGNALS = [
/\bscript-src\b/,
];
+const NEXT_MIDDLEWARE_FILES = new Set([
+ 'middleware.ts',
+ 'middleware.js',
+ 'middleware.mjs',
+]);
+const NEXT_PROXY_FILES = new Set([
+ 'proxy.ts',
+ 'proxy.js',
+ 'proxy.mjs',
+]);
+const NEXT_CONFIG_FILES = [
+ 'next.config.js',
+ 'next.config.mjs',
+ 'next.config.cjs',
+ 'next.config.ts',
+ 'next.config.mts',
+ 'next.config.cts',
+];
const MIDDLEWARE_HINT = /headers\.set\(\s*["']Content-Security-Policy["']/i;
const META_TAG_HINT = /http-equiv\s*=\s*["']Content-Security-Policy["']/i;
+function hasNextProjectMarker(projectRoot) {
+ if (NEXT_CONFIG_FILES.some(name => fs.existsSync(path.join(projectRoot, name)))) return true;
+ if (['app', 'pages', 'src/app', 'src/pages'].some(rel => fs.existsSync(path.join(projectRoot, rel)))) return true;
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
+ return ['dependencies', 'devDependencies', 'peerDependencies']
+ .some(group => pkg?.[group] && Object.prototype.hasOwnProperty.call(pkg[group], 'next'));
+ } catch {
+ return false;
+ }
+}
+
+function isNextRequestHookFile(root, absPath, relPath, base) {
+ if (NEXT_MIDDLEWARE_FILES.has(base)) return true;
+ if (!NEXT_PROXY_FILES.has(base)) return false;
+ const normalized = relPath.split(path.sep).join('/').toLowerCase();
+ // Next.js 16 recognizes proxy at the project root or in the optional src/
+ // directory, alongside app/ or pages/. The scan root is commonly a
+ // monorepo, so also accept that placement relative to a nested directory
+ // that carries a concrete Next.js project marker. A same-named helper
+ // elsewhere in the tree is not the framework request hook.
+ if (normalized === base || normalized === `src/${base}`) return true;
+ const hookDir = path.dirname(absPath);
+ const projectRoot = path.basename(hookDir).toLowerCase() === 'src'
+ ? path.dirname(hookDir)
+ : hookDir;
+ if (path.resolve(projectRoot) === path.resolve(root)) return true;
+ return hasNextProjectMarker(projectRoot);
+}
+
/**
* @param {string} cwd Project root.
* @returns {{ shape: string|null, signals: string[] }}
@@ -133,8 +182,7 @@ export function detectCsp(cwd = process.cwd()) {
// === detect-only shapes ===
- if ((base === 'middleware.ts' || base === 'middleware.js' || base === 'middleware.mjs') &&
- MIDDLEWARE_HINT.test(body)) {
+ if (isNextRequestHookFile(cwd, absPath, relPath, base) && MIDDLEWARE_HINT.test(body)) {
hits.middleware.push(relPath);
}
diff --git a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs
index febf7f297..76fc558e5 100644
--- a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs
+++ b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs
@@ -1228,14 +1228,17 @@ if (IS_BROWSER) {
isHidden: isElementHidden(el),
findings: findings.map(f => {
const ap = ANTIPATTERNS.find(a => a.id === (f.type || f.id));
+ const severity = f.severity || ap?.severity || 'warning';
return {
type: f.type || f.id,
category: ap ? ap.category : 'quality',
- severity: f.severity || ap?.severity || 'warning',
+ severity,
// Advisory findings (em-dash overuse, etc.) are surfaced but never
// treated as failures; carry the flag so the overlay/extension can
// render them with the mildest affordance and consumers can filter.
- advisory: (ap && ap.advisory === true) || f.advisory === true,
+ // Per-finding promotions override the registry default, so derive
+ // this strictly from the effective severity.
+ advisory: severity === 'advisory',
detail: f.detail || f.snippet,
ignoreValue: f.ignoreValue || f.value || '',
name: ap ? ap.name : (f.type || f.id),
@@ -1277,6 +1280,381 @@ if (IS_BROWSER) {
else groupMap.set(el, [...kept]);
}
+ function pseudoElementHostSelector(selector) {
+ const raw = String(selector || '');
+ const legacyNames = new Set(['before', 'after', 'first-letter', 'first-line']);
+ const isNameChar = char => /[a-zA-Z0-9_-]/.test(char || '');
+ const consumeFunction = (start) => {
+ let depth = 0;
+ let quote = '';
+ for (let i = start; i < raw.length; i += 1) {
+ const char = raw[i];
+ if (char === '\\') {
+ i += 1;
+ continue;
+ }
+ if (quote) {
+ if (char === quote) quote = '';
+ continue;
+ }
+ if (char === '"' || char === "'") {
+ quote = char;
+ continue;
+ }
+ if (char === '(') depth += 1;
+ if (char === ')' && --depth === 0) return i + 1;
+ }
+ return raw.length;
+ };
+
+ let output = '';
+ let found = false;
+ for (let i = 0; i < raw.length;) {
+ const char = raw[i];
+ if (char === '\\') {
+ output += raw.slice(i, Math.min(raw.length, i + 2));
+ i += 2;
+ continue;
+ }
+ if (char === '"' || char === "'") {
+ const quote = char;
+ const start = i;
+ i += 1;
+ while (i < raw.length) {
+ if (raw[i] === '\\') {
+ i += 2;
+ continue;
+ }
+ const value = raw[i];
+ i += 1;
+ if (value === quote) break;
+ }
+ output += raw.slice(start, i);
+ continue;
+ }
+ if (char !== ':') {
+ output += char;
+ i += 1;
+ continue;
+ }
+
+ let end = i + 1;
+ let isPseudoElement = false;
+ if (raw[end] === ':') {
+ end += 1;
+ const nameStart = end;
+ while (isNameChar(raw[end])) end += 1;
+ isPseudoElement = end > nameStart;
+ } else {
+ const nameStart = end;
+ while (isNameChar(raw[end])) end += 1;
+ isPseudoElement = legacyNames.has(raw.slice(nameStart, end).toLowerCase());
+ }
+ if (!isPseudoElement) {
+ output += char;
+ i += 1;
+ continue;
+ }
+ if (raw[end] === '(') end = consumeFunction(end);
+ found = true;
+ if (!output || /[\s>+~,]/.test(output[output.length - 1])) output += '*';
+ i = end;
+ }
+ if (!found) return null;
+ return output.trim().replace(/,\s*(?=,|$)/g, '');
+ }
+
+ function selectorNodesForLiveDom(root, selector) {
+ const raw = String(selector || '').trim();
+ if (!raw) return null;
+ const fallback = pseudoElementHostSelector(raw);
+ if (fallback == null) {
+ // An empty result from a valid full selector is authoritative. In
+ // particular, do not broaden inactive :hover/:focus/:not() rules to
+ // their host element by stripping pseudo-classes.
+ try { return Array.from(root.querySelectorAll(raw)); }
+ catch { return null; }
+ }
+
+ // Resolve pseudo-elements to their originating live elements. An attached
+ // pseudo-element (`.card::before`) belongs to the element before it, while
+ // a hostless pseudo-element after a combinator (`main > ::before`) belongs
+ // to a matching element at that position (`main > *`). Replacing every
+ // pseudo indiscriminately with an empty string leaves the latter as the
+ // invalid selector `main >` and makes absent hosts indistinguishable from
+ // selectors the DOM API cannot parse.
+ if (!fallback || /^[,\s]*$/.test(fallback)) return null;
+ try { return Array.from(root.querySelectorAll(fallback)); }
+ catch { return null; }
+ }
+
+ let containerProbeSequence = 0;
+
+ function isContainerCssRule(rule) {
+ return rule?.constructor?.name === 'CSSContainerRule'
+ || /^\s*@container\b/i.test(rule?.cssText || '');
+ }
+
+ function styleRuleAppliesToLiveMatches(rule, matches) {
+ const style = rule?.style;
+ if (!style || !matches?.length || typeof getComputedStyle !== 'function') return false;
+ const sequence = ++containerProbeSequence;
+ const property = `--impeccable-container-probe-${sequence}-${Math.random().toString(36).slice(2)}`;
+ const value = `impeccable-container-active-${sequence}`;
+ const previousValue = style.getPropertyValue(property);
+ const previousPriority = style.getPropertyPriority(property);
+ try {
+ style.setProperty(property, value, 'important');
+ } catch {
+ return false;
+ }
+
+ const pseudoElements = [...new Set(
+ String(rule.selectorText || '').match(/::[a-zA-Z-]+(?:\([^)]*\))?/g) || [],
+ )];
+ try {
+ return matches.some(el => [null, ...pseudoElements].some(pseudo => {
+ try {
+ const computed = pseudo ? getComputedStyle(el, pseudo) : getComputedStyle(el);
+ return computed.getPropertyValue(property).trim() === value;
+ } catch {
+ return false;
+ }
+ }));
+ } finally {
+ if (previousValue) style.setProperty(property, previousValue, previousPriority);
+ else style.removeProperty(property);
+ }
+ }
+
+ function conditionalCssRuleIsActive(rule) {
+ const type = Number(rule?.type);
+ const constructorName = rule?.constructor?.name || '';
+ if (constructorName === 'CSSMediaRule' || type === 4) {
+ const condition = rule.conditionText || rule.media?.mediaText || '';
+ if (!condition || typeof window.matchMedia !== 'function') return true;
+ try { return window.matchMedia(condition).matches; }
+ catch { return true; }
+ }
+ if (constructorName === 'CSSSupportsRule' || type === 12) {
+ const condition = rule.conditionText || '';
+ if (!condition || typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return true;
+ try { return CSS.supports(condition); }
+ catch { return true; }
+ }
+ return true;
+ }
+
+ function splitCssCommaList(value) {
+ const parts = [];
+ let current = '';
+ let quote = '';
+ let escaped = false;
+ for (const char of String(value || '')) {
+ if (escaped) {
+ current += char;
+ escaped = false;
+ continue;
+ }
+ if (char === '\\') {
+ current += char;
+ escaped = true;
+ continue;
+ }
+ if (quote) {
+ current += char;
+ if (char === quote) quote = '';
+ continue;
+ }
+ if (char === '"' || char === "'") {
+ quote = char;
+ current += char;
+ continue;
+ }
+ if (char === ',') {
+ parts.push(current);
+ current = '';
+ continue;
+ }
+ current += char;
+ }
+ parts.push(current);
+ return parts;
+ }
+
+ function normalizeAnimationName(value) {
+ const name = String(value || '').trim();
+ if (name.length >= 2 && name[0] === name[name.length - 1] && (name[0] === '"' || name[0] === "'")) {
+ return name.slice(1, -1);
+ }
+ return name;
+ }
+
+ function animationNamesDeclaredByRule(rule) {
+ const style = rule?.style;
+ if (!style) return [];
+ let value = '';
+ try {
+ value = style.animationName
+ || style.getPropertyValue?.('animation-name')
+ || style.webkitAnimationName
+ || style.getPropertyValue?.('-webkit-animation-name')
+ || '';
+ } catch {
+ return [];
+ }
+ return splitCssCommaList(value)
+ .map(normalizeAnimationName)
+ .filter(name => name && name.toLowerCase() !== 'none');
+ }
+
+ function keyframesRuleName(rule, cssText) {
+ const constructorName = rule?.constructor?.name || '';
+ const type = Number(rule?.type);
+ const isKeyframes = constructorName === 'CSSKeyframesRule'
+ || constructorName === 'WebKitCSSKeyframesRule'
+ || type === 7
+ || /^\s*@(?:-webkit-)?keyframes\b/i.test(cssText);
+ if (!isKeyframes) return '';
+ const match = String(cssText || '').match(/^\s*@(?:-webkit-)?keyframes\s+([^\s{]+)/i);
+ return normalizeAnimationName(rule?.name || match?.[1] || '');
+ }
+
+ function cssPropertyName(property) {
+ if (property.startsWith('--')) return property;
+ return property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
+ }
+
+ function resolvedAnimationKeyframes(candidateNames) {
+ if (typeof document.getAnimations !== 'function') return null;
+ let animations;
+ try { animations = document.getAnimations(); }
+ catch { return null; }
+
+ const resolved = new Map();
+ const metadata = new Set(['offset', 'computedOffset', 'easing', 'composite']);
+ for (const animation of animations) {
+ const name = normalizeAnimationName(animation?.animationName || '');
+ if (!name || !candidateNames.has(name) || resolved.has(name)) continue;
+ let frames;
+ try { frames = animation.effect?.getKeyframes?.() || []; }
+ catch { continue; }
+ const blocks = [];
+ for (const frame of frames) {
+ const rawOffset = Number.isFinite(frame.computedOffset) ? frame.computedOffset : frame.offset;
+ if (!Number.isFinite(rawOffset)) continue;
+ const offset = Math.round(rawOffset * 1000000) / 10000;
+ const declarations = Object.entries(frame)
+ .filter(([property, value]) => !metadata.has(property) && value != null && value !== '')
+ .map(([property, value]) => `${cssPropertyName(property)}: ${value};`);
+ const easing = String(frame.easing || '').trim();
+ if (easing && easing.toLowerCase() !== 'linear') {
+ declarations.push(`animation-timing-function: ${easing};`);
+ }
+ if (declarations.length === 0) continue;
+ blocks.push(`${offset}% { ${declarations.join(' ')} }`);
+ }
+ if (blocks.length > 0) resolved.set(name, `@keyframes ${name} { ${blocks.join(' ')} }`);
+ }
+ return resolved;
+ }
+
+ // Read CSS that is absent from document.outerHTML. Inline