From dac3f77d894717349bd02238c0aee23dcb1c3952 Mon Sep 17 00:00:00 2001
From: Paul Bakaus
Date: Mon, 17 Aug 2026 21:10:08 -0700
Subject: [PATCH] Scripts dir: replace the Node scripts with the impeccable
launcher
skill/scripts keeps command-metadata.json and the page JS; every .mjs entry
point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX
launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION),
scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary
into skill/scripts/bin/-/, and gitignores that bin dir.
Prepared with AI assistance (Claude Code).
---
.gitignore | 6 +-
ENGINE_VERSION | 1 +
package.json | 1 +
scripts/fetch-engine.mjs | 137 +
skill/scripts/VERSION | 1 +
skill/scripts/concept-seed.mjs | 793 ------
skill/scripts/context-signals.mjs | 325 ---
skill/scripts/context.mjs | 1565 -----------
skill/scripts/critique-storage.mjs | 473 ----
skill/scripts/detect-csp.mjs | 198 --
skill/scripts/detect.mjs | 30 -
skill/scripts/doctor.mjs | 329 ---
skill/scripts/embed-prompt.mjs | 166 --
skill/scripts/generate-image.mjs | 447 ---
skill/scripts/hook-admin.mjs | 819 ------
skill/scripts/hook-before-edit.mjs | 538 ----
skill/scripts/hook-lib.mjs | 2490 -----------------
skill/scripts/hook.mjs | 79 -
skill/scripts/impeccable | 95 +
skill/scripts/impeccable.cmd | 44 +
skill/scripts/lib/artifact-schema.mjs | 93 -
skill/scripts/lib/composition-catalog.mjs | 200 --
skill/scripts/lib/concept-catalog.mjs | 396 ---
skill/scripts/lib/design-parser.mjs | 880 ------
skill/scripts/lib/impeccable-paths.mjs | 137 -
skill/scripts/lib/is-generated.mjs | 72 -
skill/scripts/lib/open-system-browser.mjs | 26 -
skill/scripts/lib/provider.mjs | 5 -
skill/scripts/lib/roll-selection.mjs | 369 ---
skill/scripts/lib/staleness-deep.mjs | 485 ----
skill/scripts/lib/staleness-notice.mjs | 169 --
skill/scripts/lib/staleness.mjs | 533 ----
skill/scripts/lib/surface-briefs.mjs | 149 -
skill/scripts/lib/target-args.mjs | 42 -
skill/scripts/lib/target-slug.mjs | 33 -
skill/scripts/lib/template-extensions.mjs | 146 -
skill/scripts/live-accept.mjs | 938 -------
skill/scripts/live-commit-manual-edits.mjs | 1200 --------
skill/scripts/live-complete.mjs | 107 -
skill/scripts/live-copy-edit-agent.mjs | 800 ------
skill/scripts/live-discard-manual-edits.mjs | 51 -
skill/scripts/live-inject.mjs | 463 ---
skill/scripts/live-insert.mjs | 292 --
skill/scripts/live-manual-edit-evidence.mjs | 368 ---
skill/scripts/live-poll.mjs | 430 ---
skill/scripts/live-resume.mjs | 123 -
skill/scripts/live-server.mjs | 1698 -----------
skill/scripts/live-status.mjs | 71 -
skill/scripts/live-target.mjs | 30 -
skill/scripts/live-wrap.mjs | 927 ------
skill/scripts/live.mjs | 334 ---
skill/scripts/live/accept-css.mjs | 617 ----
skill/scripts/live/accept-verify.mjs | 60 -
skill/scripts/live/browser-script-parts.mjs | 84 -
skill/scripts/live/completion.mjs | 28 -
skill/scripts/live/event-validation.mjs | 199 --
skill/scripts/live/frameworks/astro.mjs | 47 -
.../scripts/live/frameworks/detect-utils.mjs | 73 -
skill/scripts/live/frameworks/index.mjs | 143 -
skill/scripts/live/frameworks/journal.mjs | 197 --
skill/scripts/live/frameworks/nextjs.mjs | 49 -
skill/scripts/live/frameworks/nuxt.mjs | 161 --
skill/scripts/live/frameworks/script-src.mjs | 17 -
skill/scripts/live/frameworks/static-html.mjs | 26 -
skill/scripts/live/frameworks/sveltekit.mjs | 71 -
.../scripts/live/frameworks/tag-strategy.mjs | 247 --
.../live/frameworks/tanstack-start.mjs | 70 -
.../scripts/live/frameworks/vite-generic.mjs | 42 -
skill/scripts/live/generation-preflight.mjs | 149 -
skill/scripts/live/insert-ui.mjs | 458 ---
skill/scripts/live/instructions.mjs | 142 -
skill/scripts/live/manual-apply.mjs | 939 -------
skill/scripts/live/manual-edit-routes.mjs | 357 ---
skill/scripts/live/manual-edits-buffer.mjs | 152 -
skill/scripts/live/poll-lanes.mjs | 14 -
skill/scripts/live/roots.mjs | 508 ----
skill/scripts/live/session-store.mjs | 563 ----
skill/scripts/live/source-lock.mjs | 105 -
skill/scripts/live/source-search.mjs | 105 -
skill/scripts/live/svelte-ast.mjs | 969 -------
skill/scripts/live/svelte-component.mjs | 1366 ---------
skill/scripts/live/sveltekit-adapter.mjs | 304 --
skill/scripts/live/tanstack-adapter.mjs | 259 --
skill/scripts/live/ui-surfaces.mjs | 75 -
skill/scripts/live/vocabulary.mjs | 171 --
skill/scripts/palette.mjs | 628 -----
skill/scripts/pin.mjs | 224 --
skill/scripts/serve-question.mjs | 1783 ------------
skill/scripts/surface-brief.mjs | 74 -
89 files changed, 284 insertions(+), 31296 deletions(-)
create mode 100644 ENGINE_VERSION
create mode 100644 scripts/fetch-engine.mjs
create mode 100644 skill/scripts/VERSION
delete mode 100644 skill/scripts/concept-seed.mjs
delete mode 100644 skill/scripts/context-signals.mjs
delete mode 100644 skill/scripts/context.mjs
delete mode 100644 skill/scripts/critique-storage.mjs
delete mode 100644 skill/scripts/detect-csp.mjs
delete mode 100644 skill/scripts/detect.mjs
delete mode 100644 skill/scripts/doctor.mjs
delete mode 100644 skill/scripts/embed-prompt.mjs
delete mode 100644 skill/scripts/generate-image.mjs
delete mode 100644 skill/scripts/hook-admin.mjs
delete mode 100644 skill/scripts/hook-before-edit.mjs
delete mode 100644 skill/scripts/hook-lib.mjs
delete mode 100644 skill/scripts/hook.mjs
create mode 100755 skill/scripts/impeccable
create mode 100644 skill/scripts/impeccable.cmd
delete mode 100644 skill/scripts/lib/artifact-schema.mjs
delete mode 100644 skill/scripts/lib/composition-catalog.mjs
delete mode 100644 skill/scripts/lib/concept-catalog.mjs
delete mode 100644 skill/scripts/lib/design-parser.mjs
delete mode 100644 skill/scripts/lib/impeccable-paths.mjs
delete mode 100644 skill/scripts/lib/is-generated.mjs
delete mode 100644 skill/scripts/lib/open-system-browser.mjs
delete mode 100644 skill/scripts/lib/provider.mjs
delete mode 100644 skill/scripts/lib/roll-selection.mjs
delete mode 100644 skill/scripts/lib/staleness-deep.mjs
delete mode 100644 skill/scripts/lib/staleness-notice.mjs
delete mode 100644 skill/scripts/lib/staleness.mjs
delete mode 100644 skill/scripts/lib/surface-briefs.mjs
delete mode 100644 skill/scripts/lib/target-args.mjs
delete mode 100644 skill/scripts/lib/target-slug.mjs
delete mode 100644 skill/scripts/lib/template-extensions.mjs
delete mode 100644 skill/scripts/live-accept.mjs
delete mode 100644 skill/scripts/live-commit-manual-edits.mjs
delete mode 100644 skill/scripts/live-complete.mjs
delete mode 100644 skill/scripts/live-copy-edit-agent.mjs
delete mode 100755 skill/scripts/live-discard-manual-edits.mjs
delete mode 100644 skill/scripts/live-inject.mjs
delete mode 100644 skill/scripts/live-insert.mjs
delete mode 100644 skill/scripts/live-manual-edit-evidence.mjs
delete mode 100644 skill/scripts/live-poll.mjs
delete mode 100644 skill/scripts/live-resume.mjs
delete mode 100644 skill/scripts/live-server.mjs
delete mode 100644 skill/scripts/live-status.mjs
delete mode 100644 skill/scripts/live-target.mjs
delete mode 100644 skill/scripts/live-wrap.mjs
delete mode 100644 skill/scripts/live.mjs
delete mode 100644 skill/scripts/live/accept-css.mjs
delete mode 100644 skill/scripts/live/accept-verify.mjs
delete mode 100644 skill/scripts/live/browser-script-parts.mjs
delete mode 100644 skill/scripts/live/completion.mjs
delete mode 100644 skill/scripts/live/event-validation.mjs
delete mode 100644 skill/scripts/live/frameworks/astro.mjs
delete mode 100644 skill/scripts/live/frameworks/detect-utils.mjs
delete mode 100644 skill/scripts/live/frameworks/index.mjs
delete mode 100644 skill/scripts/live/frameworks/journal.mjs
delete mode 100644 skill/scripts/live/frameworks/nextjs.mjs
delete mode 100644 skill/scripts/live/frameworks/nuxt.mjs
delete mode 100644 skill/scripts/live/frameworks/script-src.mjs
delete mode 100644 skill/scripts/live/frameworks/static-html.mjs
delete mode 100644 skill/scripts/live/frameworks/sveltekit.mjs
delete mode 100644 skill/scripts/live/frameworks/tag-strategy.mjs
delete mode 100644 skill/scripts/live/frameworks/tanstack-start.mjs
delete mode 100644 skill/scripts/live/frameworks/vite-generic.mjs
delete mode 100644 skill/scripts/live/generation-preflight.mjs
delete mode 100644 skill/scripts/live/insert-ui.mjs
delete mode 100644 skill/scripts/live/instructions.mjs
delete mode 100644 skill/scripts/live/manual-apply.mjs
delete mode 100644 skill/scripts/live/manual-edit-routes.mjs
delete mode 100644 skill/scripts/live/manual-edits-buffer.mjs
delete mode 100644 skill/scripts/live/poll-lanes.mjs
delete mode 100644 skill/scripts/live/roots.mjs
delete mode 100644 skill/scripts/live/session-store.mjs
delete mode 100644 skill/scripts/live/source-lock.mjs
delete mode 100644 skill/scripts/live/source-search.mjs
delete mode 100644 skill/scripts/live/svelte-ast.mjs
delete mode 100644 skill/scripts/live/svelte-component.mjs
delete mode 100644 skill/scripts/live/sveltekit-adapter.mjs
delete mode 100644 skill/scripts/live/tanstack-adapter.mjs
delete mode 100644 skill/scripts/live/ui-surfaces.mjs
delete mode 100644 skill/scripts/live/vocabulary.mjs
delete mode 100644 skill/scripts/palette.mjs
delete mode 100644 skill/scripts/pin.mjs
delete mode 100644 skill/scripts/serve-question.mjs
delete mode 100644 skill/scripts/surface-brief.mjs
diff --git a/.gitignore b/.gitignore
index a111620a5..cb241e049 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,6 +83,11 @@ src/lib/impeccable/__runtime.js
# Extension build artifacts
extension/detector/
+# Engine binaries: fetched per platform (scripts/fetch-engine.mjs), never tracked.
+# The launcher next to them (skill/scripts/impeccable) is the tracked file.
+skill/scripts/bin/
+**/skills/impeccable/scripts/bin/
+
# Legacy design context (pre-v3.1, auto-migrated to PRODUCT.md by load-context.mjs)
.impeccable.md
# Note: PRODUCT.md and DESIGN.md are INTENTIONALLY tracked in this repo —
@@ -129,4 +134,3 @@ tmp/
# PNGs, the old card backup). The canonical generator is `bun run og-image`
# (scripts/generate-og-image.js); this dir is throwaway and safe to delete.
.og-build/
-tests/oracle/vectors/calls/
diff --git a/ENGINE_VERSION b/ENGINE_VERSION
new file mode 100644
index 000000000..6e8bf73aa
--- /dev/null
+++ b/ENGINE_VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/package.json b/package.json
index 6cdb56656..798bd190e 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
"build:browser": "node scripts/build-browser-detector.js",
"build:extension": "node scripts/build-extension.js",
+ "fetch:engine": "node scripts/fetch-engine.mjs",
"clean": "rm -rf dist build",
"rebuild": "bun run clean && bun run build",
"rebuild:release": "bun run clean && bun run build:release",
diff --git a/scripts/fetch-engine.mjs b/scripts/fetch-engine.mjs
new file mode 100644
index 000000000..0ea68ad31
--- /dev/null
+++ b/scripts/fetch-engine.mjs
@@ -0,0 +1,137 @@
+#!/usr/bin/env node
+/**
+ * Fetch the pinned engine binary (root ENGINE_VERSION) for one or every
+ * platform into skill/scripts/bin/-/impeccable[.exe], the sibling
+ * layout the launcher (skill/scripts/impeccable) looks in first.
+ *
+ * node scripts/fetch-engine.mjs # current platform
+ * node scripts/fetch-engine.mjs --all # every release target
+ * node scripts/fetch-engine.mjs --target linux-x64 [--target ...]
+ * node scripts/fetch-engine.mjs --dest # /-/impeccable[.exe]
+ * node scripts/fetch-engine.mjs --lenient # a target that cannot be fetched warns instead of failing
+ *
+ * Environment (same names the launcher honors):
+ * IMPECCABLE_DOWNLOAD_BASE release channel root (default: the public dist releases)
+ * IMPECCABLE_BIN copy this local binary for the current platform instead of downloading
+ *
+ * The URL scheme is the launcher's: /v/impeccable--[.exe],
+ * with an optional .sha256 next to it that is verified when present.
+ */
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { createHash } from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+export const DEFAULT_DOWNLOAD_BASE = 'https://github.com/renaissance-geek-inc/impeccable-dist/releases/download';
+export const ENGINE_TARGETS = ['darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', 'windows-x64'];
+
+export function readEngineVersion(root = ROOT) {
+ return fs.readFileSync(path.join(root, 'ENGINE_VERSION'), 'utf-8').trim();
+}
+
+export function currentTarget() {
+ const platform = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[os.platform()] || 'unknown';
+ const arch = { arm64: 'arm64', x64: 'x64' }[os.arch()] || 'unknown';
+ return `${platform}-${arch}`;
+}
+
+export function binaryName(target) {
+ return target.startsWith('windows-') ? 'impeccable.exe' : 'impeccable';
+}
+
+export function assetUrl(version, target, base = process.env.IMPECCABLE_DOWNLOAD_BASE || DEFAULT_DOWNLOAD_BASE) {
+ const asset = `impeccable-${target}${target.startsWith('windows-') ? '.exe' : ''}`;
+ return `${base.replace(/\/$/, '')}/v${version}/${asset}`;
+}
+
+export function binaryPath(target, dest = path.join(ROOT, 'skill', 'scripts', 'bin')) {
+ return path.join(dest, target, binaryName(target));
+}
+
+async function download(url) {
+ const res = await fetch(url, { redirect: 'follow' });
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`);
+ return Buffer.from(await res.arrayBuffer());
+}
+
+function install(buffer, target, dest) {
+ const out = binaryPath(target, dest);
+ fs.mkdirSync(path.dirname(out), { recursive: true });
+ const tmp = `${out}.part.${process.pid}`;
+ fs.writeFileSync(tmp, buffer);
+ fs.chmodSync(tmp, 0o755);
+ fs.renameSync(tmp, out);
+ return out;
+}
+
+/**
+ * Fetch one target. Returns the installed path. Throws when the asset is
+ * unavailable or its checksum does not match.
+ */
+export async function fetchEngine(target, { version = readEngineVersion(), dest, base } = {}) {
+ const local = process.env.IMPECCABLE_BIN;
+ if (local && target === currentTarget()) {
+ if (!fs.existsSync(local)) throw new Error(`IMPECCABLE_BIN points at a missing file: ${local}`);
+ return install(fs.readFileSync(local), target, dest);
+ }
+ const url = assetUrl(version, target, base);
+ const buffer = await download(url);
+ let checksum = null;
+ try {
+ checksum = (await download(`${url}.sha256`)).toString('utf-8').trim().split(/\s+/)[0];
+ } catch {
+ // No checksum published for this asset: accept the download as-is, like the launcher.
+ }
+ if (checksum) {
+ const actual = createHash('sha256').update(buffer).digest('hex');
+ if (actual !== checksum) throw new Error(`checksum mismatch for ${url}: expected ${checksum}, got ${actual}`);
+ }
+ return install(buffer, target, dest);
+}
+
+function parseArgs(argv) {
+ const opts = { targets: [], all: false, dest: undefined, lenient: false };
+ for (let i = 0; i < argv.length; i++) {
+ const a = argv[i];
+ if (a === '--all') opts.all = true;
+ else if (a === '--lenient') opts.lenient = true;
+ else if (a === '--target') opts.targets.push(argv[++i]);
+ else if (a === '--dest') opts.dest = path.resolve(argv[++i]);
+ else if (a === '--help' || a === '-h') { opts.help = true; }
+ else throw new Error(`Unknown argument: ${a}`);
+ }
+ return opts;
+}
+
+export async function main(argv = process.argv.slice(2)) {
+ const opts = parseArgs(argv);
+ if (opts.help) {
+ process.stdout.write('Usage: node scripts/fetch-engine.mjs [--all | --target ...] [--dest ] [--lenient]\n');
+ return 0;
+ }
+ const version = readEngineVersion();
+ const targets = opts.all ? ENGINE_TARGETS : opts.targets.length ? opts.targets : [currentTarget()];
+ let failures = 0;
+ for (const target of targets) {
+ if (!ENGINE_TARGETS.includes(target)) {
+ process.stderr.write(`fetch-engine: unsupported target ${target} (known: ${ENGINE_TARGETS.join(', ')})\n`);
+ failures++;
+ continue;
+ }
+ try {
+ const out = await fetchEngine(target, { version, dest: opts.dest });
+ process.stdout.write(`fetch-engine: ${target} v${version} -> ${path.relative(ROOT, out)}\n`);
+ } catch (err) {
+ const line = `fetch-engine: ${target} v${version} unavailable: ${err.message}\n`;
+ if (opts.lenient) process.stderr.write(`warning: ${line}`);
+ else { process.stderr.write(line); failures++; }
+ }
+ }
+ return failures ? 1 : 0;
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ main().then((code) => process.exit(code), (err) => { process.stderr.write(`fetch-engine: ${err.message}\n`); process.exit(1); });
+}
diff --git a/skill/scripts/VERSION b/skill/scripts/VERSION
new file mode 100644
index 000000000..6e8bf73aa
--- /dev/null
+++ b/skill/scripts/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/skill/scripts/concept-seed.mjs b/skill/scripts/concept-seed.mjs
deleted file mode 100644
index 991dc629b..000000000
--- a/skill/scripts/concept-seed.mjs
+++ /dev/null
@@ -1,793 +0,0 @@
-#!/usr/bin/env node
-/**
- * External concept seed: the dice half of new-work's complete-direction and
- * established-world surface procedures.
- *
- * Before this script runs, the model retrieves cultural material and derives
- * a grounded shortlist of complete candidate directions from it (see
- * reference/new-work.md). Left alone, it then always builds its #1 —
- * and a single model's resonance ranking is deterministic, so every run
- * in a category ships the same one or two concepts. Measured: 30/35
- * identical concepts across 16 prompt framings; the model cannot roll
- * its own dice.
- *
- * This script rolls them from outside, the same trick that made the
- * palette seed work:
- * - ASSIGNED INDEX: which entry of the model's own resonance-ordered
- * shortlist gets built. The assignment is the dice: it never chooses an
- * ungrounded ingredient, it only refuses the argmax rut. Attended runs
- * present the assigned direction and offer re-roll instead of a ranked
- * lineup, because a lineup hands selection back to a taste function
- * (model or user) and taste functions pick the safest card.
- * - CHALLENGERS (6): outside forms from concept-ingredients.json, two from
- * each challenger tier (graphic system, instrument language, atmosphere
- * world), fused with the product first (challenger supplies form and
- * system grammar, product supplies every fact, clarity wins conflicts),
- * then weighed against the derived candidates on audience identification
- * and product clarity. They win only when they beat the grounded list;
- * measured behavior is that they lose to strong cultural material and
- * win over thin categories, which is the intended shape.
- * - RE-ROLL (--reroll ): round n of the same base key. The script
- * recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a
- * fresh assigned index, challengers, and compositions. One base key therefore
- * reproduces the entire chain of rounds.
- * - REGISTER (--register safer|bolder): the user's steering on the
- * familiar-to-bold axis, applied to a re-roll round. A register changes
- * only what this round instructs, never what it dealt: the same key and
- * reroll count reproduce the same deal whatever the register, so the
- * exclusion chain never forks. bolder presents the dealt foreign forms
- * as the whole hand (first-dealt leads, dice-assigned by deal order);
- * safer spends the dealt hand unseen and presents the familiar register,
- * the model's conventional grounded candidates plus the canon against
- * named competitors, the one sanctioned lineup of the model's own list.
- * Registers are user-requested, never pre-selected by the model.
- * - RATINGS: the reviewer's approval ratings weight the challenger draw
- * (3-star doubles the odds, 1-star sits out); the approved pool itself
- * is unchanged.
- *
- * Usage:
- * node scripts/concept-seed.mjs --scope direction --mode persuade
- * node scripts/concept-seed.mjs --scope surface --mode operate --from
- * node scripts/concept-seed.mjs --scope surface --mode operate --grain flow
- * node scripts/concept-seed.mjs --scope direction --candidate-count 6
- * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1
- * node scripts/concept-seed.mjs --scope direction --mode persuade --from --reroll 1 --register bolder
- * node scripts/concept-seed.mjs --chosen --kind challenger --from --scope direction
- * node scripts/concept-seed.mjs --kind assigned --from --scope direction
- *
- * --grain names how much of the product is in play: product, flow, view, or
- * region. A docs site, an onboarding flow, a landing page and a data table are
- * four different amounts of product and want different compositions. Grain is a
- * preference: it deals matching compositions first and tops up from the rest of
- * the register, and the rendered seed says how many actually matched so a
- * borrowed structure is never mistaken for a supplied one.
- *
- * --platform names the delivery target (web, ios, android). Unlike grain this is
- * a hard filter: a composition that needs hover or a pointer does not degrade on
- * a phone, it stops working. --mode also gates which worlds are eligible, for
- * worlds whose reviewer marked them as carrying only some modes.
- *
- * --mode names the requested surface's mode (persuade, operate, read,
- * experience) so the appended compositions match its register of work; omitted,
- * they roll from the full approved pool.
- *
- * Challenger data resolves in order: a local catalog directory (the private
- * service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll
- * API at impeccable.style, then a degraded assignment-only seed when both are
- * unavailable. The anonymous choice ping fires once per resolved attended
- * round on API-dealt rolls: --kind names which card class won (assigned,
- * pick, challenger, canon) so share metrics have a denominator, --chosen
- * carries the catalog id when a dealt challenger won, and --register rides
- * along when the round came from a steered hand. Grounded candidates' names
- * never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables
- * the ping entirely.
- *
- * Env vars:
- * IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs.
- * IMPECCABLE_CATALOG_DIR — directory holding the four catalog JSON files.
- * IMPECCABLE_API_URL — roll API base (default https://impeccable.style/api).
- * IMPECCABLE_NO_TELEMETRY — disables the choice ping (DO_NOT_TRACK also honored).
- */
-
-import crypto from 'node:crypto';
-import { dirname, join, relative, resolve } from 'node:path';
-import { readFileSync } from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import {
- approvedPoolRevision,
- readConceptCatalog,
- validateConceptCatalog,
- WELL_TIERS,
-} from './lib/concept-catalog.mjs';
-import { readCompositionCatalog } from './lib/composition-catalog.mjs';
-import {
- COMPOSITION_GRAINS,
- COMPOSITION_PLATFORMS,
- runSyncSelection,
- selectApprovedChallengers as selectApprovedChallengersCore,
- selectApprovedCompositions as selectApprovedCompositionsCore,
-} from './lib/roll-selection.mjs';
-
-const here = dirname(fileURLToPath(import.meta.url));
-
-// Data resolution order: a local catalog (the private service repo, evals, and
-// tests point IMPECCABLE_CATALOG_DIR at one), then the roll API, then a
-// degraded assignment-only seed. The full catalog does not ship with the skill.
-const CATALOG_DIR = process.env.IMPECCABLE_CATALOG_DIR || here;
-const API_BASE = (process.env.IMPECCABLE_API_URL || 'https://impeccable.style/api').replace(/\/$/, '');
-const API_TIMEOUT_MS = Number(process.env.IMPECCABLE_API_TIMEOUT || 4000);
-// All API calls in one seed run share a single deadline so an unreachable
-// network degrades after one timeout total, never one timeout per call.
-let apiDeadline = null;
-function apiBudgetMs() {
- if (apiDeadline === null) apiDeadline = Date.now() + API_TIMEOUT_MS;
- return Math.max(0, apiDeadline - Date.now());
-}
-
-const localStates = new Map();
-function loadLocal(catalogDir = CATALOG_DIR) {
- if (localStates.has(catalogDir)) return localStates.get(catalogDir);
- let localState;
- try {
- const catalogState = readConceptCatalog(
- join(catalogDir, 'concept-ingredients.json'),
- join(catalogDir, 'concept-reviews.json')
- );
- const validation = validateConceptCatalog(catalogState.catalog, catalogState.reviewData);
- if (validation.errors.length > 0) {
- throw new Error(`invalid catalog: ${validation.errors.join('; ')}`);
- }
- const compositionState = readCompositionCatalog(
- join(catalogDir, 'composition-ingredients.json'),
- join(catalogDir, 'composition-reviews.json')
- );
- localState = {
- concepts: catalogState.concepts,
- compositions: compositionState.compositions,
- };
- } catch {
- localState = null;
- }
- localStates.set(catalogDir, localState);
- return localState;
-}
-
-function requireLocalConcepts() {
- const local = loadLocal();
- if (!local) {
- throw new Error('concept-seed: no local catalog (set IMPECCABLE_CATALOG_DIR or pass sourceConcepts)');
- }
- return local;
-}
-
-async function fetchRoll({ scope, key, mode, grain, platform, reroll }) {
- const params = new URLSearchParams({ scope, key, reroll: String(reroll) });
- if (mode) params.set('mode', mode);
- if (grain) params.set('grain', grain);
- if (platform) params.set('platform', platform);
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), apiBudgetMs());
- try {
- // Race the budget explicitly: abort signals do not reliably cancel the
- // TCP connect phase, so a blackholed route would otherwise stall ~10s.
- const response = await Promise.race([
- fetch(`${API_BASE}/roll?${params}`, { signal: controller.signal }),
- new Promise(resolveTimeout => setTimeout(() => resolveTimeout(null), apiBudgetMs())),
- ]);
- if (!response) return null;
- if (!response.ok) return null;
- const roll = await response.json();
- if (!Array.isArray(roll.challengers) || roll.challengers.length === 0) return null;
- return roll;
- } catch {
- return null;
- } finally {
- clearTimeout(timer);
- }
-}
-
-function telemetryDisabled() {
- return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK);
-}
-
-// Anonymous choice ping: one per resolved attended direction round. kind
-// says which card class won (assigned / pick / challenger / canon), so
-// pick-share and canon-share have a denominator; chosenId rides along only
-// when a dealt catalog world won, and register only when the round came from
-// a steered hand. Grounded candidates' names never leave the machine: they
-// are derived from the user's project, so the ping carries the kind alone.
-// Fire-and-forget; never fails the caller.
-const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']);
-export async function pingChosen({ chosenId, key, scope, mode, kind, register }) {
- if (telemetryDisabled()) return false;
- if (kind && !PING_KINDS.has(kind)) return false;
- if (register && register !== 'safer' && register !== 'bolder') return false;
- // Legacy shape: a bare challenger id with no kind stays a valid ping.
- if (!chosenId && !kind) return false;
- if ((kind === 'challenger' || !kind) && !chosenId) return false;
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), apiBudgetMs());
- try {
- await fetch(`${API_BASE}/chosen`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- ...(chosenId ? { chosenId } : {}),
- key,
- scope,
- mode,
- ...(kind ? { kind } : {}),
- ...(register ? { register } : {}),
- }),
- signal: controller.signal,
- });
- return true;
- } catch {
- return false;
- } finally {
- clearTimeout(timer);
- }
-}
-
-const CARD_BASE = process.env.IMPECCABLE_CARD_BASE || 'https://impeccable.style/worlds/cards';
-
-export function renderChallenger(concept, index) {
- const system = concept.system.map(rule => ` - ${rule}`).join('\n');
- const board = concept.cardBoard || `${CARD_BASE}/${concept.id}.webp`;
- const hero = concept.cardHero || `${CARD_BASE}/${concept.id}-hero.webp`;
- return ` ${index + 1}. ${concept.form}
- SOURCE ID: ${concept.id}
- CREATIVE SPARK: ${concept.spark}
- SYSTEM GRAMMAR:
-${system}
- WEB LEVERAGE: ${concept.webLeverage}
- QUALITY BAR: board ${board} · hero ${hero}`;
-}
-
-export function renderComposition(composition, index = null) {
- const grammar = composition.grammar.map(rule => ` - ${rule}`).join('\n');
- return ` ${index == null ? '' : `${index + 1}. `}${composition.form}
- SOURCE ID: ${composition.id}
- SPARK: ${composition.spark}
- COMPOSITION GRAMMAR:
-${grammar}
- WEB LEVERAGE: ${composition.webLeverage}`;
-}
-
-// Selection itself lives in lib/roll-selection.mjs so this script and the roll
-// API run one algorithm rather than two that drifted. These wrappers add only
-// what is local to the skill: resolving the catalog when no pool is passed, and
-// driving the generator with Node's synchronous hash, which keeps a local render
-// synchronous for prepared eval sessions and tests.
-function driveSelection(generator) {
- return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex'));
-}
-
-export function dealCompositions({ scope, key, reroll = 0, mode = null, grain = null, platform = null, sourceCompositions = null, count = 3 }) {
- const compositions = sourceCompositions ?? requireLocalConcepts().compositions;
- return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, grain, platform, compositions, count }));
-}
-
-// Array-returning form, which is what every caller wanted before the match
-// report existed.
-export function selectApprovedCompositions(options) {
- return dealCompositions(options).picks;
-}
-
-// Compatibility for callers that need a single smoke-test sample.
-export function selectApprovedComposition(options) {
- return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null;
-}
-
-export function selectApprovedChallengers({ scope, key, reroll = 0, mode = null, sourceConcepts = null }) {
- const source = sourceConcepts ?? requireLocalConcepts().concepts;
- const { approved, picks } = driveSelection(selectApprovedChallengersCore({ scope, key, reroll, mode, concepts: source }));
- return {
- approved,
- picks,
- poolRevision: approvedPoolRevision(source),
- catalogCount: source.length,
- };
-}
-
-const SEED_MODES = new Set(['persuade', 'operate', 'read', 'experience']);
-
-export function renderConceptSeed({
- scope = 'surface',
- key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
- reroll = 0,
- register = null,
- mode = null,
- grain = null,
- platform = null,
- candidateCount = 7,
- catalogDir = CATALOG_DIR,
- _resolvedData = undefined,
-} = {}) {
- if (scope !== 'surface' && scope !== 'direction') {
- throw new Error('concept-seed: --scope must be direction or surface');
- }
- if (!Number.isInteger(reroll) || reroll < 0) {
- throw new Error('concept-seed: --reroll must be a non-negative integer');
- }
- if (register !== null && register !== 'safer' && register !== 'bolder') {
- throw new Error('concept-seed: --register must be safer or bolder');
- }
- if (register !== null && reroll < 1) {
- throw new Error('concept-seed: --register steers a re-roll round; pass --reroll with it');
- }
- if (register !== null && scope !== 'direction') {
- throw new Error('concept-seed: --register applies to direction rounds only');
- }
- if (mode !== null && !SEED_MODES.has(mode)) {
- throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
- }
- // Grain needs no mode: how much of the product is in play is independent of
- // which register of work it is.
- if (grain !== null && !COMPOSITION_GRAINS.includes(grain)) {
- throw new Error(`concept-seed: --grain must be one of ${COMPOSITION_GRAINS.join(', ')}`);
- }
- if (platform !== null && !COMPOSITION_PLATFORMS.includes(platform)) {
- throw new Error(`concept-seed: --platform must be one of ${COMPOSITION_PLATFORMS.join(', ')}`);
- }
- if (!Number.isInteger(candidateCount) || candidateCount < 5 || candidateCount > 7) {
- throw new Error('concept-seed: --candidate-count must be an integer from 5 to 7');
- }
- const unit = (salt) => {
- const h = crypto.createHash('sha256').update(`${scope}:${salt}:${key}`).digest();
- return h.readUInt32BE(0) / 0xffffffff;
- };
- const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
- const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
- // Surface scope deals a hand of three grounded structures: one card is not
- // a choice, and the full ranked list would hand selection back to the
- // model's taste. The dice pick all three; the primary index leads. The
- // no-lineup rule stays direction-only, where it was written for worlds.
- const dealtIndices = [buildIndex];
- for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) {
- const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount);
- if (!dealtIndices.includes(idx)) dealtIndices.push(idx);
- if (draw > 64) { // hash repeats cannot stall the deal
- for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) {
- if (!dealtIndices.includes(fill)) dealtIndices.push(fill);
- }
- }
- }
-
- // Local catalog first (private repo, evals, tests), then the roll API,
- // then a degraded assignment-only seed. The assigned index is pure local
- // math, so even a fully offline run keeps the anti-argmax mechanism.
- let data = _resolvedData ?? null;
- if (_resolvedData === undefined) {
- const local = loadLocal(catalogDir);
- if (local) {
- const { approved, picks, poolRevision, catalogCount } = selectApprovedChallengers({
- scope,
- key,
- reroll,
- mode,
- sourceConcepts: local.concepts,
- });
- data = {
- source: 'local',
- poolRevision,
- approvedCount: approved.length,
- catalogCount,
- challengers: picks,
- ...(() => {
- const dealt = dealCompositions({ scope, key, reroll, mode, grain, platform, sourceCompositions: local.compositions });
- return { compositions: dealt.picks, compositionMatch: dealt.match };
- })(),
- };
- } else {
- // Keep local renders synchronous for prepared eval sessions and tests;
- // installed skills without a bundled catalog resolve through the API.
- return fetchRoll({ scope, key, mode, grain, platform, reroll }).then(roll => renderConceptSeed({
- scope,
- key,
- reroll,
- register,
- mode,
- grain,
- platform,
- candidateCount,
- catalogDir,
- _resolvedData: roll ? {
- source: 'api',
- poolRevision: roll.poolRevision,
- approvedCount: roll.approvedCount,
- catalogCount: roll.catalogCount,
- challengers: roll.challengers,
- compositions: Array.isArray(roll.compositions)
- ? roll.compositions
- : Array.isArray(roll.stagings)
- ? roll.stagings
- : roll.staging ? [roll.staging] : [],
- } : null,
- }));
- }
- }
-
- const promotedInstruction = scope === 'direction'
- ? `After ordering the grounded directions by resonance, build candidate
- ${buildIndex} of your own grounded list; the assignment never points at a
- challenger. The assignment is the roll, not a suggestion: your top-ranked
- direction is what every run would ship, so the script decides which grounded
- direction gets built. Each direction joins a durable visual system to a
- concrete expression for the requested first surface, decided as one. It must
- survive the current task plus navigation, quiet and dense content,
- interaction and state, and a substantially different future surface. In an
- attended run, present the assigned direction fully committed and offer
- re-roll. You may add ONE card for your top-ranked grounded candidate when
- it is not the assigned direction, kicker IMPECCABLE’S PICK, with an honest risk line
- naming its familiarity; one pick card, never a ranked lineup, and the pick
- never takes the lead position. When the assignment IS your top candidate,
- there is no pick card. Re-roll yourself only
- on named factual grounds, when the assignment cannot carry the product's
- truth or task; taste is never grounds.`
- : `After ordering the task's grounded structural candidates by resonance,
- deal candidates ${dealtIndices.join(', ')} of your own grounded list to the
- table; index ${buildIndex} leads, and the deal never points at a challenger.
- The deal is the roll, not a suggestion: the dice decide which structures
- reach the user, so the ranking rut stays broken while the user still gets a
- real choice, and the full ranked list stays yours. In an attended run,
- present the three dealt structures as full cards of equal salience, the
- lead carrying kicker THE ROLL, with steer and re-roll, and let the user
- lock one in; the world is already settled, so this choice is composition.
- Visualize every dealt card: with image generation available and a
- comp-led default (.impeccable/config.json buildPath; the page toggle
- handles the exception), declare a comp per card and generate after
- serving, lead first; otherwise author each card's wireframe field (see
- serve-question --schema) and the page draws the schematic. Carry the
- recorded default in the payload as buildPath with toggle: true. Locking a card
- approves its comp: a surface round that put three visualized structures on
- the table replaces the three-option comp round in visualize.md. Re-roll
- yourself only when every dealt structure fails audience identification or
- product clarity on named factual grounds.`;
-
- const challengerInstruction = scope === 'direction'
- ? `Fuse each challenger before judging it: the challenger supplies the form
- and its system grammar, the product supplies every fact, and clarity wins
- conflicts. Weigh the fused result against the assigned direction on exactly
- two axes, audience identification and product clarity. Losing to strong
- grounded material is a valid outcome; beating a thin or tool-monoculture
- list is the point. A fused challenger that wins both axes becomes the build.
- Close the weighing with a verdict per challenger, decided before any
- borrowing is considered: wins (beats the assigned direction on both axes),
- competitive (holds one axis), or declined (loses both). A declined
- challenger is not spent: name the one discipline of its system the assigned
- direction lacks, and raise the assigned direction to match before
- presenting it. A donation transfers ambition and system discipline, never
- the challenger's clothes; one world owns the page. Write each raise as its
- own named line on the presented direction, and carry every verdict, kept
- line, and raise into the decision page payload.`
- : `A challenger wins only when its fused result beats the grounded list on
- audience identification and product clarity. It may change task topology or
- interaction, but never the committed visual identity.`;
-
- const authorityInstruction = scope === 'direction'
- ? `PRODUCT.md and explicit incumbent brand commitments constrain every direction.
-The seed never chooses exact colors, fonts, tokens, or a user preference, and
-it never permits the world and first surface to be selected independently.`
- : `PRODUCT.md and DESIGN.md constrain every surface candidate's identity
-vocabulary; they do not cancel task-level composition. The seed never
-authorizes a new palette, type system, material world, or unfamiliar control
-behavior.`;
-
- const richnessInstruction = `The CREATIVE SPARK is a complete visual system, not a theme or decorative
-reference. Translate every supplied system rule into the product: palette and
-material, type and composition, topology, controls and states, and adaptation.
-Keep the source's visible character, scale, rhythm, and interaction instead of
-reducing vivid grammar to generic nouns. When the source is already a credible
-interface language, commit to it across navigation, content, controls, and
-states. Otherwise keep a literal carrier only when it becomes functional.
-Ambitious motion, spatial media, or interaction is welcome when it strengthens
-the product without weakening semantics, performance, or fallback behavior.`;
-
- if (!data) {
- // A degraded roll can still serve the safer register, which needs no
- // catalog at all: the assignment machinery is suppressed entirely, the
- // same as the non-degraded safer round, because emitting both "the user
- // picks" and a mandatory numbered build order hands the model two
- // contradicting instructions and the mandatory one tends to win. The
- // bolder register is exactly the thing degradation took away, so it
- // falls back to a plain grounded round, disclosed.
- const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`;
- if (register === 'safer') {
- return `${degradedHeader}
-SAFER REGISTER (user-requested): the assigned index is suspended this
- round; the user picks, and no candidate is mandated. Present the familiar
- register: your remaining grounded candidates from the conventional end, at
- most three, as full cards with an honest risk line each, plus the canon
- executed against two or three named competitors. This is the one sanctioned
- lineup of your own ranked candidates; it exists only by this explicit
- request. When the user voices a standing preference for it, record a brand
- commitment in PRODUCT.md.
-${authorityInstruction}
-A user- or brief-pinned decision beats the roll, always.
-REGISTER (restated for truncated readers): safer, user-requested; the
-assigned index is suspended this round and the user picks; seed key ${key}.
-`;
- }
- const degradedRegister = register === 'bolder'
- ? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran
- degraded with no catalog and no roll service, so there is nothing bold to
- deal. Tell the user, then run this round as a plain grounded re-roll; the
- assignment below applies.
-`
- : '';
- return `${degradedHeader}
-${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
- ${promotedInstruction}
- The assignment exists to refuse the model's ranking rut, never to outrank
- the user or the brief. Never expose assignment metadata in user-facing labels.
-No challengers this run: the roll service was unreachable and no local
-catalog exists. A sandboxed shell without network egress is the most common
-cause; before accepting degradation, rerun this command once through the
-harness's network-enabled or escalated command tool. When that rerun needs
-an approval, state exactly what the approver must know: this script's only
-network contact is one GET to https://impeccable.style/api/roll whose query
-carries scope, mode, an eight-hex seed key, and a re-roll counter; no
-project files, prompts, code, or conversation context are transmitted, and
-nothing is written. An approval request naming that URL and payload judges
-the real action; a bare "run with network" invites rejection for contacting
-an unspecified domain. If the rerun is still refused, proceed with the
-grounded candidates alone; the assignment above still applies at full
-strength. Tell the user plainly that this roll
-ran degraded, with no challengers and no quality-bar boards; do not present
-the outcome as a full roll. A degraded roll changes the cards, not the
-channel: when a browser can open, present the direction on the decision page
-(serve-question.mjs, text-only card); the structured question tool remains
-the no-browser fallback.
-${authorityInstruction}
-A user- or brief-pinned decision beats the roll, always.
-${scope === 'direction'
- ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
-${buildIndex} of your own grounded list; seed key ${key}.`
- : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
-${buildIndex} leads. Present all three dealt structures; seed key ${key}.`}
-`;
- }
-
- // Field order is the migration: `compositions` is current, `stagings` is what
- // the API emitted while these were called stagings, and `staging` is the
- // single-pick shape from before it dealt three. Older installs keep working.
- // Compositions are pulled from the deal until the expanded catalog is
- // ready for prime time: the current pool crowds the decision more than it
- // widens it. IMPECCABLE_COMPOSITIONS=1 re-enables rendering for catalog
- // development; the draw machinery, axes, and grain report stay intact.
- const compositionsEnabled = process.env.IMPECCABLE_COMPOSITIONS === '1';
- const compositions = !compositionsEnabled ? []
- : Array.isArray(data.compositions)
- ? data.compositions
- : Array.isArray(data.stagings)
- ? data.stagings
- : data.staging ? [data.staging] : [];
- // The grain report. A top-up keeps the deal at three, which is right, but it
- // must not read as three on-target inputs: a flow request answered entirely by
- // view-grain compositions means the model has to derive the flow's own
- // structure and borrow only their sequence law. Silence here would reproduce
- // the exact failure this axis exists to fix.
- const match = data.compositionMatch ?? null;
- const grainNote = (() => {
- if (!match?.grain) return '';
- if (match.grainAvailable === 0) {
- return `\nNONE of these sit at the requested ${match.grain} grain, because the catalog holds no ${match.grain}-grain composition yet. Derive that structure yourself and borrow only their sequence and attention laws.`;
- }
- if (match.atGrain === 0) {
- return `\nNONE of these sit at the requested ${match.grain} grain, though ${match.grainAvailable} exist; these were topped up from the rest of the register. Treat their structure as borrowed.`;
- }
- if (match.atGrain < compositions.length) {
- return `\n${match.atGrain} of ${compositions.length} sit at the requested ${match.grain} grain; the rest were topped up from the register and their structure is borrowed.`;
- }
- return '';
- })();
- const compositionBlock = compositions.length > 0
- ? `\n${scope === 'direction' ? 'FIRST-SURFACE COMPOSITION INPUTS (identity-free; test them with shortlisted worlds and keep world plus composition one decision):' : 'COMPOSITION CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'}
-${compositions.map((composition, index) => renderComposition(composition, index)).join('\n')}
-Each one asks the same question of this build: what is the cleverest way to
-present, organize, or make interactive the problem in front of you? They carry
-structure only, never a palette, typeface, or material. Treat them as serious
-rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n`
- : '';
- const rerollBlock = reroll > 0
- ? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded
- and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive
- genuinely new grounded candidates from unexplored angles before judging
- these fresh challengers.`}\n`
- : '';
- // A register swaps the round's presentation, never its deal: the assigned
- // index and challenger fetch stay identical so the chain reproduces, and
- // only the instructions change.
- const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this
- round's dealt hand is spent unseen, stays excluded from future rounds, and
- is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded
- candidates from the conventional end, at most three, as full cards with an
- honest risk line each, plus the canon executed against two or three named
- competitors. This is the one sanctioned lineup of your own ranked
- candidates; it exists only by this explicit request. When the user voices a
- standing preference for it, record a brand commitment in PRODUCT.md.`;
- const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no
- grounded direction is presented this round and the assigned index is
- suspended. The hand is every dealt challenger below, each fused with the
- product and presented as a full card; the FIRST dealt challenger leads, an
- assignment by deal order, so the dice still choose. Verdicts and donations
- apply between the challengers, weighed against the leader. The pick card
- sits out; the canon stays, as always.`;
- // The one command that follows a resolved choice. It records the choice
- // (anonymous telemetry on API-dealt rolls; skipped under DO_NOT_TRACK /
- // IMPECCABLE_NO_TELEMETRY) and opens the build's phase machine, whose
- // first gate is the comp round on a comp-led build. Every run that skipped
- // the comp round did so by treating a separate "telemetry ping" as
- // bookkeeping: suppressed with >/dev/null, run after the page was written,
- // or never run. So there is no separate ping; the start command is the
- // ping, and it is not optional.
- const nextCommand = scope === 'direction'
- ? `AFTER THE CHOICE, run exactly one command and follow what it prints (do not suppress its output; do not write page code before it):
- node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --direction ${key} --kind ${data.source === 'api' ? ' [--chosen ]' : ''}${register ? ` --register ${register}` : ''}
- It records the choice${data.source === 'api' ? ' (anonymous: card kind plus catalog id; skipped under DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY)' : ''} and opens the build phases: on a comp-led build the comp round is the first gate (three comps, one approved) and no page code is written before it closes; on a code-led build it prints the contract step. A build without this state file is a build the finish reviewer treats as having skipped the round.\n`
- : (data.source === 'api'
- ? `AFTER THE CHOICE, run once: node ${relative(process.cwd(), here) || '.'}/concept-seed.mjs --kind --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''} (records the choice; the locked card's comp is the approved comp, so then: node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp ).\n`
- : `AFTER THE CHOICE: the locked card's comp is the approved comp; run node ${relative(process.cwd(), here) || '.'}/build-phase.mjs start --comp and follow what it prints.\n`);
- const telemetryBlock = nextCommand;
- const assignedBlock = register === null
- ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
- ${promotedInstruction}
- The assignment exists to refuse the model's ranking rut, never to outrank
- the user or the brief. Never expose assignment metadata in user-facing labels.`
- : register === 'safer' ? saferBlock : bolderBlock;
- // A bolder round has no assigned grounded direction, so the generic
- // weighing instruction (which measures against the assignment) would
- // contradict the register; the bolder variant weighs against the leader.
- const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form
- and its system grammar, the product supplies every fact, and clarity wins
- conflicts. Weigh every fused challenger against the fused LEADER, the first
- dealt, on exactly two axes, audience identification and product clarity;
- verdicts and donations apply between the challengers, and one that beats
- the leader on both axes presents as the hand's strongest alternate.`;
- const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction;
- const challengerSection = register === 'safer'
- ? ''
- : `CHALLENGERS:
-${data.challengers.map(renderChallenger).join('\n')}
-${compositionBlock}${roundChallengerInstruction}
-When you can view images, open the QUALITY BAR board and hero for any
-challenger you weigh seriously and for the world you build. They exist as a
-craft bar, the finish level and commitment the build is expected to reach,
-never as a mockup to copy; your surface serves this product, not that render.
-`;
- const restated = register === null
- ? (scope === 'direction'
- ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
-${buildIndex} of your own grounded list; seed key ${key}.`
- : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
-${buildIndex} leads. Present all three dealt structures; seed key ${key}.`)
- : `REGISTER (restated for truncated readers): ${register}, user-requested; the
-assigned index is suspended this round; seed key ${key}.`;
- return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
-${rerollBlock}${assignedBlock}
-${challengerSection}${authorityInstruction}
-${richnessInstruction}
-${telemetryBlock}A user- or brief-pinned decision beats the roll, always.
-${restated}
-`;
-}
-
-/**
- * What the model must do next, once a direction (or surface structure) is
- * chosen. Read from the same config the boot directive reads:
- * `.impeccable/config.local.json` over `.impeccable/config.json`,
- * `buildPath` comp|code; with neither, comp-led whenever image generation
- * exists (an OpenAI key here; a harness-native image tool is invisible to
- * this script, so the text names it too), code-led otherwise.
- */
-export function nextStepAfterChoice({ key, scope, cwd = process.cwd(), env = process.env } = {}) {
- let buildPath = null;
- for (const name of ['config.json', 'config.local.json']) {
- try {
- const raw = JSON.parse(readFileSync(resolve(cwd, '.impeccable', name), 'utf8'));
- if (raw?.buildPath === 'comp' || raw?.buildPath === 'code') buildPath = raw.buildPath;
- } catch { /* absent */ }
- }
- const scriptsDir = dirname(fileURLToPath(import.meta.url));
- const scripts = relative(cwd, scriptsDir) || '.';
- const imageGen = !!env.OPENAI_API_KEY;
- const seed = key ? ` --direction ${key}` : '';
- if (buildPath === 'code') {
- return `NEXT (code-led, from .impeccable config): write the direction contract, then build; no comp round. Load reference/new-work.md section 5 and 6.\n`;
- }
- const why = buildPath === 'comp' ? 'from .impeccable config' : imageGen ? 'default: image generation is available' : 'default: comp-led unless no image tool exists; if your harness truly has none and there is no OpenAI key, this is code-led and you say so in one line';
- if (scope === 'surface') {
- return `NEXT (comp-led, ${why}): the locked card's comp is the approved comp. Run: node ${scripts}/build-phase.mjs start --comp and follow its NEXT lines. Do not write page code before build-phase.mjs advance has closed the spec, plates, and hero gates.\n`;
- }
- return `NEXT (comp-led, ${why}): the world is chosen; the composition is not. Run: node ${scripts}/build-phase.mjs start${seed} and follow its NEXT lines: it opens the comps phase (three comps under .impeccable/mocks/, one approved by the user through the decision page or structured question, sidecar "approved": true), then spec, plates, hero, sections, motion, responsive, review. Do not write page code before those gates close. Reference: reference/visualize.md for the comp round.\n`;
-}
-
-if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
- const args = process.argv.slice(2);
- const fromIdx = args.indexOf('--from');
- const scopeIdx = args.indexOf('--scope');
- const rerollIdx = args.indexOf('--reroll');
- const registerIdx = args.indexOf('--register');
- const modeIdx = args.indexOf('--mode');
- const grainIdx = args.indexOf('--grain');
- const platformIdx = args.indexOf('--platform');
- const candidateCountIdx = args.indexOf('--candidate-count');
- const chosenIdx = args.indexOf('--chosen');
- const kindIdx = args.indexOf('--kind');
- try {
- if (chosenIdx !== -1 || kindIdx !== -1) {
- // Choice ping: always exits 0, telemetry must never fail a design flow.
- // --kind alone pings a non-challenger outcome (assigned/pick/canon);
- // --chosen alone stays the legacy challenger-win ping.
- const sent = await pingChosen({
- chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined,
- key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
- scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
- mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined,
- kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined,
- register: registerIdx !== -1 ? args[registerIdx + 1] : undefined,
- });
- process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n');
- // The choice is resolved; this is the last script output the model
- // reads before it decides what to do next, and every run that skipped
- // the comp round did so right here: prose 20 KB into new-work.md lost
- // to "direction locked, building now". So the ping prints the next
- // mandatory step from the recorded build path, and the phase machine
- // takes it from there.
- process.stdout.write(nextStepAfterChoice({
- key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
- scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
- }));
- } else {
- // A dealt roll leaves a marker the build phase clears: context.mjs and
- // detect.mjs read it and refuse to treat page work as done while a
- // direction is chosen but the build never started (COMP_ROUND_OPEN).
- try {
- const { mkdirSync, writeFileSync: wf } = await import('node:fs');
- if (scopeIdx !== -1 && args[scopeIdx + 1] === 'direction') {
- mkdirSync(resolve(process.cwd(), '.impeccable', 'build'), { recursive: true });
- wf(resolve(process.cwd(), '.impeccable', 'build', 'pending.json'), JSON.stringify({ scope: 'direction', at: new Date().toISOString() }, null, 2));
- }
- } catch { /* marker is best-effort */ }
- // Mechanical init gate: prose alone does not keep a model from dealing
- // before init, and fresh repos produced exactly that skip (the model
- // rolled directions with no PRODUCT.md, so nothing grounded the fusion).
- // The --chosen branch above stays ungated; telemetry never blocks.
- const { loadContext } = await import('./context.mjs');
- if (!loadContext(process.cwd()).hasProduct) {
- process.stdout.write([
- 'NO_PRODUCT_MD: the dice stay in the cup until product truth exists.',
- 'Complete the init ask round and write PRODUCT.md first (reference/init.md), then re-run this exact command.',
- 'Challengers fuse their form with facts from PRODUCT.md; without it every direction is ungrounded.',
- ].join(' ') + '\n');
- process.exit(1);
- }
- process.stdout.write(await renderConceptSeed({
- scope: scopeIdx !== -1 ? args[scopeIdx + 1] : 'surface',
- key: fromIdx !== -1
- ? args[fromIdx + 1]
- : (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
- reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
- register: registerIdx !== -1 ? args[registerIdx + 1] : null,
- mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
- grain: grainIdx !== -1 ? args[grainIdx + 1] : null,
- platform: platformIdx !== -1 ? args[platformIdx + 1] : null,
- candidateCount: candidateCountIdx !== -1 ? Number(args[candidateCountIdx + 1]) : 7,
- }));
- }
- } catch (error) {
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
- process.exitCode = 1;
- }
- // A raced-out fetch may still hold a socket; exit explicitly so the CLI
- // never lingers on a dead network path after output is written. Destroy
- // fetch's global undici dispatcher first: process.exit() with a live
- // keep-alive socket trips a libuv assertion on Windows and aborts the
- // process after a successful roll (nodejs/node#56645).
- const dispatcher = globalThis[Symbol.for('undici.globalDispatcher.1')];
- if (dispatcher && typeof dispatcher.destroy === 'function') {
- try { await dispatcher.destroy(); } catch { /* exit regardless */ }
- }
- process.exit(process.exitCode ?? 0);
-}
diff --git a/skill/scripts/context-signals.mjs b/skill/scripts/context-signals.mjs
deleted file mode 100644
index c307a1ea9..000000000
--- a/skill/scripts/context-signals.mjs
+++ /dev/null
@@ -1,325 +0,0 @@
-#!/usr/bin/env node
-/**
- * Context-signals gatherer for the bare Impeccable invocation
- * (no-argument) path. Collects cheap, deterministic signals about the current
- * project and emits them as JSON.
- *
- * It does NOT score or rank. The agent reasons over the raw signals using its
- * knowledge of the command catalog (see SKILL.md routing rule 1). Deliberately
- * light: no LLM calls, no detector run (`npx impeccable detect` is heavier and
- * opt-in), no file writes. Every probe is best-effort and never throws; the
- * output is always valid JSON.
- *
- * Signals:
- * - setup: PRODUCT.md / DESIGN.md presence and whether code exists
- * - critique: the latest cached critique score (.impeccable/critique)
- * - git: branch + files changed vs the default branch (a scope hint)
- * - devServer: whether a local dev server answers on a common port (gates live)
- */
-import fs from 'node:fs';
-import net from 'node:net';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { execFileSync } from 'node:child_process';
-import { loadContext, extractPlatform } from './context.mjs';
-import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
-
-/** Is there code here at all, or just context files / an empty repo? */
-function hasCode(cwd) {
- if (fs.existsSync(path.join(cwd, 'package.json'))) return true;
- for (const d of ['src', 'app', 'pages', 'site', 'public', 'components', 'lib']) {
- if (fs.existsSync(path.join(cwd, d))) return true;
- }
- return false;
-}
-
-/**
- * Summarize the most recent critique snapshot across all targets.
- */
-function latestCritique(cwd) {
- try {
- const latest = readLatestSnapshotAcrossTargets({ cwd });
- if (!latest) return null;
- const get = (key) => latest.meta[key] ?? null;
- const num = (v) => {
- if (v == null || (typeof v === 'string' && v.trim() === '')) return null;
- const n = Number(v);
- return Number.isFinite(n) ? n : null;
- };
- return {
- slug: get('slug'),
- score: num(get('total_score') ?? get('score')),
- p0: num(get('p0_count') ?? get('p0')),
- p1: num(get('p1_count') ?? get('p1')),
- timestamp: get('timestamp'),
- file: path.relative(cwd, latest.path),
- };
- } catch {
- return null;
- }
-}
-
-/** Branch + a scope hint: files changed vs the default branch, else working tree. */
-function gitSignals(cwd) {
- const run = (args, { trim = true } = {}) => {
- try {
- const out = execFileSync('git', args, {
- cwd,
- encoding: 'utf-8',
- stdio: ['ignore', 'pipe', 'ignore'],
- });
- return trim ? out.trim() : out;
- } catch {
- return null;
- }
- };
- if (run(['rev-parse', '--is-inside-work-tree']) !== 'true') {
- return { isRepo: false, branch: null, base: null, changedFiles: [], changedCount: 0 };
- }
- const branch = run(['rev-parse', '--abbrev-ref', 'HEAD']);
- // The merge target is detected, not assumed. A hardcoded main/master list
- // diffed develop-based repos against the wrong base, so git.changedFiles
- // carried the whole develop/main divergence into scan.targets (issue
- // #302). Signals, most specific first: the branch's configured upstream
- // (@{u}; a branch pushed with -u tracks itself and is skipped by the
- // self-check), then the remote's default-branch symref (origin/HEAD),
- // then the conventional integration names. The conventional fallbacks
- // are withheld when the current branch IS one of them: sitting on main
- // in a repo that also has develop must not diff the two integration
- // branches against each other.
- // Candidates carry a display name (what git.base reports) and the revs to
- // try, in order. A remote ref like `upstream/release` (fork workflows) or
- // an origin/HEAD target with no local checkout is a perfectly good diff
- // base, so revs are not limited to local branch names.
- const remotes = (run(['remote']) || '').split('\n').filter(Boolean);
- // Read @{u} as a FULL symbolic ref: refs/heads/... is a local upstream
- // (branch..remote = "."), refs/remotes//... is remote-tracking. No
- // string guessing on the abbreviated form survives contact with reality:
- // a local upstream named release/2.0 is one branch name, and a local
- // feature/foo beside a remote actually named "feature" is only told apart
- // from feature's remote-tracking refs by the full ref namespace.
- const resolveUpstream = () => {
- const full = run(['rev-parse', '--symbolic-full-name', '@{u}']);
- if (!full) return null;
- if (full.startsWith('refs/heads/')) {
- const name = full.slice('refs/heads/'.length);
- return { name, rev: name };
- }
- if (full.startsWith('refs/remotes/')) {
- const rest = full.slice('refs/remotes/'.length);
- const i = rest.indexOf('/');
- if (i > 0) return { name: rest.slice(i + 1), rev: rest };
- }
- return null;
- };
- const conventional = ['develop', 'main', 'master'];
- // On an integration branch itself the scope hint is the working tree. No
- // signal may override that: an origin/HEAD or upstream naming a DIFFERENT
- // integration branch (sitting on develop while the remote default is
- // main) would produce exactly the integration-vs-integration divergence
- // this detection exists to prevent. "Integration branch" means a
- // conventional name OR any remote's default branch (origin first, but a
- // fork-parent layout may only have an `upstream` remote), so a
- // non-standard default like trunk is guarded the same way. A detached
- // checkout (branch reads as the literal `HEAD`) has no branch identity to
- // diff for and keeps the working-tree scope too.
- const remoteHeads = [];
- for (const r of [...new Set(['origin', ...remotes])]) {
- // The symref's own prefix is the remote just queried, so it is stripped
- // directly; the remote need not be in `git remote` output (tests and
- // partial clones fabricate refs/remotes/origin/* without a remote).
- const ref = run(['symbolic-ref', '--short', `refs/remotes/${r}/HEAD`]);
- if (ref && ref.startsWith(`${r}/`)) remoteHeads.push({ name: ref.slice(r.length + 1), rev: ref });
- }
- const onIntegrationBranch = branch === 'HEAD'
- || conventional.includes(branch)
- || remoteHeads.some((head) => head.name === branch);
- let base = null;
- let baseRev = null;
- if (!onIntegrationBranch) {
- const upstream = resolveUpstream();
- // Every named candidate tries the local branch first, then that name on
- // every remote (origin first). Covering all remotes up front is what
- // makes the name-level dedup below safe: a develop or main that exists
- // only as upstream/ still resolves even though origin's candidate
- // claimed the name first.
- const remoteOrder = ['origin', ...remotes.filter((name) => name !== 'origin')];
- const revsFor = (name) => [name, ...remoteOrder.map((r) => `${r}/${name}`)];
- const candidates = [];
- const seen = new Set();
- const addCandidate = (name, revs) => {
- if (!name || name === branch || seen.has(name)) return;
- seen.add(name);
- candidates.push({ name, revs });
- };
- // The upstream tracks the actual merge target, so its own rev wins over
- // a possibly stale local branch of the same name.
- if (upstream) addCandidate(upstream.name, [upstream.rev]);
- // A develop branch marks a git-flow repo where features merge to develop
- // even when the platform default (origin/HEAD) was never flipped off
- // main; an existing develop therefore outranks the remote default. This
- // is #302's own repro shape, and repos without develop are unaffected.
- // A remote's advertised default prefers its own remote-tracking rev over
- // a possibly stale local checkout of the same name, for the same reason
- // the upstream candidate leads with its rev. That applies to the develop
- // candidate too when the remote default IS develop: it sits before the
- // remote-default entries in the order, so it must lead with their rev
- // itself or a stale local develop would win.
- const advertisedRevs = (name) => remoteHeads.filter((head) => head.name === name).map((head) => head.rev);
- addCandidate('develop', [...new Set([...advertisedRevs('develop'), ...revsFor('develop')])]);
- for (const head of remoteHeads) addCandidate(head.name, [...new Set([head.rev, ...revsFor(head.name)])]);
- for (const name of ['main', 'master']) addCandidate(name, revsFor(name));
- for (const c of candidates) {
- const rev = c.revs.find((r) => run(['rev-parse', '--verify', '--quiet', r]) !== null);
- if (rev) {
- base = c.name;
- baseRev = rev;
- break;
- }
- }
- }
- const diffBase = base && branch && branch !== base ? base : null;
- const fromDiff = diffBase ? run(['diff', '--name-only', `${baseRev}...HEAD`]) : null;
- // porcelain lines are `XY PATH`: a 2-char status + a space, then the path.
- // Don't trim the combined output — an unstaged-modified line starts with a
- // leading space (` M path`), and a global trim would eat the first line's
- // status column and shift the slice. Renames render as `old -> new`.
- const fromStatus = run(['-c', 'core.quotepath=false', 'status', '--porcelain'], { trim: false });
- let changed = [];
- if (fromDiff) {
- changed = fromDiff.split('\n').filter(Boolean);
- } else if (fromStatus) {
- changed = fromStatus.split(/\r?\n/).filter(Boolean).map((l) => {
- const p = l.slice(3);
- const arrow = p.indexOf(' -> ');
- return arrow === -1 ? p : p.slice(arrow + 4);
- });
- }
- return {
- isRepo: true,
- branch,
- base: diffBase,
- changedFiles: changed.slice(0, 50),
- changedCount: changed.length,
- };
-}
-
-const COMMON_DEV_PORTS = [4321, 3000, 5173, 5174, 8080, 8000, 4200];
-
-function probePort(port, timeout = 250) {
- return new Promise((resolve) => {
- const sock = new net.Socket();
- let settled = false;
- const finish = (ok) => {
- if (settled) return;
- settled = true;
- try { sock.destroy(); } catch { /* ignore */ }
- resolve(ok);
- };
- sock.setTimeout(timeout);
- sock.once('connect', () => finish(true));
- sock.once('timeout', () => finish(false));
- sock.once('error', () => finish(false));
- sock.connect(port, '127.0.0.1');
- });
-}
-
-async function devServerSignals() {
- const open = [];
- await Promise.all(
- COMMON_DEV_PORTS.map(async (p) => {
- if (await probePort(p)) open.push(p);
- }),
- );
- open.sort((a, b) => a - b);
- return { running: open.length > 0, ports: open };
-}
-
-// Extensions the detector scans (mirrors the engine's walkDir set + HTML).
-const SCANNABLE_EXT = new Set([
- '.html', '.htm', '.css', '.scss',
- '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
-]);
-// Where UI source typically lives. The detector walks these and skips
-// node_modules / dist / build and all hidden dirs automatically.
-const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
-
-// A changed file under a hidden or dependency/build directory is not app
-// source — it's a vendored AI-harness install (.claude/skills/..., .cursor/,
-// .impeccable/, issue #303), a build artifact, or a dependency. Mirrors the
-// engine walkDir's skip rule so git-changes targeting can't resurface paths
-// the walker would never visit.
-function isVendoredPath(rel) {
- const dirSegments = rel.split(/[\\/]/).slice(0, -1);
- return dirSegments.some(
- (seg) =>
- (seg.startsWith('.') && seg !== '.vitepress' && seg !== '.vuepress' && seg !== '.storybook') ||
- seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === '__pycache__',
- );
-}
-
-/**
- * Local paths the agent should point the bundled detector at — never a URL.
- * A URL means a costly Puppeteer browser render, and a probed dev-server port
- * may not even belong to this project. An HTML *file* or a source tree is
- * scanned by the cheap, jsdom-free static engine. This script does NOT run the
- * detector; it just surfaces the target(s) so the agent can run
- * `node /detect.mjs --json ` and fold the hits in.
- */
-function scanTargets(cwd, git) {
- // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's
- // what the user is working on, it's a small set, and it's local.
- if (git.isRepo && git.changedFiles.length) {
- const changed = git.changedFiles
- .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
- .filter((f) => !isVendoredPath(f))
- .filter((f) => fs.existsSync(path.join(cwd, f)));
- if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
- }
- // 2. Otherwise scan the local source dirs that exist.
- const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d)));
- if (dirs.length) return { targets: dirs, via: 'source-dir' };
- // 3. A root HTML entry, or the project root as a last resort when there's
- // code but no conventional source dir (walkDir still skips heavy dirs).
- if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' };
- if (hasCode(cwd)) return { targets: ['.'], via: 'root' };
- return { targets: [], via: null };
-}
-
-export async function gatherSignals(cwd = process.cwd()) {
- const ctx = loadContext(cwd);
- const git = gitSignals(cwd);
- return {
- setup: {
- hasProduct: ctx.hasProduct,
- productPath: ctx.productPath,
- hasDesign: ctx.hasDesign,
- designPath: ctx.designPath,
- hasCode: hasCode(cwd),
- platform: extractPlatform(ctx.product),
- },
- critique: { latest: latestCritique(cwd) },
- git,
- devServer: await devServerSignals(),
- scan: scanTargets(cwd, git),
- };
-}
-
-async function cli() {
- const signals = await gatherSignals(process.cwd());
- process.stdout.write(`${JSON.stringify(signals, null, 2)}\n`);
-}
-
-function invokedAsScript() {
- const arg = process.argv[1];
- if (!arg) return false;
- try {
- return fs.realpathSync(arg) === fs.realpathSync(fileURLToPath(import.meta.url));
- } catch {
- return false;
- }
-}
-
-if (invokedAsScript()) {
- cli();
-}
diff --git a/skill/scripts/context.mjs b/skill/scripts/context.mjs
deleted file mode 100644
index 41112429a..000000000
--- a/skill/scripts/context.mjs
+++ /dev/null
@@ -1,1565 +0,0 @@
-/**
- * Context loader: prints PRODUCT.md, DESIGN.md when present, the matching
- * persisted surface brief when one can be resolved, and native-platform
- * guidance selected from PRODUCT.md. It prints a
- * `NO_PRODUCT_MD:` message when no
- * PRODUCT.md is found anywhere. The skill keys off that message to branch:
- * from-scratch build requests (plus init / teach / shape) and clear
- * build/shape intent divert into the init flow, while scoped commands proceed
- * using the existing code as context.
- *
- * Path resolution (first match wins):
- * 1. Active project root, if PRODUCT.md or DESIGN.md is there. An explicit
- * --target selects the active project: the workspace child in a
- * monorepo, or the nearest directory around the target carrying
- * canonical context files in an ordinary repo (issue #376).
- * 2. Active project .agents/context/ then docs/
- * 3. Repo root context, using the same order, as a per-file fallback
- * whenever the active project is nested below it (a repo counts as a
- * monorepo when a package manager declares workspaces, or
- * `.impeccable/config.json` declares `projectRoots`)
- * 4. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user
- * escape hatch, only consulted when defaults are empty
- * 5. Active project root as a "nothing found" default
- *
- * `resolveContextDir()` and `loadContext()` are also exported for the
- * server-side scripts (live.mjs, live-server.mjs) that need the structured
- * shape rather than the markdown block.
- */
-import fs from 'node:fs';
-import { spawnSync } from 'node:child_process';
-import os from 'node:os';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { parseTargetOptions } from './lib/target-args.mjs';
-import { IMPECCABLE_COMMAND, IMPECCABLE_PROVIDER_ID } from './lib/provider.mjs';
-import { resolveSurfaceBrief } from './lib/surface-briefs.mjs';
-import { collectBootFindings, designSidecarCandidatesFor } from './lib/staleness.mjs';
-import {
- buildStalenessDirective,
- filterFreshFindings,
- stalenessCheckDisabled,
-} from './lib/staleness-notice.mjs';
-
-const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
-const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
-const SKILL_REFERENCE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'reference');
-const FALLBACK_DIRS = ['.agents/context', 'docs'];
-const MONOREPO_MARKER_FILES = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
-const MONOREPO_FALLBACK_PROJECT_DIRS = ['apps', 'packages'];
-const WORKSPACE_DISCOVERY_IGNORED_DIRS = new Set([
- 'node_modules',
- '.git',
- 'dist',
- 'build',
- '.next',
- '.nuxt',
- '.svelte-kit',
- '.turbo',
- '.cache',
- 'coverage',
- 'vendor',
- 'vendors',
-]);
-const VISUAL_SOURCE_DIRS = ['src', 'app', 'pages', 'components', 'site', 'public', 'styles'];
-const STYLE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.styl']);
-const UI_EXTENSIONS = new Set(['.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte', '.astro']);
-const VISUAL_SCAN_FILE_LIMIT = 250;
-const VISUAL_SCAN_DEPTH_LIMIT = 4;
-
-// ─── Update check ──────────────────────────────────────────────────────────
-// Piggyback a lightweight skill-version check on the once-per-session boot.
-// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent
-// can offer `npx impeccable update`. Everything here is best-effort and
-// silent on failure: a network problem, sandbox, or missing cache must never
-// block context output or print an error.
-
-const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, '');
-const UPDATE_CACHE_PATH =
- process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json');
-const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day
-const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week
-const FETCH_TIMEOUT_MS = 1200;
-
-export function resolveContextDir(cwd = process.cwd(), options = {}) {
- return resolveContext(cwd, options).contextDir;
-}
-
-export function loadContext(cwd = process.cwd(), options = {}) {
- const resolved = resolveContext(cwd, options);
- const absCwd = path.resolve(cwd);
- const productPath = resolved.productPath;
- const designPath = resolved.designPath;
- const product = productPath ? safeRead(productPath) : null;
- const design = designPath ? safeRead(designPath) : null;
- const platform = extractPlatform(product);
- const surfaceResolution = resolveSurfaceBrief(
- resolved.projectRoot,
- hasTargetOption(options) ? options.targetPath : null,
- );
- const surfaceBrief = surfaceResolution.brief;
- return {
- hasProduct: !!product,
- product,
- productPath: productPath ? path.relative(absCwd, productPath) : null,
- hasDesign: !!design,
- design,
- designPath: designPath ? path.relative(absCwd, designPath) : null,
- contextDir: resolved.contextDir,
- productContextDir: productPath ? path.dirname(productPath) : null,
- designContextDir: designPath ? path.dirname(designPath) : null,
- hasSurfaceBrief: !!surfaceBrief,
- surfaceBrief: surfaceBrief?.text ?? null,
- surfaceBriefPath: surfaceBrief?.path ? path.relative(absCwd, surfaceBrief.path) : null,
- surfaceBriefReason: surfaceResolution.reason,
- surfaceBriefCandidates: surfaceResolution.candidates.map((brief) => ({
- slug: brief.slug,
- path: path.relative(absCwd, brief.path),
- primaryTarget: brief.primaryTarget,
- relatedTargets: brief.relatedTargets,
- })),
- hasVisualImplementation: hasVisualImplementation(resolved.projectRoot),
- platform,
- projectRoot: resolved.projectRoot,
- repoRoot: resolved.repoRoot,
- isMonorepo: resolved.isMonorepo,
- };
-}
-
-function resolveContext(cwd = process.cwd(), options = {}) {
- const absCwd = path.resolve(cwd);
- const project = resolveProject(absCwd, options);
- const projectContextDir = resolveLocalContextDir(project.projectRoot);
- // Per-file inheritance from the repo root whenever the active project is
- // nested below it: monorepo workspace children and explicit-target nested
- // products in ordinary repos behave the same way.
- const rootContextDir = project.repoRoot !== project.projectRoot
- ? resolveLocalContextDir(project.repoRoot)
- : null;
-
- let productPath =
- (projectContextDir ? firstExisting(projectContextDir, PRODUCT_NAMES) : null)
- || (rootContextDir ? firstExisting(rootContextDir, PRODUCT_NAMES) : null);
- let designPath =
- (projectContextDir ? firstExisting(projectContextDir, DESIGN_NAMES) : null)
- || (rootContextDir ? firstExisting(rootContextDir, DESIGN_NAMES) : null);
-
- let envContextDir = null;
- if (!productPath && !designPath) {
- envContextDir = resolveEnvContextDir(absCwd);
- if (envContextDir) {
- productPath = firstExisting(envContextDir, PRODUCT_NAMES);
- designPath = firstExisting(envContextDir, DESIGN_NAMES);
- }
- }
-
- return {
- contextDir: productPath
- ? path.dirname(productPath)
- : designPath
- ? path.dirname(designPath)
- : envContextDir || project.projectRoot,
- productPath,
- designPath,
- projectRoot: project.projectRoot,
- repoRoot: project.repoRoot,
- isMonorepo: project.isMonorepo,
- targetDir: project.targetDir,
- };
-}
-
-export function resolveProjectRoot(cwd = process.cwd(), options = {}) {
- return resolveProject(cwd, options).projectRoot;
-}
-
-export function resolveTargetSelection(cwd = process.cwd(), options = {}) {
- if (hasTargetOption(options)) return null;
- const project = resolveProject(cwd);
- if (
- !project.isMonorepo
- || !project.projectRoot
- || !project.repoRoot
- || path.resolve(project.projectRoot) !== path.resolve(project.repoRoot)
- ) {
- return null;
- }
- const targetCandidates = discoverTargetCandidates(project.repoRoot);
- // No discoverable child apps (e.g. `workspaces: ["."]`, a root-only workspace,
- // or a marker file with no apps/packages children): there is nothing to choose,
- // so treat the repo root as the active project rather than blocking on an empty
- // selection prompt that the user cannot answer.
- if (targetCandidates.length === 0) return null;
- return {
- targetPath: null,
- projectRoot: project.projectRoot,
- repoRoot: project.repoRoot,
- targetCandidates,
- };
-}
-
-function resolveProject(cwd = process.cwd(), options = {}) {
- const absCwd = path.resolve(cwd);
- const targetDir = resolveTargetDir(absCwd, options);
- let repoRoot = findMonorepoRoot(targetDir);
- if (!repoRoot && targetDir !== absCwd) {
- const cwdRepoRoot = findMonorepoRoot(absCwd);
- if (cwdRepoRoot && isPathInside(targetDir, cwdRepoRoot)) {
- repoRoot = cwdRepoRoot;
- }
- }
- if (!repoRoot) {
- return {
- targetDir,
- projectRoot: nearestTargetContextRoot(absCwd, targetDir) || absCwd,
- repoRoot: absCwd,
- isMonorepo: false,
- };
- }
- return {
- targetDir,
- projectRoot: resolveWorkspaceProjectRoot(repoRoot, targetDir) || repoRoot,
- repoRoot,
- isMonorepo: true,
- };
-}
-
-function isPathInside(candidate, root) {
- const rel = path.relative(root, candidate);
- return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
-}
-
-function resolveLocalContextDir(root) {
- if (firstExisting(root, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
- return root;
- }
- for (const rel of FALLBACK_DIRS) {
- const candidate = path.resolve(root, rel);
- if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) {
- return candidate;
- }
- }
- return null;
-}
-
-function resolveEnvContextDir(cwd) {
- const envDir = process.env.IMPECCABLE_CONTEXT_DIR;
- if (!envDir || !envDir.trim()) return null;
- const trimmed = envDir.trim();
- return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
-}
-
-function resolveTargetDir(cwd, options = {}) {
- const targetPath = options && typeof options === 'object' ? options.targetPath : null;
- if (!targetPath || !String(targetPath).trim()) return cwd;
- const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
- try {
- const stat = fs.statSync(abs);
- return stat.isDirectory() ? abs : path.dirname(abs);
- } catch {
- return path.extname(abs) ? path.dirname(abs) : abs;
- }
-}
-
-function findMonorepoRoot(startDir) {
- let dir = path.resolve(startDir);
- const homeDir = path.resolve(os.homedir());
- while (true) {
- if (dir === homeDir) return null;
- // isMonorepoRoot is checked before hasGitBoundary on purpose: a workspace
- // root that also carries its own .git is still recognized. The trade-off is
- // deliberate — a directory with a monorepo *marker* but no workspace patterns
- // and no apps/packages children is not a monorepo root, so its .git stops
- // traversal and a further-up root is not searched. The nested .git is treated
- // as an independent project boundary, which is the intended isolation.
- if (isMonorepoRoot(dir)) return dir;
- if (hasGitBoundary(dir)) return null;
- const parent = path.dirname(dir);
- if (parent === dir) return null;
- dir = parent;
- }
-}
-
-function isMonorepoRoot(dir) {
- if (readProjectPatterns(dir).some((pattern) => !normalizeWorkspacePattern(pattern).startsWith('!'))) return true;
- if (!MONOREPO_MARKER_FILES.some((file) => fs.existsSync(path.join(dir, file)))) return false;
- return hasFallbackWorkspaceChildren(dir);
-}
-
-function hasGitBoundary(dir) {
- return fs.existsSync(path.join(dir, '.git'));
-}
-
-function hasFallbackWorkspaceChildren(dir) {
- for (const name of MONOREPO_FALLBACK_PROJECT_DIRS) {
- const base = path.join(dir, name);
- let entries;
- try {
- entries = fs.readdirSync(base, { withFileTypes: true });
- } catch {
- continue;
- }
- if (entries.some((entry) => entry.isDirectory() && !isIgnoredWorkspaceDiscoveryDir(entry.name))) return true;
- }
- return false;
-}
-
-function discoverTargetCandidates(repoRoot) {
- const roots = new Map();
- const patternGroups = readProjectPatternGroups(repoRoot);
- for (const patterns of patternGroups) {
- for (const pattern of patterns) {
- for (const root of discoverRootsForPattern(repoRoot, pattern)) {
- roots.set(path.relative(repoRoot, root).split(path.sep).join('/'), root);
- }
- }
- }
- if (MONOREPO_MARKER_FILES.some((file) => fs.existsSync(path.join(repoRoot, file)))) {
- for (const name of MONOREPO_FALLBACK_PROJECT_DIRS) {
- const base = path.join(repoRoot, name);
- let entries;
- try {
- entries = fs.readdirSync(base, { withFileTypes: true });
- } catch {
- continue;
- }
- for (const entry of entries) {
- if (!entry.isDirectory() || isIgnoredWorkspaceDiscoveryDir(entry.name)) continue;
- const root = path.join(base, entry.name);
- roots.set(path.relative(repoRoot, root).split(path.sep).join('/'), root);
- }
- }
- }
- return [...roots.entries()]
- .filter(([rel]) => rel && !rel.startsWith('..'))
- .filter(([rel]) => isSelectableCandidate(repoRoot, rel, patternGroups))
- .sort(([a], [b]) => a.localeCompare(b))
- .map(([rel, root]) => {
- const targetExample = findTargetExample(repoRoot, root);
- return {
- name: path.basename(root),
- path: rel,
- targetExample,
- ...resolveCandidateContextSummary(repoRoot, root, targetExample),
- };
- });
-}
-
-function resolveCandidateContextSummary(repoRoot, projectRoot, targetPath) {
- const ctx = resolveContext(repoRoot, { targetPath });
- return {
- productStatus: contextSourceStatus(ctx.productPath, repoRoot, projectRoot),
- productPath: contextSourcePath(ctx.productPath, repoRoot),
- designStatus: contextSourceStatus(ctx.designPath, repoRoot, projectRoot),
- designPath: contextSourcePath(ctx.designPath, repoRoot),
- };
-}
-
-// Selection candidates surface one of four statuses: 'child' (a canonical
-// PRODUCT.md/DESIGN.md directly in the app root), 'inherited' (resolved from the
-// repo root in a monorepo), 'missing' (no file found), and 'fallback'. 'fallback'
-// intentionally covers two non-canonical locations: a file inside the project
-// root but in a subdirectory (FALLBACK_DIRS, e.g. `.agents/context/`), and a file
-// outside both the project and repo roots (IMPECCABLE_CONTEXT_DIR override).
-function contextSourceStatus(filePath, repoRoot, projectRoot) {
- if (!filePath) return 'missing';
- const absPath = path.resolve(filePath);
- const absProjectRoot = path.resolve(projectRoot);
- const absRepoRoot = path.resolve(repoRoot);
- if (isPathInsideOrEqual(absPath, absProjectRoot)) {
- return path.dirname(absPath) === absProjectRoot ? 'child' : 'fallback';
- }
- if (absProjectRoot !== absRepoRoot && isPathInsideOrEqual(absPath, absRepoRoot)) {
- return 'inherited';
- }
- return 'fallback';
-}
-
-function contextSourcePath(filePath, repoRoot) {
- if (!filePath) return null;
- const rel = path.relative(repoRoot, filePath);
- if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
- return rel.split(path.sep).join('/');
- }
- return filePath;
-}
-
-function discoverRootsForPattern(repoRoot, rawPattern) {
- const pattern = normalizeWorkspacePattern(rawPattern);
- if (!pattern || pattern.startsWith('!')) return [];
- const segments = pattern.split('/').filter(Boolean);
- if (!segments.length) return [];
- const firstGlobIndex = segments.findIndex((segment) => segment.includes('*'));
- const literalPrefix = firstGlobIndex === -1 ? segments : segments.slice(0, firstGlobIndex);
- const base = path.join(repoRoot, ...literalPrefix);
- if (!fs.existsSync(base)) return [];
- if (segments.includes('**')) {
- const packageRoots = [];
- walkDirs(base, (dir) => {
- if (dir !== base && isCandidateProjectRoot(dir)) packageRoots.push(dir);
- });
- if (packageRoots.length) return packageRoots;
- return directChildDirs(base);
- }
- return expandSimplePattern(repoRoot, segments);
-}
-
-function expandSimplePattern(repoRoot, patternSegments, index = 0, current = repoRoot) {
- if (index >= patternSegments.length) return fs.existsSync(current) ? [current] : [];
- const segment = patternSegments[index];
- if (!segment.includes('*')) {
- return expandSimplePattern(repoRoot, patternSegments, index + 1, path.join(current, segment));
- }
- let entries;
- try {
- entries = fs.readdirSync(current, { withFileTypes: true });
- } catch {
- return [];
- }
- const roots = [];
- for (const entry of entries) {
- if (!entry.isDirectory() || isIgnoredWorkspaceDiscoveryDir(entry.name)) continue;
- if (!segmentMatches(segment, entry.name)) continue;
- roots.push(...expandSimplePattern(repoRoot, patternSegments, index + 1, path.join(current, entry.name)));
- }
- return roots;
-}
-
-function directChildDirs(dir) {
- try {
- return fs.readdirSync(dir, { withFileTypes: true })
- .filter((entry) => entry.isDirectory() && !isIgnoredWorkspaceDiscoveryDir(entry.name))
- .map((entry) => path.join(dir, entry.name));
- } catch {
- return [];
- }
-}
-
-function walkDirs(root, visit) {
- let entries;
- try {
- entries = fs.readdirSync(root, { withFileTypes: true });
- } catch {
- return;
- }
- for (const entry of entries) {
- if (!entry.isDirectory() || isIgnoredWorkspaceDiscoveryDir(entry.name)) continue;
- const dir = path.join(root, entry.name);
- visit(dir);
- walkDirs(dir, visit);
- }
-}
-
-function isCandidateProjectRoot(dir) {
- return !!(
- fs.existsSync(path.join(dir, 'package.json'))
- || firstExisting(dir, [...PRODUCT_NAMES, ...DESIGN_NAMES])
- || fs.existsSync(path.join(dir, 'src'))
- || fs.existsSync(path.join(dir, 'app'))
- || fs.existsSync(path.join(dir, 'pages'))
- || fs.existsSync(path.join(dir, 'public'))
- );
-}
-
-function isIgnoredWorkspaceDiscoveryDir(name) {
- return name.startsWith('.') || WORKSPACE_DISCOVERY_IGNORED_DIRS.has(name);
-}
-
-function findTargetExample(repoRoot, projectRoot) {
- const examples = [
- 'src/App.jsx',
- 'src/App.tsx',
- 'src/main.jsx',
- 'src/main.tsx',
- 'src/index.jsx',
- 'src/index.ts',
- 'app/page.tsx',
- 'pages/index.tsx',
- 'public/index.html',
- ];
- for (const rel of examples) {
- const abs = path.join(projectRoot, rel);
- if (fs.existsSync(abs)) return path.relative(repoRoot, abs).split(path.sep).join('/');
- }
- return path.relative(repoRoot, projectRoot).split(path.sep).join('/');
-}
-
-function resolveWorkspaceProjectRoot(repoRoot, targetDir) {
- const rel = path.relative(repoRoot, targetDir);
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return repoRoot;
- const relSegments = rel.split(path.sep).filter(Boolean);
- for (const patterns of readProjectPatternGroups(repoRoot)) {
- if (isExcludedByWorkspacePattern(relSegments, patterns)) return repoRoot;
- for (const pattern of patterns) {
- const projectRoot = projectRootFromWorkspacePattern(repoRoot, relSegments, pattern);
- if (projectRoot) return projectRoot;
- }
- }
- if (
- relSegments.length >= 2
- && MONOREPO_FALLBACK_PROJECT_DIRS.includes(relSegments[0])
- ) {
- return path.join(repoRoot, relSegments[0], relSegments[1]);
- }
- const nearest = nearestProjectLikeRoot(repoRoot, targetDir);
- if (nearest) return nearest;
- return repoRoot;
-}
-
-// A discovered folder is only selectable when picking it would resolve back to
-// itself. Impeccable `projectRoots` patterns govern every path they match:
-// a negation drops the candidate (resolveWorkspaceProjectRoot would send it to
-// the repo root), and a positive match with a different boundary drops it too,
-// because the boundary root is already its own candidate and choosing the
-// deeper folder would silently resolve there. Paths the Impeccable group does
-// not match fall through to the package-manager negations, which is the
-// pre-existing behavior for package workspaces and marker-dir fallbacks.
-function isSelectableCandidate(repoRoot, rel, patternGroups) {
- const relSegments = rel.split('/').filter(Boolean);
- const [impeccablePatterns, packagePatterns] = patternGroups;
- if (isExcludedByWorkspacePattern(relSegments, impeccablePatterns)) return false;
- for (const pattern of impeccablePatterns) {
- const boundary = projectRootFromWorkspacePattern(repoRoot, relSegments, pattern);
- if (boundary) return path.resolve(boundary) === path.resolve(path.join(repoRoot, ...relSegments));
- }
- return !isExcludedByWorkspacePattern(relSegments, packagePatterns);
-}
-
-function isExcludedByWorkspacePattern(relSegments, patterns) {
- return patterns.some((rawPattern) => {
- const pattern = normalizeWorkspacePattern(rawPattern);
- if (!pattern.startsWith('!')) return false;
- return workspacePatternMatchesRel(pattern.slice(1), relSegments);
- });
-}
-
-// An explicit --target in an ordinary (non-monorepo) repository must still
-// select a nested product's own context (issue #376). Walk from the target up
-// to — but not including — the invocation root and return the nearest
-// directory carrying context files, in the canonical spot or a fallback dir
-// (resolveLocalContextDir covers both). Context files only, not package.json:
-// without the monorepo root-context fallback, a package.json marker would
-// strand targets inside plain subpackages away from the root PRODUCT.md. The
-// cwd's own fallback context dirs (.agents/context, docs) hold the root
-// project's context, not a nested product, so they never count.
-// Returns null when nothing nested is found, keeping the cwd default.
-function nearestTargetContextRoot(absCwd, targetDir) {
- if (!isPathInside(targetDir, absCwd)) return null;
- const rootFallbackDirs = FALLBACK_DIRS.map((rel) => path.resolve(absCwd, rel));
- let dir = path.resolve(targetDir);
- while (dir && dir !== absCwd) {
- if (!rootFallbackDirs.includes(dir) && resolveLocalContextDir(dir)) {
- return dir;
- }
- const parent = path.dirname(dir);
- if (parent === dir) break;
- dir = parent;
- }
- return null;
-}
-
-function nearestProjectLikeRoot(repoRoot, targetDir) {
- let dir = path.resolve(targetDir);
- const stop = path.resolve(repoRoot);
- while (dir && dir !== stop) {
- if (
- firstExisting(dir, [...PRODUCT_NAMES, ...DESIGN_NAMES])
- || fs.existsSync(path.join(dir, 'package.json'))
- ) {
- return dir;
- }
- const parent = path.dirname(dir);
- if (parent === dir) break;
- dir = parent;
- }
- return null;
-}
-
-function nearestPackageRootBetween(repoRoot, targetDir, stopDir) {
- let dir = path.resolve(targetDir);
- const stop = path.resolve(stopDir || repoRoot);
- const root = path.resolve(repoRoot);
- while (dir && dir !== stop && isPathInsideOrEqual(dir, root)) {
- if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
- const parent = path.dirname(dir);
- if (parent === dir) break;
- dir = parent;
- }
- return null;
-}
-
-function isPathInsideOrEqual(candidate, root) {
- return path.resolve(candidate) === path.resolve(root) || isPathInside(candidate, root);
-}
-
-function workspacePatternMatchesRel(pattern, relSegments) {
- const patternSegments = normalizeWorkspacePattern(pattern).split('/').filter(Boolean);
- if (!patternSegments.length) return false;
- if (patternSegments.includes('**')) {
- const firstGlobIndex = patternSegments.findIndex((segment) => segment.includes('*'));
- const literalPrefix = firstGlobIndex === -1
- ? patternSegments
- : patternSegments.slice(0, firstGlobIndex);
- if (relSegments.length < literalPrefix.length + 1) return false;
- for (let i = 0; i < literalPrefix.length; i++) {
- if (!segmentMatches(literalPrefix[i], relSegments[i])) return false;
- }
- return true;
- }
- if (relSegments.length < patternSegments.length) return false;
- for (let i = 0; i < patternSegments.length; i++) {
- if (!segmentMatches(patternSegments[i], relSegments[i])) return false;
- }
- return true;
-}
-
-// Project boundaries come from two sources, in precedence order: explicit
-// `projectRoots` globs in .impeccable config, then package-manager workspace
-// declarations. A path matched by any Impeccable pattern — positive or
-// negated — is governed by the Impeccable group alone; package-manager
-// patterns only apply to paths the Impeccable group does not match. Within a
-// group, negations win over positives.
-function readProjectPatternGroups(repoRoot) {
- return [
- readImpeccableProjectRoots(repoRoot),
- [
- ...readPackageWorkspaces(repoRoot),
- ...readPnpmWorkspaces(repoRoot),
- ...readLernaWorkspaces(repoRoot),
- ].filter(Boolean),
- ];
-}
-
-function readProjectPatterns(repoRoot) {
- return readProjectPatternGroups(repoRoot).flat();
-}
-
-function readImpeccableProjectRoots(repoRoot) {
- const patterns = [];
- for (const name of ['config.json', 'config.local.json']) {
- const cfg = readJson(path.join(repoRoot, '.impeccable', name));
- if (!Array.isArray(cfg?.projectRoots)) continue;
- for (const entry of cfg.projectRoots) {
- if (typeof entry === 'string' && entry.trim()) patterns.push(entry.trim());
- }
- }
- return patterns;
-}
-
-function readPackageWorkspaces(repoRoot) {
- const pkg = readJson(path.join(repoRoot, 'package.json'));
- const workspaces = pkg?.workspaces;
- if (Array.isArray(workspaces)) return workspaces;
- if (Array.isArray(workspaces?.packages)) return workspaces.packages;
- return [];
-}
-
-function readLernaWorkspaces(repoRoot) {
- const lerna = readJson(path.join(repoRoot, 'lerna.json'));
- return Array.isArray(lerna?.packages) ? lerna.packages : [];
-}
-
-function readPnpmWorkspaces(repoRoot) {
- try {
- const body = fs.readFileSync(path.join(repoRoot, 'pnpm-workspace.yaml'), 'utf-8');
- const patterns = [];
- let inPackages = false;
- for (const line of body.split(/\r?\n/)) {
- const trimmed = stripYamlInlineComment(line).trim();
- if (!trimmed || trimmed.startsWith('#')) continue;
- const flowMatch = trimmed.match(/^packages:\s*\[(.*)\]\s*$/);
- if (flowMatch) {
- patterns.push(...parseYamlFlowList(flowMatch[1]));
- inPackages = false;
- continue;
- }
- if (/^packages:\s*$/.test(trimmed)) {
- inPackages = true;
- continue;
- }
- if (inPackages && /^[A-Za-z0-9_-]+:\s*/.test(trimmed)) break;
- if (inPackages) {
- const match = trimmed.match(/^-\s*(.+)$/);
- if (match) patterns.push(unquoteYamlValue(match[1]));
- }
- }
- return patterns;
- } catch {
- return [];
- }
-}
-
-function stripYamlInlineComment(line) {
- let quote = null;
- for (let i = 0; i < line.length; i++) {
- const ch = line[i];
- if ((ch === '"' || ch === "'") && line[i - 1] !== '\\') {
- quote = quote === ch ? null : quote || ch;
- continue;
- }
- if (ch === '#' && !quote) return line.slice(0, i);
- }
- return line;
-}
-
-function parseYamlFlowList(body) {
- const items = [];
- let quote = null;
- let current = '';
- for (let i = 0; i < body.length; i++) {
- const ch = body[i];
- if ((ch === '"' || ch === "'") && body[i - 1] !== '\\') {
- quote = quote === ch ? null : quote || ch;
- current += ch;
- continue;
- }
- if (ch === ',' && !quote) {
- const value = unquoteYamlValue(current);
- if (value) items.push(value);
- current = '';
- continue;
- }
- current += ch;
- }
- const value = unquoteYamlValue(current);
- if (value) items.push(value);
- return items;
-}
-
-function unquoteYamlValue(value) {
- return String(value || '')
- .trim()
- .replace(/^['"]|['"]$/g, '');
-}
-
-function readJson(filePath) {
- try {
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
- } catch {
- return null;
- }
-}
-
-function projectRootFromWorkspacePattern(repoRoot, relSegments, rawPattern) {
- const pattern = normalizeWorkspacePattern(rawPattern);
- if (!pattern || pattern.startsWith('!')) return null;
- const patternSegments = pattern.split('/').filter(Boolean);
- if (!patternSegments.length) return null;
- if (patternSegments.includes('**')) {
- return projectRootFromDoubleStarPattern(repoRoot, relSegments, patternSegments);
- }
- if (relSegments.length < patternSegments.length) return null;
- for (let i = 0; i < patternSegments.length; i++) {
- if (!segmentMatches(patternSegments[i], relSegments[i])) return null;
- }
- return path.join(repoRoot, ...relSegments.slice(0, patternSegments.length));
-}
-
-function projectRootFromDoubleStarPattern(repoRoot, relSegments, patternSegments) {
- const firstGlobIndex = patternSegments.findIndex((segment) => segment.includes('*'));
- const literalPrefix = firstGlobIndex === -1
- ? patternSegments
- : patternSegments.slice(0, firstGlobIndex);
- if (relSegments.length < literalPrefix.length + 1) return null;
- for (let i = 0; i < literalPrefix.length; i++) {
- if (!segmentMatches(literalPrefix[i], relSegments[i])) return null;
- }
- const prefixDir = path.join(repoRoot, ...literalPrefix);
- const targetDir = path.join(repoRoot, ...relSegments);
- const packageRoot = nearestPackageRootBetween(repoRoot, targetDir, prefixDir);
- if (packageRoot) return packageRoot;
- return path.join(repoRoot, ...relSegments.slice(0, literalPrefix.length + 1));
-}
-
-function normalizeWorkspacePattern(pattern) {
- return String(pattern || '')
- .trim()
- .replace(/^['"]|['"]$/g, '')
- .replace(/^\.\//, '')
- .replace(/\/+$/, '');
-}
-
-function segmentMatches(patternSegment, relSegment) {
- if (patternSegment === '*') return true;
- if (!patternSegment.includes('*')) return patternSegment === relSegment;
- const re = new RegExp(`^${escapeRegExp(patternSegment).replace(/\\\*/g, '[^/]*')}$`);
- return re.test(relSegment);
-}
-
-function firstExisting(dir, names) {
- for (const name of names) {
- const abs = path.join(dir, name);
- if (fs.existsSync(abs)) return abs;
- }
- return null;
-}
-
-function safeRead(p) {
- try {
- return fs.readFileSync(p, 'utf-8');
- } catch {
- return null;
- }
-}
-
-function loadNativePlatformReferences(platform) {
- const names = platform === 'adaptive'
- ? ['ios', 'android']
- : platform === 'ios' || platform === 'android'
- ? [platform]
- : [];
- return names.flatMap((name) => {
- const filePath = path.join(SKILL_REFERENCE_DIR, `${name}.md`);
- const content = safeRead(filePath);
- return content ? [{ name, filePath, content }] : [];
- });
-}
-
-/**
- * Best-effort evidence that the project already has an incumbent visual
- * implementation. DESIGN.md is documentation, not the only source of design
- * authority: real tokens, chosen type, and a component system in code must not
- * be mistaken for a greenfield identity merely because the document is absent.
- *
- * The scan is deliberately bounded and conservative. A package.json or one
- * empty scaffold component is not enough; a tokenized stylesheet, an authored
- * HTML surface, or several styled UI components is.
- */
-export function hasVisualImplementation(projectRoot) {
- if (!projectRoot) return false;
- const root = path.resolve(projectRoot);
- const queue = [];
- for (const rel of VISUAL_SOURCE_DIRS) {
- const dir = path.join(root, rel);
- if (fs.existsSync(dir)) queue.push({ dir, depth: 0 });
- }
-
- let scannedFiles = 0;
- let styledComponents = 0;
-
- const inspectFile = (filePath) => {
- const ext = path.extname(filePath).toLowerCase();
- if (!STYLE_EXTENSIONS.has(ext) && !UI_EXTENSIONS.has(ext)) return false;
- const base = path.basename(filePath).toLowerCase();
- if (/\.min\.[a-z]+$/.test(base)) return false;
- if (scannedFiles++ >= VISUAL_SCAN_FILE_LIMIT) return false;
- let body;
- try {
- body = fs.readFileSync(filePath, 'utf-8').slice(0, 64 * 1024);
- } catch {
- return false;
- }
-
- const evidence = body
- .replace(/\/\*[\s\S]*?\*\//g, '')
- .replace(//g, '')
- .replace(/^\s*\/\/.*$/gm, '');
- if (STYLE_EXTENSIONS.has(ext)) {
- const customProperties = evidence.match(/--[a-z0-9_-]+\s*:/gi)?.length ?? 0;
- const visualDeclarations = evidence.match(/\b(?:color|background(?:-color)?|border(?:-color)?|font-family)\s*:/gi)?.length ?? 0;
- if (/\b(?:tokens?|theme|design-system)\b/.test(base) && evidence.trim().length > 80) return true;
- if (customProperties >= 3 || visualDeclarations >= 5) return true;
- }
-
- if ((ext === '.html' || ext === '.htm') && evidence.length > 600 && /' : ''));
- if (paramValues && Object.keys(paramValues).length > 0) {
- lines.push(
- bodyIndent + commentSyntax.open + ' impeccable-param-values ' + id + ': ' + JSON.stringify(paramValues) + ' ' + commentSyntax.close,
- );
- }
- lines.push(bodyIndent + commentSyntax.open + ' impeccable-carbonize-end ' + id + ' ' + commentSyntax.close);
- lines.push(bodyIndent + '');
- lines.push(...bodyRestored);
- lines.push(bodyIndent + '
');
- };
-
- if (isJsx) {
- const wrapperStyle = 'style={{ display: "contents" }}';
- lines.push(indent + '');
- pushCarbonizeBody(indent + ' ');
- lines.push(indent + '
');
- } else {
- pushCarbonizeBody(indent);
- }
-
- return lines;
-}
-
-function reindentContent(contentLines, fromIndent, toIndent) {
- return contentLines.map((line) => {
- if (line.trim() === '') return '';
- if (line.startsWith(fromIndent)) return toIndent + line.slice(fromIndent.length);
- return toIndent + line.trimStart();
- });
-}
-
-function handleAccept(id, variantNum, _lines, targetFile, paramValues) {
- return withSourceLockSync(targetFile, 'accept:' + id, () => {
- const lines = fs.readFileSync(targetFile, 'utf-8').split('\n');
- return handleAcceptUnlocked(id, variantNum, lines, targetFile, paramValues);
- }, { waitMs: ACCEPT_LOCK_WAIT_MS });
-}
-
-function handleAcceptUnlocked(id, variantNum, lines, targetFile, paramValues) {
- const built = buildAcceptedWrappedSource(id, variantNum, lines, targetFile, paramValues);
- if (built.handled === false) return built;
- fs.writeFileSync(targetFile, built.content, 'utf-8');
- return {
- carbonize: built.carbonize,
- acceptedOriginalText: built.acceptedOriginalText,
- };
-}
-
-function buildAcceptedWrappedSource(id, variantNum, lines, targetFile, paramValues) {
- const block = findMarkerBlock(id, lines);
- if (!block) return { handled: false, error: 'Markers not found' };
-
- const commentSyntax = detectCommentSyntax(targetFile);
- const isJsx = commentSyntax.open === '{/*';
- // Anchor indent on the line we're replacing FROM (the outer wrapper),
- // not on `block.start` — for JSX that's the marker comment 2 spaces
- // deeper than the original element. See handleDiscard for the full
- // rationale.
- const replaceRange = expandReplaceRange(block, lines, isJsx);
- const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
-
- // Extract the chosen variant's inner content
- const variantContent = extractVariant(lines, block, variantNum);
- if (!variantContent) return { handled: false, error: 'Variant ' + variantNum + ' not found' };
- const originalContent = extractOriginal(lines, block);
-
- // Extract CSS block if present
- const cssContent = extractCss(lines, block, id);
-
- // Check if carbonizing is needed:
- // - CSS block exists, OR
- // - variant HTML contains helper classes/attributes that need cleanup
- const variantText = variantContent.join('\n');
- const hasHelperAttrs = variantText.includes('data-impeccable-variant');
- const needsCarbonize = !!(cssContent || hasHelperAttrs);
-
- const restored = deindentContent(variantContent, indent);
- const replacement = buildCarbonizeReplacement({
- indent,
- commentSyntax,
- isJsx,
- id,
- variantNum,
- cssContent,
- paramValues,
- restored,
- });
-
- const newLines = [
- ...lines.slice(0, replaceRange.start),
- ...replacement,
- ...lines.slice(replaceRange.end + 1),
- ];
- return {
- content: newLines.join('\n'),
- carbonize: needsCarbonize,
- acceptedOriginalText: originalContent.join('\n'),
- };
-}
-
-
-function readSourceShadowPreviewMeta(content, id) {
- const escaped = escapeRegExp(id);
- const wrapperRe = new RegExp('<[^>]+data-impeccable-variants=(["\'])' + escaped + '\\1[^>]*>');
- const match = String(content || '').match(wrapperRe);
- if (!match) return null;
- const tag = match[0];
- if (readHtmlAttr(tag, 'data-impeccable-preview') !== 'source-shadow') return null;
- const sourceFile = readHtmlAttr(tag, 'data-impeccable-source-file');
- const sourceStartLine = Number(readHtmlAttr(tag, 'data-impeccable-source-start'));
- const sourceEndLine = Number(readHtmlAttr(tag, 'data-impeccable-source-end'));
- if (!sourceFile || !Number.isFinite(sourceStartLine) || !Number.isFinite(sourceEndLine)) return null;
- return { sourceFile, sourceStartLine, sourceEndLine };
-}
-
-function readHtmlAttr(tag, name) {
- const match = String(tag || '').match(new RegExp('\\s' + escapeRegExp(name) + '\\s*=\\s*(["\'])(.*?)\\1'));
- if (!match) return null;
- return decodeHtmlAttr(match[2]);
-}
-
-function decodeHtmlAttr(value) {
- return String(value || '')
- .replace(/"/g, '"')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/&/g, '&');
-}
-
-// ---------------------------------------------------------------------------
-// Parsing helpers
-// ---------------------------------------------------------------------------
-
-/**
- * Find the start/end marker lines for a session.
- * Returns { start, end } (0-indexed line numbers) or null.
- */
-function findMarkerBlock(id, lines) {
- let start = -1;
- let end = -1;
- const startPattern = 'impeccable-variants-start ' + id;
- const endPattern = 'impeccable-variants-end ' + id;
-
- for (let i = 0; i < lines.length; i++) {
- if (start === -1 && lines[i].includes(startPattern)) start = i;
- if (lines[i].includes(endPattern)) { end = i; break; }
- }
-
- return (start !== -1 && end !== -1) ? { start, end, id } : null;
-}
-
-/**
- * Compute the line range to REPLACE (vs. just the marker range to extract
- * from). For JSX/TSX wrappers, live-wrap places the marker comments INSIDE
- * the `` outer wrapper so the picked
- * element's JSX slot keeps a single child — a Fragment `<>>` would have
- * solved the multi-sibling case but failed inside `asChild` / cloneElement
- * parents with "Invalid prop supplied to React.Fragment".
- *
- * That means the marker block is enclosed by the wrapper `
` opener
- * (with `data-impeccable-variants="ID"`) and its matching `
`. We
- * walk back to the opener and forward to the closer so accept/discard
- * remove the entire scaffold, not just the inner markers.
- *
- * Marker lines themselves stay where they were so extractOriginal /
- * extractVariant / extractCss continue to walk the same range.
- */
-function expandReplaceRange(block, lines, isJsx) {
- if (!isJsx) return { start: block.start, end: block.end };
-
- let { start, end } = block;
-
- // Walk back for the wrapper `
= 0; i--) {
- if (isVariantEndMarkerLine(lines[i], block.id)) break;
- if (hasVariantWrapperAttr(lines[i], block.id)) {
- let opener = i;
- while (opener > 0 && !/
` by div-depth tracking from the
- // wrapper opener. Operate on JOINED text instead of per-line: a
- // multi-line self-closing JSX `
` would
- // fool per-line regex tracking (the `
` line never matches selfCloseRe since it needs `
` 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 `
` (self-close, group 1 is `/`), `
`
- // (open, group 1 is empty), or `
`.
- const tagRe = /
]*?(\/?)>|<\/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
- * ` 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(''; }
-
-/**
- * `scriptAttrs` is a pre-rendered attribute string (trailing space included)
- * that the registry supplies for the target file. Astro is the only framework
- * that uses it today: Astro processes `\n' +
- open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
- );
-}
-
-function detectLineEnding(content) {
- if (content.includes('\r\n')) return '\r\n';
- if (content.includes('\r')) return '\r';
- return '\n';
-}
-
-function normalizeLineEndings(content, lineEnding) {
- return lineEnding === '\n' ? content : content.replace(/\n/g, lineEnding);
-}
-
-function readLineEndingAt(content, index) {
- if (content[index] === '\r' && content[index + 1] === '\n') return '\r\n';
- if (content[index] === '\n') return '\n';
- if (content[index] === '\r') return '\r';
- return '';
-}
-
-export function insertTag(content, config, port, token, scriptAttrs = '') {
- const lineEnding = detectLineEnding(content);
- const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, token, scriptAttrs), lineEnding);
- // insertBefore: match the LAST occurrence. Anchors like `
` open near the top of the document.
- const idx = content.indexOf(config.insertAfter);
- if (idx === -1) return content;
- const after = idx + config.insertAfter.length;
- // Preserve an existing trailing newline if the anchor already has one.
- // Slice the remainder from the original anchor offset, not prefix.length:
- // in the no-newline case prefix is one char longer than the anchor (the
- // appended '\n'), so slicing by prefix.length would drop the first real
- // character after the anchor (#227).
- const existingNewline = readLineEndingAt(content, after);
- const prefix = content.slice(0, after) + (existingNewline || lineEnding);
- const rest = content.slice(after + existingNewline.length);
- return prefix + block + rest;
-}
-
-/**
- * Remove the live script block. Matches either HTML or JSX comment markers
- * regardless of config (so stale tags from a wrong config can still be cleaned).
- *
- * Indent-preserving: captures any whitespace immediately preceding the opener
- * marker and re-emits it in place of the removed block. `insertTag` inserted
- * the block *after* the original line's indent and *before* the anchor (e.g.
- * `` naturally
- // belong at the end, and the same literal can appear earlier in code blocks
- // within rendered documentation pages.
- if (config.insertBefore) {
- const idx = content.lastIndexOf(config.insertBefore);
- if (idx === -1) return content;
- return content.slice(0, idx) + block + content.slice(idx);
- }
- // insertAfter: match the FIRST occurrence — typical anchors like `
` or
- // `
`), which moved the indent onto the opener line and left the anchor
- * unindented. Replacing the whole block (plus its trailing newline) with just
- * the captured indent hands the indent back to the anchor that follows.
- */
-export function removeTag(content, _syntax) {
- const patterns = [
- /([ \t]*)[\s\S]*?([ \t]*(?:\r\n|\n|\r|$)?)/,
- /([ \t]*)\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}([ \t]*(?:\r\n|\n|\r|$)?)/,
- ];
- for (const pat of patterns) {
- let changed = false;
- let next = content;
- do {
- content = next;
- next = content.replace(pat, (_match, leadingIndent, trailing = '') => {
- if (/[\r\n]/.test(trailing)) return leadingIndent;
- return leadingIndent || trailing || '';
- });
- if (next !== content) changed = true;
- } while (next !== content);
- if (changed) return next;
- }
- return content;
-}
-
-// ---------------------------------------------------------------------------
-// Content-Security-Policy meta-tag patcher
-//
-// When the user's HTML carries ``,
-// the cross-origin load of /live.js (and the SSE/POST connection back to
-// localhost:PORT) is blocked unless the CSP explicitly allows that origin.
-//
-// On insert: append `http://localhost:PORT` to `script-src` and `connect-src`,
-// and stash the original `content` value in a `data-impeccable-csp-original`
-// attribute (base64) so revert is exact.
-//
-// On remove: detect the marker attribute, decode it, restore the original
-// content value verbatim, drop the marker.
-//
-// Header-based CSP (Next.js headers, Nuxt routeRules, SvelteKit kit.csp,
-// shared helpers) is NOT patched here — those need framework-specific config
-// edits and are handled via the existing detect-csp.mjs reference output.
-// Only the in-source meta-tag form gets the auto-patch.
-// ---------------------------------------------------------------------------
-
-const CSP_MARKER_ATTR = 'data-impeccable-csp-original';
-
-function findCspMetaTags(content) {
- const out = [];
- const tagRe = /]*?)\/?>/gis;
- let m;
- while ((m = tagRe.exec(content)) !== null) {
- const attrs = m[1];
- if (!/(http-equiv|httpEquiv)\s*=\s*(['"])Content-Security-Policy\2/i.test(attrs)) continue;
- out.push({ start: m.index, end: m.index + m[0].length, full: m[0], attrs });
- }
- return out;
-}
-
-function getAttr(attrs, name) {
- const re = new RegExp(`\\b${name}\\s*=\\s*(['"])([\\s\\S]*?)\\1`, 'i');
- const m = attrs.match(re);
- return m ? { quote: m[1], value: m[2], full: m[0] } : null;
-}
-
-function appendOriginToDirective(csp, directive, origin) {
- const re = new RegExp(`(^|;)(\\s*)(${directive})\\s+([^;]*)`, 'i');
- const m = csp.match(re);
- if (m) {
- const tokens = m[4].trim().split(/\s+/);
- if (tokens.includes(origin)) return csp;
- return csp.replace(re, `${m[1]}${m[2]}${m[3]} ${[...tokens, origin].join(' ')}`);
- }
- // Directive missing — add it. Use 'self' + origin so we don't inadvertently
- // narrow the policy compared to the default-src fallback (most users with
- // an explicit CSP have 'self' there).
- return csp.trim().replace(/;?\s*$/, '') + `; ${directive} 'self' ${origin}`;
-}
-
-export function patchCspMeta(content, port) {
- const tags = findCspMetaTags(content);
- if (tags.length === 0) return content;
- const origin = `http://localhost:${port}`;
-
- // Walk last-to-first so prior splices don't invalidate later indices.
- let result = content;
- for (let i = tags.length - 1; i >= 0; i--) {
- const tag = tags[i];
- const attrs = tag.attrs;
- if (getAttr(attrs, CSP_MARKER_ATTR)) continue; // already patched
- const contentAttr = getAttr(attrs, 'content');
- if (!contentAttr) continue;
-
- const original = contentAttr.value;
- let patched = original;
- patched = appendOriginToDirective(patched, 'script-src', origin);
- patched = appendOriginToDirective(patched, 'connect-src', origin);
- // The shader overlay during 'generating' creates a screenshot via
- // URL.createObjectURL, producing a `blob:` URL — img-src 'self' rejects
- // those. Add `blob:` so the overlay doesn't throw a CSP violation.
- patched = appendOriginToDirective(patched, 'img-src', 'blob:');
- if (patched === original) continue;
-
- const newContentAttr = `content=${contentAttr.quote}${patched}${contentAttr.quote}`;
- const marker = `${CSP_MARKER_ATTR}="${Buffer.from(original, 'utf-8').toString('base64')}"`;
- // The tagRe captures any whitespace between the last attribute and the
- // closing `/>` as part of `attrs`. Naively appending ` ${marker}` after
- // a replace would land it BEFORE that trailing space, leaving a double
- // space inside attrs and clobbering the space before `/>`. Split off
- // the trailing whitespace, splice the marker into the attribute body,
- // and re-append the original trailing whitespace so a self-closing
- // `` round-trips byte-for-byte.
- const trailingWs = (attrs.match(/[ \t]*$/) || [''])[0];
- const attrsBody = attrs.slice(0, attrs.length - trailingWs.length);
- const newAttrs = attrsBody.replace(contentAttr.full, newContentAttr) + ' ' + marker + trailingWs;
- const newTag = tag.full.replace(attrs, newAttrs);
-
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
- }
- return result;
-}
-
-export function revertCspMeta(content) {
- const tags = findCspMetaTags(content);
- if (tags.length === 0) return content;
-
- let result = content;
- for (let i = tags.length - 1; i >= 0; i--) {
- const tag = tags[i];
- const origAttr = getAttr(tag.attrs, CSP_MARKER_ATTR);
- if (!origAttr) continue;
- const contentAttr = getAttr(tag.attrs, 'content');
- if (!contentAttr) continue;
-
- let originalValue;
- try { originalValue = Buffer.from(origAttr.value, 'base64').toString('utf-8'); }
- catch { continue; }
-
- const newContentAttr = `content=${contentAttr.quote}${originalValue}${contentAttr.quote}`;
- let newAttrs = tag.attrs.replace(contentAttr.full, newContentAttr);
- // Drop the marker attribute and any single space immediately preceding it.
- newAttrs = newAttrs.replace(new RegExp(`\\s*${origAttr.full}`), '');
- const newTag = tag.full.replace(tag.attrs, newAttrs);
-
- result = result.slice(0, tag.start) + newTag + result.slice(tag.end);
- }
- return result;
-}
-
-/** The journal's undo for a tag-strategy patch: drop the block, restore CSP. */
-export function unpatchTagFile(content) {
- return revertCspMeta(removeTag(content));
-}
diff --git a/skill/scripts/live/frameworks/tanstack-start.mjs b/skill/scripts/live/frameworks/tanstack-start.mjs
deleted file mode 100644
index 9bfb3db4a..000000000
--- a/skill/scripts/live/frameworks/tanstack-start.mjs
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * TanStack Start registry entry.
- *
- * Detection and the apply/remove pair are the existing adapter's
- * (`../tanstack-adapter.mjs`); this file only declares them to the registry
- * and names the artifacts the journal has to be able to heal.
- */
-
-import {
- TANSTACK_MARKER_OPEN,
- applyTanStackLiveAdapter,
- detectTanStackStartProject,
- removeTanStackLiveAdapter,
- unpatchTanStackRoot,
-} from '../tanstack-adapter.mjs';
-
-export const tanstackStart = {
- name: 'tanstack-start',
-
- detect(cwd) {
- return detectTanStackStartProject(cwd);
- },
-
- inject: {
- kind: 'adapter',
-
- apply({ cwd, port, token, project }) {
- return applyTanStackLiveAdapter({ cwd, port, token, project });
- },
-
- remove({ cwd, project }) {
- return removeTanStackLiveAdapter({ cwd, project });
- },
-
- // The mount component's extension follows the root route's, so the path
- // cannot live in the static ignore list.
- ignorePatterns(project) {
- return project?.componentFile ? [project.componentFile] : [];
- },
-
- artifacts({ project }) {
- if (!project) return [];
- return [
- {
- kind: 'created',
- path: project.componentFile,
- marker: 'impeccable-live-tanstack',
- pruneTo: 'src',
- },
- {
- kind: 'patched',
- path: project.rootRoute,
- patch: 'tanstack-root',
- markers: [TANSTACK_MARKER_OPEN],
- },
- ];
- },
-
- unpatch: {
- 'tanstack-root': unpatchTanStackRoot,
- },
- },
-
- source: {
- extensions: ['.tsx', '.jsx'],
- preview: 'source',
- styleMode: 'scoped',
- commentSyntax: 'jsx',
- },
-};
diff --git a/skill/scripts/live/frameworks/vite-generic.mjs b/skill/scripts/live/frameworks/vite-generic.mjs
deleted file mode 100644
index 4713670f4..000000000
--- a/skill/scripts/live/frameworks/vite-generic.mjs
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Generic Vite registry entry: a bundled app with a real `index.html` entry
- * and no framework-specific document ownership. React, Vue, Solid, Preact and
- * a plain TanStack Router SPA all land here — the marker-wrapped script block
- * goes straight into the HTML entry.
- *
- * This is the entry that catches everything with a bundler config; only
- * static-html sits below it.
- */
-
-import { fileExists, findConfigFile, hasAnyDependency } from './detect-utils.mjs';
-
-const VITE_CONFIG_RE = /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
-
-export function detectViteProject(cwd = process.cwd()) {
- const configFile = findConfigFile(cwd, VITE_CONFIG_RE);
- if (configFile) return { configFile, via: 'config' };
- if (hasAnyDependency(cwd, ['vite'])) return { configFile: null, via: 'package' };
- // A zero-config Vite app is index.html + package.json, the same pair
- // roots.mjs treats as an app root.
- if (fileExists(cwd, 'index.html') && fileExists(cwd, 'package.json')) {
- return { configFile: null, via: 'zero-config' };
- }
- return null;
-}
-
-export const viteGeneric = {
- name: 'vite-generic',
-
- detect(cwd) {
- return detectViteProject(cwd);
- },
-
- inject: { kind: 'tag' },
-
- source: {
- extensions: ['.tsx', '.jsx'],
- preview: 'source',
- styleMode: 'scoped',
- commentSyntax: 'jsx',
- },
-};
diff --git a/skill/scripts/live/generation-preflight.mjs b/skill/scripts/live/generation-preflight.mjs
deleted file mode 100644
index bfe81b32f..000000000
--- a/skill/scripts/live/generation-preflight.mjs
+++ /dev/null
@@ -1,149 +0,0 @@
-import { execFile } from 'node:child_process';
-import path from 'node:path';
-import { promisify } from 'node:util';
-
-const execFileAsync = promisify(execFile);
-const PREFLIGHT_TIMEOUT_MS = 15_000;
-
-// Per-target cache of the resolved source file. The wrap search walks the whole
-// project tree and was measured at ~7.6s on a large repo; it re-ran on every
-// generate for the same picked element (re-rolls, param passes). Keyed by the
-// target signature (locator + route), so it invalidates automatically when the
-// element or route changes; a failed resolution evicts its entry (see below).
-const sourceResolutionCache = new Map();
-
-/** Test/lifecycle hook: drop all cached source resolutions. */
-export function clearSourceResolutionCache() {
- sourceResolutionCache.clear();
-}
-
-function targetSignature(event) {
- const isInsert = event.mode === 'insert';
- const target = isInsert ? insertTarget(event) : replaceTarget(event);
- return JSON.stringify({
- mode: isInsert ? 'insert' : 'replace',
- position: isInsert ? target.position : null,
- elementId: target.elementId || null,
- classes: target.classes || null,
- tag: target.tag || null,
- pageUrl: event.pageUrl || null,
- });
-}
-
-export function buildGenerationPreflight(event, scriptsDir, { cache = null } = {}) {
- if (!event || event.type !== 'generate' || !event.id) return null;
-
- const isInsert = event.mode === 'insert';
- const target = isInsert ? insertTarget(event) : replaceTarget(event);
- if (!target.elementId && !target.classes) return null;
-
- const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
- const args = [script, '--id', event.id, '--count', String(event.count || 3)];
- // Compute the scaffold but do not write it into source for source-preview
- // targets. The agent writes wrapper + variants atomically; a premature
- // server-side write reloads the framework and strands the browser at 0/N.
- // No-op on the svelte-component path, which never writes the route source.
- args.push('--defer-source-write');
- if (isInsert) args.push('--position', target.position);
- if (target.elementId) args.push('--element-id', target.elementId);
- if (target.classes) args.push('--classes', target.classes);
- if (target.tag) args.push('--tag', target.tag);
- if (target.text) args.push('--text', target.text);
- if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl);
- const signature = targetSignature(event);
- // A cached resolution points the helper straight at the file, skipping the
- // tree search. The helper still reads current content, so line ranges stay
- // fresh; only discovery is cached.
- const cachedFile = cache ? cache.get(signature) : null;
- if (cachedFile) args.push('--file', cachedFile);
- return { script, args, mode: isInsert ? 'insert' : 'replace', signature };
-}
-
-/**
- * Scaffold the source for a generate event before handing it to an agent.
- *
- * Async on purpose. This spawns `live-wrap.mjs`, which walks the project's
- * source tree and can take seconds (measured at ~7.6s on a large repo when the
- * element is not found, with a 15s ceiling). The live server is single-threaded
- * and calls this while leasing a poll, so a synchronous spawn froze the whole
- * server for that entire window: Accept and Discard POSTs, SSE progress
- * broadcasts, and every other poll stalled behind it.
- */
-export async function runGenerationPreflight(event, {
- cwd = process.cwd(),
- scriptsDir,
- execFileImpl = execFileAsync,
- timeoutMs = PREFLIGHT_TIMEOUT_MS,
- cache = sourceResolutionCache,
-} = {}) {
- const command = buildGenerationPreflight(event, scriptsDir, { cache });
- if (!command) {
- return { ok: false, skipped: true, reason: 'insufficient_locator' };
- }
-
- const startedAt = performance.now();
- try {
- const { stdout } = await execFileImpl(process.execPath, command.args, {
- cwd,
- encoding: 'utf-8',
- timeout: timeoutMs,
- });
- const line = String(stdout).trim().split('\n').filter(Boolean).pop();
- if (!line) throw new Error('preflight returned no scaffold metadata');
- const scaffold = JSON.parse(line);
- // Cache the resolved SOURCE file (route source, not the svelte manifest) so
- // the next generate on this target skips the tree search.
- const resolvedSource = scaffold.sourceFile || scaffold.file;
- if (cache && command.signature && typeof resolvedSource === 'string') {
- cache.set(command.signature, resolvedSource);
- }
- return {
- ok: true,
- mode: command.mode,
- durationMs: performance.now() - startedAt,
- scaffold,
- };
- } catch (error) {
- // Evict a stale/failed resolution so the next attempt does a full search
- // (the element may have moved out of the previously cached file).
- if (cache && command.signature) cache.delete(command.signature);
- return {
- ok: false,
- mode: command.mode,
- durationMs: performance.now() - startedAt,
- error: compactError(error),
- };
- }
-}
-
-function replaceTarget(event) {
- return normalizeTarget(event.element || {});
-}
-
-function insertTarget(event) {
- return {
- ...normalizeTarget(event.insert?.anchor || {}),
- position: event.insert?.position === 'before' ? 'before' : 'after',
- };
-}
-
-function normalizeTarget(target) {
- const classes = Array.isArray(target.classes)
- ? target.classes.join(' ')
- : String(target.classes || '').trim();
- const text = typeof target.textContent === 'string'
- ? target.textContent.trim().slice(0, 80)
- : '';
- return {
- elementId: target.id || target.elementId || undefined,
- classes: classes || undefined,
- tag: target.tagName || target.tag || undefined,
- text: text || undefined,
- };
-}
-
-function compactError(error) {
- const stderr = error?.stderr ? String(error.stderr).trim() : '';
- const message = stderr.split('\n').filter(Boolean).pop() || error?.message || 'preflight failed';
- return String(message).slice(0, 500);
-}
diff --git a/skill/scripts/live/insert-ui.mjs b/skill/scripts/live/insert-ui.mjs
deleted file mode 100644
index ae54f6f93..000000000
--- a/skill/scripts/live/insert-ui.mjs
+++ /dev/null
@@ -1,458 +0,0 @@
-/**
- * Pure helpers for live-mode insert UI (browser + tests).
- * Kept separate from live-browser.js so insert logic is unit-testable.
- */
-
-export const PLACEHOLDER_DEFAULT_HEIGHT = 80;
-export const PLACEHOLDER_MIN_HEIGHT = 48;
-export const PLACEHOLDER_MIN_WIDTH = 120;
-
-/** @typedef {'before' | 'after'} InsertPosition */
-/** @typedef {'row' | 'column'} InsertAxis */
-
-/**
- * Infer sibling flow axis from a container's computed layout styles.
- * @param {{ display?: string, flexDirection?: string, gridTemplateColumns?: string, gridAutoFlow?: string }} style
- * @returns {InsertAxis}
- */
-export function detectInsertAxisFromStyle(style) {
- const display = style?.display || 'block';
- if (display.includes('flex')) {
- const dir = style.flexDirection || 'row';
- return dir.startsWith('row') ? 'row' : 'column';
- }
- if (display === 'grid' || display === 'inline-grid') {
- const flow = style.gridAutoFlow || 'row';
- if (flow.includes('column')) return 'column';
- const cols = (style.gridTemplateColumns || '').trim();
- if (cols && cols !== 'none') {
- const colCount = cols.split(/\s+/).filter(Boolean).length;
- if (colCount > 1) return 'row';
- }
- return 'row';
- }
- return 'column';
-}
-
-/**
- * Pick insertion side from pointer position against an anchor element box.
- * @param {number} clientX
- * @param {number} clientY
- * @param {{ top: number, left: number, width: number, height: number, bottom?: number, right?: number }} rect
- * @param {InsertAxis} [axis]
- * @returns {InsertPosition}
- */
-export function computeInsertPosition(clientX, clientY, rect, axis = 'column') {
- if (!rect) return 'after';
- if (axis === 'row') {
- if (!Number.isFinite(rect.left) || !Number.isFinite(rect.width) || rect.width <= 0) return 'after';
- const mid = rect.left + rect.width / 2;
- return clientX < mid ? 'before' : 'after';
- }
- if (!Number.isFinite(rect.top) || !Number.isFinite(rect.height) || rect.height <= 0) return 'after';
- const mid = rect.top + rect.height / 2;
- return clientY < mid ? 'before' : 'after';
-}
-
-/**
- * Whether Create is allowed for an insert session.
- * Requires a non-empty prompt OR at least one annotation.
- */
-export function canCreateInsert({ prompt, comments, strokes }) {
- const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0;
- const hasComments = Array.isArray(comments) && comments.length > 0;
- const hasStrokes = Array.isArray(strokes) && strokes.some(
- (s) => Array.isArray(s?.points) && s.points.length >= 2,
- );
- return hasPrompt || hasComments || hasStrokes;
-}
-
-/** Tooltip/title when Create is disabled. */
-export function insertCreateDisabledReason({ prompt, comments, strokes }) {
- if (canCreateInsert({ prompt, comments, strokes })) return null;
- return 'Add a prompt or annotate the placeholder to create';
-}
-
-/**
- * Fixed-position insert line coordinates (viewport px).
- * @param {{ top: number, left: number, width: number, height: number, bottom?: number, right?: number }} rect
- * @param {InsertPosition} position
- * @param {InsertAxis} [axis]
- */
-export function insertLineCoords(rect, position, axis = 'column') {
- if (axis === 'row') {
- const right = rect.right ?? rect.left + rect.width;
- const x = position === 'before' ? rect.left - 2 : right + 2;
- return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height };
- }
- const bottom = rect.bottom ?? rect.top + rect.height;
- const y = position === 'before' ? rect.top - 2 : bottom + 2;
- return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 };
-}
-
-/** Cursor while hovering an insert boundary. */
-export function cursorForInsertAxis(axis) {
- return axis === 'row' ? 'ew-resize' : 'ns-resize';
-}
-
-function groupSiblingRows(siblings, rowThreshold = 8) {
- const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
- const rows = [];
- for (const entry of sorted) {
- let placed = false;
- for (const row of rows) {
- if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) {
- row.push(entry);
- placed = true;
- break;
- }
- }
- if (!placed) rows.push([entry]);
- }
- return rows;
-}
-
-function horizontalOverlap(a, b) {
- const left = Math.max(a.left, b.left);
- const right = Math.min(a.right ?? a.left + a.width, b.right ?? b.left + b.width);
- return Math.max(0, right - left);
-}
-
-/**
- * Hit-test the gap between adjacent siblings (flex rows, grid columns, stacked blocks).
- * @param {number} clientX
- * @param {number} clientY
- * @param {Array<{ el: unknown, rect: { top: number, left: number, width: number, height: number, bottom?: number, right?: number } }>} siblings
- * @param {{ slop?: number, minOverlap?: number }} [opts]
- */
-export function hitSiblingInsertGap(clientX, clientY, siblings, opts = {}) {
- if (!Array.isArray(siblings) || siblings.length < 2) return null;
- const slop = opts.slop ?? 12;
- const minOverlap = opts.minOverlap ?? 0.25;
-
- for (const row of groupSiblingRows(siblings)) {
- if (row.length < 2) continue;
- const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left);
- for (let i = 0; i < sorted.length - 1; i++) {
- const a = sorted[i];
- const b = sorted[i + 1];
- const aRight = a.rect.right ?? a.rect.left + a.rect.width;
- const bLeft = b.rect.left;
- if (bLeft <= aRight) continue;
- const top = Math.max(a.rect.top, b.rect.top);
- const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height;
- const bBottom = b.rect.bottom ?? b.rect.top + b.rect.height;
- const bottom = Math.min(aBottom, bBottom);
- const span = bottom - top;
- const minH = Math.min(a.rect.height, b.rect.height);
- if (span < minH * minOverlap) continue;
-
- const inX = clientX >= aRight - slop && clientX <= bLeft + slop;
- const inY = clientY >= top - slop && clientY <= bottom + slop;
- if (!inX || !inY) continue;
-
- const midX = (aRight + bLeft) / 2;
- return {
- anchor: b.el,
- position: 'before',
- axis: 'row',
- line: { axis: 'row', left: midX, top, width: 0, height: span },
- };
- }
- }
-
- const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
- for (let i = 0; i < sortedCol.length - 1; i++) {
- const a = sortedCol[i];
- const b = sortedCol[i + 1];
- const overlap = horizontalOverlap(a.rect, b.rect);
- const minW = Math.min(a.rect.width, b.rect.width);
- if (overlap < minW * minOverlap) continue;
-
- const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height;
- const gapTop = aBottom;
- const gapBottom = b.rect.top;
- if (gapBottom <= gapTop) continue;
-
- const overlapLeft = Math.max(a.rect.left, b.rect.left);
- const overlapRight = Math.min(
- a.rect.right ?? a.rect.left + a.rect.width,
- b.rect.right ?? b.rect.left + b.rect.width,
- );
- const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop;
- const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop;
- if (!inY || !inX) continue;
-
- const midY = (gapTop + gapBottom) / 2;
- return {
- anchor: b.el,
- position: 'before',
- axis: 'column',
- line: { axis: 'column', top: midY, left: overlapLeft, width: overlap, height: 0 },
- };
- }
-
- return null;
-}
-
-/**
- * Resolve insert hover target, side, axis, and indicator line for the pointer.
- */
-export function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) {
- const gap = hitSiblingInsertGap(clientX, clientY, siblings);
- if (gap) return gap;
-
- const position = computeInsertPosition(clientX, clientY, rect, axis);
- const line = insertLineCoords(rect, position, axis);
- return { anchor: target, position, axis, line };
-}
-
-/**
- * How the in-flow placeholder should participate in layout.
- * Prefer implicit sizing (flex / %) so row inserts don't inherit the full parent width in px.
- * @returns {{ kind: 'flex', flex: string, minWidth: number } | { kind: 'percent' } | { kind: 'auto' } | { kind: 'explicit', width: number }}
- */
-export function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) {
- const display = parentDisplay || 'block';
- const w = Number.isFinite(parentWidth) ? parentWidth : 0;
-
- if (axis === 'row') {
- if (display.includes('flex')) {
- const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto'
- ? anchorFlex
- : '1 1 0';
- return { kind: 'flex', flex, minWidth: 0 };
- }
- if (display === 'grid' || display === 'inline-grid') {
- return { kind: 'auto' };
- }
- }
-
- if (w >= PLACEHOLDER_MIN_WIDTH) {
- return { kind: 'percent' };
- }
-
- return {
- kind: 'explicit',
- width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH),
- };
-}
-
-/** Width kinds that need materializing to px before edge-resize. */
-export function placeholderWidthIsImplicit(kind) {
- return kind === 'flex' || kind === 'percent' || kind === 'auto';
-}
-
-/**
- * Clamp user-resized placeholder dimensions.
- */
-export function clampPlaceholderSize(width, height, parentWidth, opts = {}) {
- const minW = opts.minWidth ?? PLACEHOLDER_MIN_WIDTH;
- const minH = opts.minHeight ?? PLACEHOLDER_MIN_HEIGHT;
- const maxW = opts.maxWidth ?? Math.max(minW, parentWidth || minW);
- return {
- width: Math.min(maxW, Math.max(minW, Math.round(width))),
- height: Math.max(minH, Math.round(height)),
- };
-}
-
-/** CSS cursor for a placeholder edge resize handle. */
-export function cursorForPlaceholderEdge(edge) {
- if (edge === 'n' || edge === 's') return 'ns-resize';
- if (edge === 'e' || edge === 'w') return 'ew-resize';
- return 'default';
-}
-
-/**
- * Compute placeholder box after dragging one edge (in-flow margins shift for n/w).
- * @param {{ width: number, height: number, marginLeft?: number, marginTop?: number }} start
- * @param {'n'|'e'|'s'|'w'} edge
- * @param {number} dx pointer delta X since drag start
- * @param {number} dy pointer delta Y since drag start
- * @param {number} parentWidth
- */
-export function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth, opts = {}) {
- const base = {
- width: start.width,
- height: start.height,
- marginLeft: start.marginLeft ?? 0,
- marginTop: start.marginTop ?? 0,
- };
- if (edge === 'e') base.width = start.width + dx;
- else if (edge === 'w') {
- base.width = start.width - dx;
- base.marginLeft = start.marginLeft + dx;
- } else if (edge === 's') base.height = start.height + dy;
- else if (edge === 'n') {
- base.height = start.height - dy;
- base.marginTop = start.marginTop + dy;
- }
-
- const clamped = clampPlaceholderSize(base.width, base.height, parentWidth, opts);
- if (edge === 'w') {
- base.marginLeft = start.marginLeft + start.width - clamped.width;
- } else if (edge === 'n') {
- base.marginTop = start.marginTop + start.height - clamped.height;
- }
-
- return {
- width: clamped.width,
- height: clamped.height,
- marginLeft: Math.round(base.marginLeft),
- marginTop: Math.round(base.marginTop),
- };
-}
-
-/** Pick and insert toggles are independent but turning one ON turns the other OFF. */
-export function applyPickToggle(pickActive, insertActive) {
- const nextPick = !pickActive;
- return {
- pickActive: nextPick,
- insertActive: nextPick ? false : insertActive,
- };
-}
-
-export function applyInsertToggle(pickActive, insertActive) {
- const nextInsert = !insertActive;
- return {
- pickActive: nextInsert ? false : pickActive,
- insertActive: nextInsert,
- };
-}
-
-/**
- * Build the browser generate payload for insert mode.
- */
-export function buildInsertGeneratePayload({
- id,
- count,
- pageUrl,
- anchorContext,
- position,
- placeholder,
- freeformPrompt,
- comments,
- strokes,
- screenshotPath,
-}) {
- const payload = {
- type: 'generate',
- mode: 'insert',
- id,
- count,
- pageUrl,
- insert: {
- position,
- anchor: anchorContext,
- },
- placeholder,
- freeformPrompt: freeformPrompt?.trim() || undefined,
- };
- if (comments?.length) payload.comments = comments;
- if (strokes?.length) payload.strokes = strokes;
- if (screenshotPath) payload.screenshotPath = screenshotPath;
- return payload;
-}
-
-/**
- * Whether a variant wrapper is currently shown (handles `hidden` and display:none).
- * @param {{ hidden?: boolean, style?: { display?: string } } | null | undefined} el
- */
-export function isVariantShown(el) {
- if (!el) return false;
- if (el.hidden) return false;
- if (el.style?.display === 'none') return false;
- return true;
-}
-
-/**
- * Show or hide a variant wrapper for cycling.
- * @param {{ hidden?: boolean, style?: { display?: string }, removeAttribute?: (name: string) => void, setAttribute?: (name: string, value?: string) => void } | null | undefined} el
- * @param {boolean} shown
- */
-export function setVariantShown(el, shown) {
- if (!el) return;
- if (shown) {
- el.removeAttribute?.('hidden');
- if (el.style) el.style.display = '';
- } else {
- el.setAttribute?.('hidden', '');
- if (el.style) el.style.display = 'none';
- }
-}
-
-/**
- * Pick the best live anchor during an insert session (placeholder until variants land).
- * @param {{
- * wrapper?: unknown,
- * variantCount?: number,
- * visibleVariant?: number,
- * placeholder?: unknown,
- * insertAnchor?: unknown,
- * pickVariantContent?: (wrapper: unknown, index: number) => unknown,
- * }} opts
- */
-export function resolveInsertSessionAnchor(opts) {
- const {
- wrapper,
- variantCount = 0,
- visibleVariant = 0,
- placeholder,
- insertAnchor,
- pickVariantContent,
- } = opts || {};
- if (wrapper && variantCount > 0 && visibleVariant > 0 && pickVariantContent) {
- const vis = pickVariantContent(wrapper, visibleVariant);
- if (vis) return vis;
- }
- return placeholder || insertAnchor || null;
-}
-
-/**
- * Snapshot placeholder geometry + anchor fingerprint so HMR can recreate the box.
- * @param {{
- * tagName?: string,
- * className?: string,
- * textContent?: string,
- * }} anchor
- * @param {{
- * offsetWidth?: number,
- * offsetHeight?: number,
- * style?: { marginLeft?: string, marginTop?: string },
- * }} placeholder
- * @param {{ position: 'before' | 'after', layoutAxis?: 'row' | 'column' }} meta
- */
-export function buildInsertPlaceholderSnapshot(anchor, placeholder, { position, layoutAxis }) {
- return {
- width: Math.round(placeholder.offsetWidth || 0),
- height: Math.round(placeholder.offsetHeight || PLACEHOLDER_DEFAULT_HEIGHT),
- marginLeft: parseFloat(placeholder.style?.marginLeft || '') || 0,
- marginTop: parseFloat(placeholder.style?.marginTop || '') || 0,
- position,
- layoutAxis: layoutAxis || 'column',
- anchorTag: anchor.tagName || 'DIV',
- anchorClasses: anchor.className || '',
- anchorText: (anchor.textContent || '').trim().slice(0, 120),
- };
-}
-
-/**
- * Re-find an insert anchor after framework HMR replaced the live DOM node.
- * @param {Pick} doc
- * @param {ReturnType | null | undefined} snapshot
- * @param {Element | null | undefined} liveAnchor
- */
-export function findInsertAnchorInDom(doc, snapshot, liveAnchor = null) {
- if (liveAnchor && doc.body.contains(liveAnchor)) return liveAnchor;
- if (!snapshot) return null;
- const tag = (snapshot.anchorTag || 'div').toLowerCase();
- const cls = (snapshot.anchorClasses || '').split(/\s+/).filter(Boolean)[0];
- const needle = snapshot.anchorText || '';
- const sel = cls ? `${tag}.${cls}` : tag;
- const candidates = doc.querySelectorAll(sel);
- for (const candidate of candidates) {
- if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue;
- return candidate;
- }
- return null;
-}
diff --git a/skill/scripts/live/instructions.mjs b/skill/scripts/live/instructions.mjs
deleted file mode 100644
index 19f6a1ae3..000000000
--- a/skill/scripts/live/instructions.mjs
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Just-in-time agent instructions for live mode.
- *
- * The live scripts, not the reference doc, own situational plumbing: every
- * event printed by live-poll carries an `_instructions` string describing
- * exactly what to do NEXT, with real ids, paths, and line numbers already
- * substituted and only the active path's rules included (a svelte-component
- * session never sees JSX guidance, and vice versa). live.md stays lean: the
- * session contract, harness policy, and design-quality guidance that is not
- * situational (identity lock, variation axes, parameter budgets).
- *
- * Keep these strings imperative, concrete, and short. They are read by an
- * agent mid-session; every sentence must earn its tokens. Instructions are
- * versioned with the scripts, so they cannot drift from behavior the way a
- * hand-maintained doc can.
- */
-
-const PLAN_POINTER = 'Plan per live.md section 4: extract the identity lock, pick default vs departure mode, commit each variant to a DIFFERENT primary axis, squint-test the trio. Size parameter knobs per section 7 budgets.';
-
-function pollCmd(scriptsPath) {
- return `node ${scriptsPath}/live-poll.mjs`;
-}
-
-function replyCmd(scriptsPath, id, rest) {
- return `${pollCmd(scriptsPath)} --reply ${id} ${rest}`;
-}
-
-export function instructionsForEvent(event, { scriptsPath = '{{scripts_path}}' } = {}) {
- if (!event || typeof event !== 'object') return undefined;
- switch (event.type) {
- case 'generate':
- return generateInstructions(event, scriptsPath);
- case 'steer':
- return `Do what the message asks (page edits, navigation help, or a short answer). Then reply exactly once: ${replyCmd(scriptsPath, event.id, 'steer_done ["optional short toast"]')} (on failure: --reply ${event.id} error "Short reason"). No pickup ack; poll again immediately after.`;
- case 'prefetch':
- return `Speculative pre-read, no reply owed: resolve ${JSON.stringify(event.pageUrl || '/')} to its source file (root "/" is usually the boot's pageFile; multi-page sites map /foo to public/foo/index.html; SPAs map all routes to one entry), read it into context, then poll again. Skip if you cannot resolve it confidently.`;
- case 'variant_mount_failed':
- return `The browser could NOT render variant ${event.variant}${event.url ? ` (module: ${event.url})` : ''}${event.error ? `: ${String(event.error).slice(0, 200)}` : ''}. The user sees a persistent error card, not variants. Fix the variant source files, then reply ${replyCmd(scriptsPath, event.id, 'done --file ')}; the browser retries on its own. Poll again after the reply.`;
- case 'accept':
- return acceptInstructions(event, scriptsPath);
- case 'discard':
- return event?._completionAck?.ok === true
- ? 'Original restored and durable completion acknowledged; nothing to do. Poll again.'
- : `Completion was not acknowledged: run node ${scriptsPath}/live-complete.mjs --id ${event.id} --discarded, then poll again.`;
- case 'manual_edit_apply':
- return `The user already clicked Apply; never ask, discard, or redirect. Delegate the source edits to the impeccable_manual_edit_applier subagent when available (pass cwd, scripts path, event id, page URL, chunk/deadline, batch, evidencePath); it must not poll or reply. ${event.repair ? 'A `repair` payload is present: the previous Apply changed source but validation failed; fix the CURRENT source, never roll back yourself. ' : ''}Reply exactly once: ${replyCmd(scriptsPath, event.id, `done --data '{"status":"done","appliedEntryIds":[...],"failed":[],"files":[...],"notes":[]}'`)} (status "partial"/"error" with failed[] when not every entry applied). Then poll again.`;
- case 'timeout':
- return 'No event arrived; poll again immediately.';
- case 'exit':
- return `Session over: kill any background poll, then node ${scriptsPath}/live-server.mjs stop (removes the injected script tag). Sweep leftover impeccable-variants-start / impeccable-carbonize-start markers from source.`;
- default:
- return undefined;
- }
-}
-
-function generateInstructions(event, scriptsPath) {
- const id = event.id;
- const scaffold = event.scaffold;
- const steps = [];
-
- if (event.screenshotPath) {
- steps.push(`Read the annotated screenshot first: ${event.screenshotPath}. Comment {x,y} positions bind text to the child under that point; strokes read by shape (loop = emphasis on this thing, arrow = direction, cross = delete).`);
- } else {
- steps.push('No screenshot was sent (the user did not annotate); do not ask for one and do not screenshot the page. Work from element.outerHTML, the computed styles, and the prompt.');
- }
-
- if (event.mode === 'insert') {
- steps.push(insertScaffoldInstructions(event, scriptsPath));
- } else if (scaffold?.previewMode === 'svelte-component') {
- steps.push(svelteComponentInstructions(event, scaffold, scriptsPath));
- } else if (scaffold && scaffold.sourceWritten === false) {
- steps.push(deferredWrapperInstructions(event, scaffold, scriptsPath));
- } else if (scaffold) {
- steps.push(`The wrapper is already written into ${scaffold.file}. Splice preview CSS plus all ${event.count} variants at line ${scaffold.insertLine} in ONE edit, following the returned cssAuthoring contract (styleTag, selector strategy, forbidden patterns). Each variant div holds exactly ONE top-level element (same tag as the original); first visible, others display: none.`);
- } else {
- steps.push(`Preflight could not scaffold${event.scaffoldError ? ` (${event.scaffoldError})` : ''}. Run node ${scriptsPath}/live-wrap.mjs --id ${id} --count ${event.count} --element-id "${event.element?.id || ''}" --classes "${(event.element?.classes || []).join(',')}" --tag "${event.element?.tagName || ''}" --text "". Keep the flags separate; --text disambiguates repeated siblings. On a fallback error, follow live.md's Handle fallback.`);
- }
-
- steps.push(event.action && event.action !== 'impeccable'
- ? `Action is "${event.action}": read reference/${event.action}.md before planning; its MUST params are non-negotiable. ${PLAN_POINTER}`
- : `Freeform action: work from SKILL.md rules plus craft-floor.md; no sub-command file. ${PLAN_POINTER}`);
-
- steps.push(`When all ${event.count} variants are delivered: ${replyCmd(scriptsPath, id, 'done --file ')}. Then poll again. If generation fails after the browser flipped to GENERATING, reply --reply ${id} error "Short reason" so the bar resets (never live-accept --discard for this).`);
-
- return steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
-}
-
-function svelteComponentInstructions(event, scaffold, scriptsPath) {
- const dir = scaffold.componentDir;
- const count = event.count;
- return `Svelte component preview. EDIT the existing stubs ${dir}/v1.svelte ... v${count}.svelte in place; never delete or recreate them; do not read them back (the prop-substituted markup is in scaffold.componentStubMarkup). Keep the stub's control flow ({#each}, {#if}) and propContract prop names exactly; never flatten a loop into literal items. The stub \n`;
-}
-
-function buildInsertVariantStub(variantNum) {
- return `${buildPropsScript([])}Insert variant ${variantNum}
\n\n\n`;
-}
-
-/**
- * Scaffold a component-preview session. The scaffold is AST-based: the app's
- * own svelte compiler parses the selected markup, control-flow blocks are
- * preserved (an each collection crosses the prop contract as ONE structured
- * prop, its loop body verbatim), and constructs a detached preview cannot
- * support return `{ fallback: 'source-preview', reason }` so the caller keeps
- * the markup inside the route file instead of shipping a wrong preview.
- */
-export function scaffoldSvelteComponentSession({
- id,
- count,
- sourceFile,
- sourceStartLine,
- sourceEndLine,
- originalLines,
- cwd = process.cwd(),
-}) {
- const originalMarkup = originalLines.join('\n');
-
- const compiler = loadSvelteCompiler(cwd);
- if (!compiler) {
- return { fallback: 'source-preview', reason: 'svelte 5 compiler not resolvable from the app root' };
- }
- const analysis = analyzeSvelteMarkup(originalMarkup, compiler.parse);
- if (!analysis.ok) {
- return { fallback: 'source-preview', reason: analysis.reason };
- }
-
- ensureRuntimeHelper(cwd);
- const dir = componentSessionDir(id, cwd);
- fs.mkdirSync(dir, { recursive: true });
-
- const contract = analysis.contract;
- const seeded = extractMatchingSourceCss(
- safeReadSource(path.resolve(cwd, sourceFile)),
- originalMarkup,
- );
- const seededCss = seeded.css;
- // The preview compiles in isolation, so NONE of these source rules applied
- // to what the user approved. Accept enforces that preview truth: any of
- // them the variant does not re-declare is superseded and removed, instead
- // of re-attaching to the accepted markup through kept class names (the
- // ".decisions grid grabs the new board" failure). Only the CLASS-matched
- // selectors are candidates; tag rules style shared route elements.
- const seededSelectors = [...seeded.supersedable];
-
- const manifest = {
- id,
- previewMode: 'svelte-component',
- contractVersion: 2,
- sourceFile: sourceFile.split(path.sep).join('/'),
- sourceStartLine,
- sourceEndLine,
- count,
- propContract: contract,
- originalMarkup,
- seededSelectors,
- componentDir: path.relative(cwd, dir).split(path.sep).join('/'),
- // Absolute paths let the browser fall back to /@fs/ imports when the dev
- // server's base or root makes root-relative URLs miss, and probe whether
- // the preview tree is reachable at all before blaming a variant.
- componentDirAbs: dir.split(path.sep).join('/'),
- runtimeModule: `/${SVELTE_RUNTIME_FILE}`,
- runtimeModuleAbs: path.join(cwd, SVELTE_RUNTIME_FILE).split(path.sep).join('/'),
- probeModule: `/${SVELTE_PROBE_FILE}`,
- probeModuleAbs: path.join(cwd, SVELTE_PROBE_FILE).split(path.sep).join('/'),
- };
-
- 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, buildVariantStubV2(n, analysis.markupWithProps, contract, seededCss), 'utf-8');
- }
- }
-
- return {
- manifest,
- manifestFile: path.relative(cwd, path.join(dir, 'manifest.json')).split(path.sep).join('/'),
- componentDir: manifest.componentDir,
- propContract: contract,
- // Inlined so the generate event's scaffold payload carries the stub
- // shape; the agent edits vN.svelte in place instead of spending reads on
- // the manifest and stub files (or deleting and recreating them).
- stubMarkup: analysis.markupWithProps,
- seededCss,
- };
-}
-
-function safeReadSource(filePath) {
- try { return fs.readFileSync(filePath, 'utf-8'); } catch { return ''; }
-}
-
-function escapeSelectorToken(token) {
- return String(token).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-/**
- * Seed variant stubs with the source component's rules that already style the
- * selected markup, so variants start from the real cascade (a detached
- * preview inherits none of the route's compile-scoped CSS) instead of
- * reimplementing it blind.
- *
- * Returns { css, supersedable }. `css` is every matching rule (class OR tag
- * matched). `supersedable` holds only the CLASS-matched selectors: those are
- * the accept-time removal candidates. Tag selectors (h1, a, p) style shared
- * elements across the whole route, so they seed the preview but are never
- * candidates for removal.
- */
-export function extractMatchingSourceCss(routeSource, originalMarkup) {
- const empty = { css: '', supersedable: new Set() };
- const styleMatch = String(routeSource || '').match(/\n`
- : `\n\n`;
- return `${buildPropsScriptV2(contract)}${propsComment}${markupWithProps.trim()}\n${css}`;
-}
-
-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('/'),
- componentDirAbs: dir.split(path.sep).join('/'),
- runtimeModule: `/${SVELTE_RUNTIME_FILE}`,
- runtimeModuleAbs: path.join(cwd, SVELTE_RUNTIME_FILE).split(path.sep).join('/'),
- probeModule: `/${SVELTE_PROBE_FILE}`,
- probeModuleAbs: path.join(cwd, SVELTE_PROBE_FILE).split(path.sep).join('/'),
- };
-
- 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);
- }
- // Legacy location: a session scaffolded by an older version can still be
- // accepted after an upgrade.
- const legacyDirect = path.join(cwd, LEGACY_SVELTE_COMPONENT_ROOT, id, 'manifest.json');
- if (fs.existsSync(legacyDirect)) {
- return readManifest(legacyDirect);
- }
- for (const rootRel of [SVELTE_COMPONENT_ROOT, LEGACY_SVELTE_COMPONENT_ROOT]) {
- const root = path.join(cwd, rootRel);
- if (!fs.existsSync(root)) continue;
- 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 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 compiler = loadSvelteCompiler(cwd);
- const mergedMarkup = mergeOriginalTopLevelAttrs(markup, manifest.originalMarkup || '');
-
- // Restore props back to route expressions. Contract v2 restores through the
- // AST so a prop used without braces (each headers, attribute positions)
- // still maps back to its original expression; v1 falls back to the textual
- // placeholder swap.
- let restoredText;
- if (Number(manifest.contractVersion) === 2 && compiler) {
- const restored = restoreSvelteMarkup(mergedMarkup, contract, compiler.parse);
- if (!restored.ok) {
- return { handled: false, error: 'Accepted variant does not parse: ' + restored.reason, ...resultBase };
- }
- restoredText = restored.markup;
- } else {
- restoredText = substitutePropsWithExprs(mergedMarkup, contract);
- }
- const restoredMarkup = restoredText.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 = reindentPreservingStructure(restoredMarkup, indent);
-
- let newLines = [
- ...sourceLines.slice(0, start),
- ...indentedMarkup,
- ...sourceLines.slice(end + 1),
- ];
-
- // Selectors that were already unused before this accept are the user's
- // pre-existing code; the pruning pass must not touch them.
- const preUnused = compiler ? collectUnusedSelectors(sourceContent, compiler.compile) : new Set();
-
- // Bake params (declared kinds from params.json drive branch pruning), then
- // MERGE into the component's existing style block: matching selectors are
- // replaced, new ones appended. Appending alone is how superseded rules used
- // to survive their own replacement.
- const declaredParams = readDeclaredParams(manifest, variantNum, cwd);
- let variantCss = cssLines.join('\n');
- if (/data-impeccable-variant|impeccable-variant-ready/.test(variantCss)) {
- // Defensive: strip preview-wrapper selectors that authoring rules forbid
- // on this path but an off-spec agent may still emit.
- variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n');
- }
- const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {});
- const cssStats = { replaced: 0, appended: 0, pruned: [], superseded: [] };
- if (bakedCss.trim()) {
- const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss);
- newLines = merged.text.split('\n');
- cssStats.replaced = merged.replaced;
- cssStats.appended = merged.appended;
- }
-
- let finalText = newLines.join('\n');
-
- // Preview truth: the detached preview never applied the source rules that
- // styled the replaced selection, so the user approved a design without
- // them. Any seeded selector the variant did not re-declare is superseded;
- // left in place it re-attaches through kept class names (the accepted root
- // keeps its original classes) and re-layouts markup it no longer owns.
- //
- // Removal is bounded by ownership: a selector whose classes are still used
- // by route markup OUTSIDE the replaced region does not belong to the pick
- // alone, and removing it would strip styling from markup this accept never
- // touched. Keeping it risks a visible re-attachment quirk on the accepted
- // region; deleting it breaks the rest of the route. Keep it.
- const outsideMarkup = [...sourceLines.slice(0, start), ...sourceLines.slice(end + 1)]
- .join('\n')
- .replace(/`;
- return {
- text: text.slice(0, lastMatch.index) + rebuilt + text.slice(lastMatch.index + lastMatch[0].length),
- removed,
- };
-}
-
-export function findLostSelectors(beforeSource, afterSource, prunedSelectors = []) {
- const before = collectAllSelectors(styleBlockText(beforeSource));
- const after = collectAllSelectors(styleBlockText(afterSource));
- const pruned = new Set((prunedSelectors || []).map((s) => normalizeSelector(s)));
- const lost = [];
- for (const selector of before) {
- if (!after.has(selector) && !pruned.has(selector)) lost.push(selector);
- }
- return lost;
-}
-
-function readDeclaredParams(manifest, variantNum, cwd) {
- try {
- const raw = JSON.parse(fs.readFileSync(path.join(cwd, manifest.componentDir, 'params.json'), 'utf-8'));
- const list = raw?.[String(variantNum)];
- return Array.isArray(list) ? list : [];
- } catch {
- return [];
- }
-}
-
-/**
- * Merge CSS into a svelte component's top-level style block (created when
- * absent), replacing rules whose selectors match and appending the rest.
- */
-export function mergeCssIntoSvelteSource(sourceText, incomingCss) {
- const text = String(sourceText || '');
- const styleRe = /\n`,
- replaced,
- appended,
- };
- }
-
- const inner = lastMatch[1];
- const { css, replaced, appended } = reconcileCss(inner, incomingCss);
- const openTag = lastMatch[0].slice(0, lastMatch[0].indexOf('>') + 1);
- const replacedBlock = `${openTag}\n${indentCssBlock(css)}\n`;
- return {
- text: text.slice(0, lastMatch.index) + replacedBlock + text.slice(lastMatch.index + lastMatch[0].length),
- replaced,
- appended,
- };
-}
-
-function indentCssBlock(css) {
- return String(css || '')
- .split('\n')
- .map((line) => (line.trim() === '' ? '' : ' ' + line))
- .join('\n');
-}
-
-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 = reindentPreservingStructure(restoredMarkup, indent);
-
- let newLines = [
- ...sourceLines.slice(0, insertIndex),
- ...indentedMarkup,
- ...sourceLines.slice(insertIndex),
- ];
-
- let variantCss = cssLines.join('\n');
- if (/data-impeccable-variant|impeccable-variant-ready/.test(variantCss)) {
- variantCss = sanitizeAcceptedSvelteCss(cssLines, variantNum, paramValues, rootTag).join('\n');
- }
- const declaredParams = readDeclaredParams(manifest, variantNum, cwd);
- const bakedCss = bakeParamValues(variantCss, declaredParams, paramValues || {});
- if (bakedCss.trim()) {
- const merged = mergeCssIntoSvelteSource(newLines.join('\n'), bakedCss);
- newLines = merged.text.split('\n');
- }
-
- 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);
-
- const verify = verifyAcceptedSource(newLines.join('\n'));
- return {
- handled: true,
- verify,
- ...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(SVELTE_ROOT_IMPORT_LINE_RE, '');
- 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) {
- return firstExistingFile(cwd, [
- 'src/routes/+layout.svelte',
- 'src/routes/(app)/+layout.svelte',
- ]) || 'src/routes/+layout.svelte';
-}
-
-function defaultSvelteLayout() {
- return `\n\n{@render children?.()}\n`;
-}
-
-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/skill/scripts/live/tanstack-adapter.mjs b/skill/scripts/live/tanstack-adapter.mjs
deleted file mode 100644
index b53978f4f..000000000
--- a/skill/scripts/live/tanstack-adapter.mjs
+++ /dev/null
@@ -1,259 +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}
- *