diff --git a/.changeset/median-headings-vessel-docs.md b/.changeset/median-headings-vessel-docs.md
new file mode 100644
index 00000000..6067b73c
--- /dev/null
+++ b/.changeset/median-headings-vessel-docs.md
@@ -0,0 +1,5 @@
+---
+"@design-intelligence/ghost": patch
+---
+
+Reframe the median as Ghost-stamped model truth and align Vessel's median check with heading anchors.
diff --git a/.changeset/median-one-home.md b/.changeset/median-one-home.md
new file mode 100644
index 00000000..388757b3
--- /dev/null
+++ b/.changeset/median-one-home.md
@@ -0,0 +1,5 @@
+---
+"@design-intelligence/ghost": minor
+---
+
+The median floor gets one authored home: `anti-goal.median` rules become markdown headings with per-rule check references, so `ghost validate` warns on every flag orphaned by pruning. All init templates (`skeleton`, `minimal`, `composition`) now stamp the median node; `ghost checks init` copies the paired check from the packaged payload and skips it when the node is absent.
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index d24ed68d..26d17987 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -51,7 +51,7 @@ a finding (exit `1`).
### Initialize: `init`
-Scaffold a `.ghost/` package with the skeleton starter: a manifest, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, four signature nodes as unanswered dials awaiting your brand's values, and `anti-goal.median` — the model's measured defaults as reject→replace rules, pruned per brand. Nothing in the skeleton is fiction: grammar and the median floor are safe to consume verbatim; the dials tell the agent to ask or flag, never freehand. Use `--template minimal` when you only want the small manifest/glossary/index starter. Add truths by adding files (`principle.density.md`). Use
+Scaffold a `.ghost/` package. Every template and body includes `anti-goal.median`; the skeleton starter also includes a manifest, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, and four signature nodes as unanswered dials awaiting your brand's values. The median node is Ghost's measured knowledge of unsteered model behavior — model truth, not brand truth — stamped at init and owned (pruned, adapted) by you thereafter. Nothing in the skeleton is fiction: grammar and the median floor are safe to consume verbatim; the dials tell the agent to ask or flag, never freehand. Use `--template minimal` when you only want the small manifest/glossary/index starter. Add truths by adding files (`principle.density.md`). Use
`--package
` for an exact directory, or set `GHOST_PACKAGE_DIR` when a host
wrapper stores Ghost files outside the default `.ghost`. Core `init` scaffolds
the fingerprint only; pass `--with checks` to also add the checks directory in
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index fa994988..3dd3cebe 100644
--- a/apps/docs/src/content/docs/getting-started.mdx
+++ b/apps/docs/src/content/docs/getting-started.mdx
@@ -80,7 +80,7 @@ by decision. One high-confidence truth beats an empty catalog.
The CLI handles the deterministic package work. Your agent handles the authoring:
interviewing you, working with the material you show it, drafting node prose,
-and asking you to curate the claims. `ghost init` scaffolds the skeleton starter: `manifest.yml`, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, four signature nodes as unanswered dials, and `anti-goal.median` — the model's measured defaults as reject→replace rules headed by a prune instruction. Nothing in the skeleton is fiction: grammar and the median floor hold for any brand, and each dial tells the agent to ask you or flag the gap rather than freehand a value. Answering the dials is how the skeleton becomes your brand. Use `ghost init --template minimal` when you only want the small manifest/glossary/index starter.
+and asking you to curate the claims. `ghost init` stamps `anti-goal.median` into every template and body; the median node is Ghost's measured knowledge of unsteered model behavior — model truth, not brand truth — owned (pruned, adapted) by you after init. The skeleton starter also includes `manifest.yml`, a `glossary.md` declaring the grammar/signature/register/anti-goal kinds, a core `index.md`, six grammar nodes of value-free decision logic, and four signature nodes as unanswered dials. Nothing in the skeleton is fiction: grammar and the median floor hold for any brand, and each dial tells the agent to ask you or flag the gap rather than freehand a value. Answering the dials is how the skeleton becomes your brand. Use `ghost init --template minimal` when you only want the small manifest/glossary/index starter.
```bash
ghost init
diff --git a/packages/ghost/src/commands/checks-command.ts b/packages/ghost/src/commands/checks-command.ts
index e1dd4b32..2fa22d7a 100644
--- a/packages/ghost/src/commands/checks-command.ts
+++ b/packages/ghost/src/commands/checks-command.ts
@@ -35,7 +35,11 @@ export function registerChecksCommand(cli: CAC): void {
if (opts.format === "json") {
process.stdout.write(
`${JSON.stringify(
- { dir: result.dir, written: result.written },
+ {
+ dir: result.dir,
+ written: result.written,
+ skipped: result.skipped,
+ },
null,
2,
)}\n`,
@@ -45,6 +49,9 @@ export function registerChecksCommand(cli: CAC): void {
for (const file of result.written) {
process.stdout.write(` ${file}\n`);
}
+ for (const file of result.skipped) {
+ process.stdout.write(` skipped ${file}\n`);
+ }
}
process.exit(0);
} catch (err) {
diff --git a/packages/ghost/src/commands/init-command.ts b/packages/ghost/src/commands/init-command.ts
index f9a7c9cf..02946d3c 100644
--- a/packages/ghost/src/commands/init-command.ts
+++ b/packages/ghost/src/commands/init-command.ts
@@ -78,7 +78,12 @@ export function registerInitCommand(cli: CAC): void {
dir: result.paths.dir,
written: result.written,
...(addedChecks !== undefined
- ? { checks: { written: addedChecks.written } }
+ ? {
+ checks: {
+ written: addedChecks.written,
+ skipped: addedChecks.skipped,
+ },
+ }
: {}),
},
null,
@@ -97,6 +102,9 @@ export function registerInitCommand(cli: CAC): void {
for (const file of addedChecks.written) {
process.stdout.write(` checks/${file}\n`);
}
+ for (const file of addedChecks.skipped) {
+ process.stdout.write(` skipped checks/${file}\n`);
+ }
}
}
process.exit(0);
diff --git a/packages/ghost/src/init-payloads/skeleton/anti-goal.median.md b/packages/ghost/src/init-payloads/median/anti-goal.median.md
similarity index 82%
rename from packages/ghost/src/init-payloads/skeleton/anti-goal.median.md
rename to packages/ghost/src/init-payloads/median/anti-goal.median.md
index 4e1dadf6..a0a5cefb 100644
--- a/packages/ghost/src/init-payloads/skeleton/anti-goal.median.md
+++ b/packages/ghost/src/init-payloads/median/anti-goal.median.md
@@ -4,81 +4,82 @@ description: "The model's median defaults this fingerprint refuses — gather fo
This is the model's median, not your brand. Each rule is reject→replace.
Delete every line your brand legitimately violates — `ghost validate` will
-surface any check the deletion orphans.
+warn on any check reference the deletion orphans — delete the paired flag too.
These are not aesthetic opinions. Where a count is given, it is the measured
convergence of 300 unsteered generations across three frontier models (the
antimedian experiment): the defaults a model reaches for when nobody hands it
a brand. An output showing several of these tells reads as generated,
whatever else it does right. A deterministic floor rides alongside these
-rules in checks — contrast, reduced motion, sane z-index, no overshoot
-easing — verified at review, never steered in prose.
+rules in the paired check — mechanical tells like gradient-filled text,
+emoji as interface icons, em-dash pileups, and aphoristic cadence —
+verified at review, never steered in prose.
-
+### Hover-lift
Reject hover-lift (`translateY` + growing shadow) as the default interaction
→ confirm with color and background change at the fingerprint's fast
duration. (measured: 341)
-
+### Indigo accent
Reject the indigo/blue/purple default accent (`#4f46e5`, `#2563eb`,
`#8b5cf6` family; purple/violet hue 260–310) → the fingerprint's declared
palette; absent one, monochrome plus a single functional accent.
-
+### Dark theme
Reject unprompted dark theme → the fingerprint's declared surface.
Dark-to-look-cool and light-to-be-safe are the same retreat from a decision.
(measured: 271)
-
+### Gradients
Reject gradient page and section backgrounds and gradient-filled CTAs → flat
surfaces in semantic roles.
-
+### Side-stripe
Reject side-stripe borders (a thick colored border on one side of a rounded
card) — the single most recognizable tell of AI-generated UI → a full
hairline border, a 4–8% surface tint, or a leading glyph.
-
+### Cream surface
Reject the cream/sand/beige default surface (warm off-white; token names
like `--cream`, `--sand`, `--parchment` are the tell) → a true off-white,
the brand's own hue, or a committed color; warmth via accent and type, not
the ground.
-
+### Glassmorphism
Reject glassmorphism (decorative backdrop blur) → flat surfaces with real
borders and the fingerprint's elevation tiers.
-
+### Chat bubbles
Reject chat bubbles with initials-circle avatars for assistant turns →
assistant text plain on the page surface; user turns marked compactly.
-
+### Stock copy
Reject stock template copy ("Simple, transparent pricing") → headings that
state what this product specifically does.
-
+### Celebration
Reject celebration copy and UI (exclamation success, confetti language) →
quiet factual confirmation.
-
+### Nested cards
Reject nested cards and everything-in-cards → one surface level per region;
interior hierarchy from spacing and type; spacing and alignment group
without card overhead.
-
+### Hero metric
Reject the hero-metric template (big number + small label + supporting stats
as default proof) → evidence specific to the product, or nothing. A
prominent metric showing real user data is fine.
-
+### Every button primary
Reject every button a primary button → one primary per view; the rest step
down the fingerprint's emphasis ladder.
-
+### Eyebrow kicker
Reject an eyebrow kicker on every section → at most one, where the register
sanctions it; one named kicker is voice, every-section is AI grammar.
-
+### Decorative motion
Reject decorative looping animation (pulse/float/shimmer) and uniform
fade-and-rise on every scrolled section → motion as evidence of state
change; loops only for genuine loading; decoration never compensates for
diff --git a/packages/ghost/src/init-payloads/median/median-tells.md b/packages/ghost/src/init-payloads/median/median-tells.md
new file mode 100644
index 00000000..fd8e8acd
--- /dev/null
+++ b/packages/ghost/src/init-payloads/median/median-tells.md
@@ -0,0 +1,117 @@
+---
+name: Median tells
+description: Flags the measured defaults of unsteered generation, the deterministic floor, and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, contrast, frequency tells, and per-model signatures.
+severity: high
+references:
+ - anti-goal.median > Hover-lift
+ - anti-goal.median > Indigo accent
+ - anti-goal.median > Dark theme
+ - anti-goal.median > Gradients
+ - anti-goal.median > Glassmorphism
+ - anti-goal.median > Side-stripe
+ - anti-goal.median > Cream surface
+ - anti-goal.median > Chat bubbles
+ - anti-goal.median > Stock copy
+ - anti-goal.median > Celebration
+ - anti-goal.median > Hero metric
+ - anti-goal.median > Eyebrow kicker
+---
+
+These flags target the measured convergence patterns of unsteered model
+generation, the deterministic floor the median node licenses, and tells
+specific to individual models. Each is mechanically detectable in a diff.
+Pruning a rule from `anti-goal.median` orphans its paired reference here —
+`ghost validate` warns; delete the flag and its reference together.
+
+Flag `transform` with `translateY` inside a `:hover` rule on cards,
+buttons, or list items, especially paired with a shadow increase. Hover
+confirmation in this fingerprint is color and background change, not lift.
+(`anti-goal.median > Hover-lift`)
+
+Flag accent values in the indigo/blue/purple default family (`#4f46e5`,
+`#6366f1`, `#2563eb`, `#3b82f6`, `#8b5cf6`, and close neighbors)
+unless the diff shows the user asked for them. They are model defaults, not
+palette members. (`anti-goal.median > Indigo accent`)
+
+Flag whole-page dark backgrounds when the ask did not request dark mode.
+Dark surfaces are a declared brand choice or an explicit theme, never an
+unprompted default. (`anti-goal.median > Dark theme`)
+
+Flag `linear-gradient` or `radial-gradient` as page or section
+backgrounds, and gradient-filled buttons. (`anti-goal.median > Gradients`)
+
+Flag `backdrop-filter: blur` used for glassmorphism cards.
+(`anti-goal.median > Glassmorphism`)
+
+Flag `background-clip: text` (with or without the `-webkit-` prefix)
+paired with a gradient. Emphasis comes from weight or size in a single
+solid color.
+
+Flag a thick colored border on one side of an element (`border-left` or a
+`border-l-*` utility at 2px or more in a non-neutral color) while the
+other sides stay thin. (`anti-goal.median > Side-stripe`)
+
+Flag warm off-white page backgrounds in the cream/sand/beige band, and token
+names like `--cream`, `--sand`, `--parchment`, `--linen` introduced
+by the diff. (`anti-goal.median > Cream surface`)
+
+Flag assistant messages rendered as bubbles with initials-circle avatars.
+(`anti-goal.median > Chat bubbles`)
+
+Flag emoji used as icons or imagery in interface chrome. Text labels carry
+meaning.
+
+Flag stock template copy in headings: "Simple, transparent pricing",
+"Welcome back", and interchangeable-with-a-competitor phrasing. Recommend
+copy that states what this product specifically does.
+(`anti-goal.median > Stock copy`)
+
+Flag exclamation-marked success copy, confetti language, and celebratory UI
+("You did it!", "Awesome!"). Confirmation is quiet and factual.
+(`anti-goal.median > Celebration`)
+
+Flag the hero-metric template — a big number, small label, and supporting
+stats as default proof — unless the metric shows real user data. Recommend
+evidence specific to the product, or nothing.
+(`anti-goal.median > Hero metric`)
+
+Deterministic floor — licensed by the median node, verified here, never
+steered in prose:
+
+Flag text/background pairs below WCAG AA contrast: 4.5:1 for body text,
+3:1 for large text (24px and up, or 18.7px bold and up). Check the worst
+stop when the background is a gradient.
+
+Flag animation or transition without a `prefers-reduced-motion: reduce`
+alternative — a crossfade or instant state change.
+
+Flag ad-hoc z-index values (`999`, `9999`, or any value outside a
+declared scale). Layering wants a semantic scale, not an arms race.
+
+Flag `cubic-bezier` easings whose control points overshoot the 0–1 range,
+and keyframe names matching bounce, elastic, wobble, or jiggle.
+
+Frequency tells — the crime is repetition, not the move (advisory):
+
+Flag three or more uppercase, tracked eyebrow kickers above section headings
+in one page. One named kicker is voice; a kicker on every section is model
+grammar. (`anti-goal.median > Eyebrow kicker`)
+
+Flag five or more em-dashes in body copy in one view.
+
+Flag three or more instances of the aphoristic rebuttal cadence ("Not X. Y."
+/ "Sentence. No qualifiers.") in one page's copy.
+
+Model-signature tells — skip any block whose model did not produce the diff
+(advisory):
+
+Codex: flag a 1px border paired with a box-shadow of 16px blur or more on
+the same element — pick a solid border or a tight shadow, not both. Flag
+border-radius of 32px or more on cards, sections, or inputs unless an
+answered shape dial (`signature.shape`) sanctions large radii — then it is
+fidelity, not a tell. Flag 1px linear-gradient grid or repeating-stripe
+backgrounds used as decoration.
+
+Gemini: flag `transform` (scale, rotate, translate) on `img` inside a
+`:hover` rule, including group-hover utilities targeting a child image.
+Animate the card's background, border, or shadow instead.
diff --git a/packages/ghost/src/init-payloads/skeleton/glossary.md b/packages/ghost/src/init-payloads/skeleton/glossary.md
index 725c4093..053ffb08 100644
--- a/packages/ghost/src/init-payloads/skeleton/glossary.md
+++ b/packages/ghost/src/init-payloads/skeleton/glossary.md
@@ -10,8 +10,8 @@ kinds:
# grammar
The brand's decision logic: closed sets, role vocabularies, and assembly
-rules, stated in token roles and never in literal values. Grammar survives a
-rebrand unchanged — swap every value and these nodes still hold. Gather
+rules, stated in token roles and never in literal values. Grammar survives an
+adaptation unchanged — swap every value and these nodes still hold. Gather
grammar before inventing structure.
# signature
diff --git a/packages/ghost/src/init-payloads/skeleton/index.md b/packages/ghost/src/init-payloads/skeleton/index.md
index b8a5b3b2..303364f0 100644
--- a/packages/ghost/src/init-payloads/skeleton/index.md
+++ b/packages/ghost/src/init-payloads/skeleton/index.md
@@ -2,13 +2,13 @@
description: "Always read first — the trust tiers of this starter fingerprint, the unanswered dials, and how to work before they are answered."
---
-This is a skeleton fingerprint: the rebrand-safe law of good interface work,
+This is a skeleton fingerprint: the adaptation-safe law of good interface work,
with every brand decision left explicitly open. It steers an agent away from
the model's median from the first generation, without pretending to be a
brand it is not.
The corpus carries two trust tiers. Grammar and the median floor are law:
-safe to consume verbatim, unchanged by any future rebrand — they speak in
+safe to consume verbatim, unchanged by any adaptation — they speak in
token roles and closed sets, never in literal values. Signature nodes are
dials, and in this starter every dial is unanswered: each states the fixed
relationship worth keeping and the question only a human can answer. Do not
diff --git a/packages/ghost/src/scan/check-scaffold.ts b/packages/ghost/src/scan/check-scaffold.ts
index d646edc5..4e23f9d9 100644
--- a/packages/ghost/src/scan/check-scaffold.ts
+++ b/packages/ghost/src/scan/check-scaffold.ts
@@ -2,109 +2,11 @@ import { access, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { UsageError } from "#ghost-core";
import { GHOST_CHECKS_DIR } from "./check-files.js";
+import { loadPayloadFile } from "./packed-payloads.js";
const EXAMPLE_CHECK_FILENAME = "example.md.example";
const MEDIAN_TELLS_FILENAME = "median-tells.md";
-const MEDIAN_TELLS_CONTENT = `---
-name: Median tells
-description: Flags the measured defaults of unsteered generation, the deterministic floor, and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, contrast, frequency tells, and per-model signatures.
-severity: high
-references:
- - anti-goal.median
- - grammar.motion
- - grammar.conversation
----
-
-These flags target the measured convergence patterns of unsteered model
-generation, the deterministic floor the median node licenses, and tells
-specific to individual models. Each is mechanically detectable in a diff.
-Pruning a rule from \`anti-goal.median\` orphans its paired flag here —
-delete both together.
-
-Flag \`transform\` with \`translateY\` inside a \`:hover\` rule on cards,
-buttons, or list items, especially paired with a shadow increase. Hover
-confirmation in this fingerprint is color and background change, not lift.
-(\`anti-goal.median > rule:median-hover-lift\`)
-
-Flag accent values in the indigo/blue/purple default family (\`#4f46e5\`,
-\`#6366f1\`, \`#2563eb\`, \`#3b82f6\`, \`#8b5cf6\`, and close neighbors)
-unless the diff shows the user asked for them. They are model defaults, not
-palette members. (\`rule:median-indigo-accent\`)
-
-Flag whole-page dark backgrounds when the ask did not request dark mode.
-Dark surfaces are a declared brand choice or an explicit theme, never an
-unprompted default. (\`rule:median-dark-theme\`)
-
-Flag \`linear-gradient\` or \`radial-gradient\` as page or section
-backgrounds, and gradient-filled buttons. (\`rule:median-gradients\`)
-
-Flag \`backdrop-filter: blur\` used for glassmorphism cards.
-(\`rule:median-glassmorphism\`)
-
-Flag \`background-clip: text\` (with or without the \`-webkit-\` prefix)
-paired with a gradient. Emphasis comes from weight or size in a single
-solid color.
-
-Flag a thick colored border on one side of an element (\`border-left\` or a
-\`border-l-*\` utility at 2px or more in a non-neutral color) while the
-other sides stay thin. (\`rule:median-side-stripe\`)
-
-Flag warm off-white page backgrounds in the cream/sand/beige band, and token
-names like \`--cream\`, \`--sand\`, \`--parchment\`, \`--linen\` introduced
-by the diff. (\`rule:median-cream-surface\`)
-
-Flag assistant messages rendered as bubbles with initials-circle avatars.
-(\`rule:median-chat-bubbles\`)
-
-Flag emoji used as icons or imagery in interface chrome. Text labels carry
-meaning.
-
-Flag stock template copy in headings: "Simple, transparent pricing",
-"Welcome back", and interchangeable-with-a-competitor phrasing. Recommend
-copy that states what this product specifically does.
-(\`rule:median-stock-copy\`)
-
-Deterministic floor — licensed by the median node, verified here, never
-steered in prose:
-
-Flag text/background pairs below WCAG AA contrast: 4.5:1 for body text,
-3:1 for large text (24px and up, or 18.7px bold and up). Check the worst
-stop when the background is a gradient.
-
-Flag animation or transition without a \`prefers-reduced-motion: reduce\`
-alternative — a crossfade or instant state change.
-
-Flag ad-hoc z-index values (\`999\`, \`9999\`, or any value outside a
-declared scale). Layering wants a semantic scale, not an arms race.
-
-Flag \`cubic-bezier\` easings whose control points overshoot the 0–1 range,
-and keyframe names matching bounce, elastic, wobble, or jiggle.
-
-Frequency tells — the crime is repetition, not the move (advisory):
-
-Flag three or more uppercase, tracked eyebrow kickers above section headings
-in one page. One named kicker is voice; a kicker on every section is model
-grammar. (\`rule:median-eyebrow-kicker\`)
-
-Flag five or more em-dashes in body copy in one view.
-
-Flag three or more instances of the aphoristic rebuttal cadence ("Not X. Y."
-/ "Sentence. No qualifiers.") in one page's copy.
-
-Model-signature tells — skip any block whose model did not produce the diff
-(advisory):
-
-Codex: flag a 1px border paired with a box-shadow of 16px blur or more on
-the same element — pick a solid border or a tight shadow, not both. Flag
-border-radius of 32px or more on cards, sections, or inputs. Flag 1px
-linear-gradient grid or repeating-stripe backgrounds used as decoration.
-
-Gemini: flag \`transform\` (scale, rotate, translate) on \`img\` inside a
-\`:hover\` rule, including group-hover utilities targeting a child image.
-Animate the card's background, border, or shadow instead.
-`;
-
const EXAMPLE_CHECK_CONTENT = `---
name: logo-clearspace-holds
description: Logo usage preserves clearspace, lockup integrity, and glyph rules.
@@ -121,6 +23,7 @@ is used when the full lockup is required.
export interface AddChecksResult {
dir: string;
written: string[];
+ skipped: string[];
}
/** Scaffold the flat `.ghost/checks/` directory with an example check. */
@@ -132,20 +35,32 @@ export async function addChecksDir(
throw new UsageError(`checks/ already exists at ${checksDir}.`);
}
+ const written: string[] = [];
+ const skipped: string[] = [];
+
await mkdir(checksDir, { recursive: true });
- await writeFile(
- join(checksDir, MEDIAN_TELLS_FILENAME),
- MEDIAN_TELLS_CONTENT,
- "utf-8",
- );
+ if (await exists(join(packageDir, "anti-goal.median.md"))) {
+ await writeFile(
+ join(checksDir, MEDIAN_TELLS_FILENAME),
+ await loadPayloadFile("median", MEDIAN_TELLS_FILENAME),
+ "utf-8",
+ );
+ written.push(MEDIAN_TELLS_FILENAME);
+ } else {
+ skipped.push(`${MEDIAN_TELLS_FILENAME} (no anti-goal.median node)`);
+ }
+
await writeFile(
join(checksDir, EXAMPLE_CHECK_FILENAME),
EXAMPLE_CHECK_CONTENT,
"utf-8",
);
+ written.push(EXAMPLE_CHECK_FILENAME);
+
return {
dir: checksDir,
- written: [MEDIAN_TELLS_FILENAME, EXAMPLE_CHECK_FILENAME],
+ written,
+ skipped,
};
}
diff --git a/packages/ghost/src/scan/fingerprint-package.ts b/packages/ghost/src/scan/fingerprint-package.ts
index 5f02bcd2..2255e023 100644
--- a/packages/ghost/src/scan/fingerprint-package.ts
+++ b/packages/ghost/src/scan/fingerprint-package.ts
@@ -425,7 +425,7 @@ function lintCheckReferences(
issues.push({
severity: "warning",
rule: "check-reference-unresolved",
- message: `check reference '${raw}' does not resolve to a fingerprint node`,
+ message: `check reference '${raw}' does not resolve to a fingerprint node — if you pruned this rule from the node, delete its paired flag in the check too`,
path: `checks/${check.id}.md.references`,
});
continue;
@@ -437,7 +437,7 @@ function lintCheckReferences(
issues.push({
severity: "warning",
rule: "check-reference-heading-missing",
- message: `check reference '${raw}' names a heading that was not found`,
+ message: `check reference '${raw}' names a heading that was not found — if you pruned this rule from the node, delete its paired flag in the check too`,
path: `checks/${check.id}.md.references`,
});
}
diff --git a/packages/ghost/src/scan/packed-payloads.ts b/packages/ghost/src/scan/packed-payloads.ts
index 9c06ff58..2c7142cb 100644
--- a/packages/ghost/src/scan/packed-payloads.ts
+++ b/packages/ghost/src/scan/packed-payloads.ts
@@ -19,10 +19,7 @@ const INIT_PAYLOAD_ROOTS = [
const BINARY_EXTENSIONS = new Set([".woff", ".woff2"]);
export async function loadPackedPayload(name: string): Promise {
- const payloadDir =
- INIT_PAYLOAD_ROOTS.map((root) => join(root, name)).find((dir) =>
- existsSync(dir),
- ) ?? join(INIT_PAYLOAD_ROOTS[0], name);
+ const payloadDir = resolvePayloadDir(name);
const files = await listPayloadFiles(payloadDir);
return Promise.all(
@@ -33,6 +30,21 @@ export async function loadPackedPayload(name: string): Promise {
);
}
+export async function loadPayloadFile(
+ payload: string,
+ relativePath: string,
+): Promise {
+ return readFile(join(resolvePayloadDir(payload), relativePath), "utf-8");
+}
+
+function resolvePayloadDir(name: string): string {
+ return (
+ INIT_PAYLOAD_ROOTS.map((root) => join(root, name)).find((dir) =>
+ existsSync(dir),
+ ) ?? join(INIT_PAYLOAD_ROOTS[0], name)
+ );
+}
+
async function listPayloadFiles(dir: string): Promise {
const entries = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(
diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts
index 0c842459..8de78dac 100644
--- a/packages/ghost/src/scan/templates.ts
+++ b/packages/ghost/src/scan/templates.ts
@@ -3,7 +3,7 @@ import {
GHOST_EVENTS_FILENAME,
LEGACY_PULL_HISTORY_FILENAME,
} from "./constants.js";
-import { loadPackedPayload } from "./packed-payloads.js";
+import { loadPackedPayload, loadPayloadFile } from "./packed-payloads.js";
/**
* A single seed file an `init` template writes, relative to the package dir.
*/
@@ -52,7 +52,8 @@ const MINIMAL_TEMPLATE: GhostInitTemplate = {
name: "minimal",
description:
"Minimal node package: manifest + glossary + a starter index node.",
- files() {
+ async files() {
+ const medianFile = await medianTemplateFile();
return [
manifestFile(),
gitignoreFile(),
@@ -87,6 +88,8 @@ evidence unless the node says the sample itself is normative.
What this brand must never look, sound, or feel like — named generic patterns
and rejected neighbors. Always-on, like a principle, but stated as the thing
to steer away from.
+\`anti-goal.median\` is the model's floor, not the brand's taste. Gather
+anti-goals before styling anything greenfield.
# asset
@@ -127,6 +130,7 @@ When a truth is narrower, state the condition in the prose — the situation whe
it applies — never a filing destination.
`,
},
+ medianFile,
];
},
};
@@ -146,7 +150,8 @@ const COMPOSITION_TEMPLATE: GhostInitTemplate = {
name: "composition",
description:
"Composition starter: minimal files + an invariants floor and a worked bound/open pattern.",
- files() {
+ async files() {
+ const medianFile = await medianTemplateFile();
return [
manifestFile(),
gitignoreFile(),
@@ -185,6 +190,8 @@ render travels with the prose.
What this brand must never look, sound, or feel like — named generic patterns
and rejected neighbors. Always-on, like a principle, but stated as the thing
to steer away from.
+\`anti-goal.median\` is the model's floor, not the brand's taste. Gather
+anti-goals before styling anything greenfield.
# asset
@@ -297,6 +304,7 @@ When a blessed render of this pattern exists, add an \`exemplar.*\` node with
\`materials\` pointing at the screenshot and the implementation path.
`,
},
+ medianFile,
];
},
};
@@ -324,7 +332,10 @@ const SKELETON_TEMPLATE: GhostInitTemplate = {
description:
"Naked skeleton: the median floor + grammar law, with the signature dials left unanswered.",
async files() {
- const skeletonFiles = await loadPackedPayload("skeleton");
+ const skeletonFiles = [
+ ...(await loadPackedPayload("skeleton")),
+ await medianTemplateFile(),
+ ];
skeletonFiles.sort(
(a, b) =>
(SKELETON_FILE_ORDER.get(a.relativePath) ?? Number.MAX_SAFE_INTEGER) -
@@ -336,6 +347,13 @@ const SKELETON_TEMPLATE: GhostInitTemplate = {
},
};
+async function medianTemplateFile(): Promise {
+ return {
+ relativePath: "anti-goal.median.md",
+ content: await loadPayloadFile("median", "anti-goal.median.md"),
+ };
+}
+
const TEMPLATES = new Map([
[MINIMAL_TEMPLATE.name, MINIMAL_TEMPLATE],
[COMPOSITION_TEMPLATE.name, COMPOSITION_TEMPLATE],
diff --git a/packages/ghost/src/skill-bundle/references/adapting-a-starter.md b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md
index 00ff794e..31792f16 100644
--- a/packages/ghost/src/skill-bundle/references/adapting-a-starter.md
+++ b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md
@@ -13,13 +13,13 @@ handoffs:
vessel-light`) or the naked skeleton (`ghost init`) — into *your* brand's
fingerprint without shipping a self-contradicting package.
-A starter is factored by rate of change under rebrand. Knowing which stratum a
+A starter is factored by rate of change under adaptation. Knowing which stratum a
file belongs to tells you what to do with it:
| Stratum | Files | On adaptation |
| --- | --- | --- |
-| Grammar | `grammar.*` | Keep unchanged — value-free decision logic that survives any rebrand. |
-| Median floor | `anti-goal.median` | Prune, never rewrite — delete lines your brand legitimately violates. |
+| Grammar | `grammar.*` | Keep unchanged — value-free decision logic that survives any adaptation. |
+| Median floor | `anti-goal.median` | Prune, never rewrite — Ghost stamps this measured model truth into every initialized package; you own it after init. |
| Signature | `signature.*` | Answer — each is a dial; restate it with your brand's answer. |
| Values | `materials/tokens.css` | Edit — the single injection point for every literal value. |
| Registers | `register.*` | Re-tune — conditions referencing signature ids; revisit after the dials change. |
@@ -34,14 +34,15 @@ itself.
1. **Change the manifest id.** Edit `id:` in `manifest.yml` to your brand's
name. This is deliberately first: it is the explicit act that marks the
- fork as begun. Until it changes, the package honestly claims to be the
+ adaptation as begun. Until it changes, the package honestly claims to be the
starter, and every consuming agent cites it as a starter default.
-2. **Prune `anti-goal.median`.** Read every rule; delete the lines your brand
- legitimately violates (a brand built on gradients deletes the gradient
- rule — that is the node working, not failing). Do not rewrite surviving
- rules; they are the model's measured floor, not your taste. Then run
- `ghost validate`: any check reference orphaned by a deletion surfaces as a
- warning — delete the paired check flag too.
+2. **Prune `anti-goal.median`.** Each rule is a `###` heading section; delete
+ the whole section for every rule your brand legitimately violates (a brand
+ built on gradients deletes the Gradients section — that is the node working,
+ not failing). Do not rewrite surviving rules; they are the model's measured
+ floor, not your taste. Then run `ghost validate`: every check reference
+ orphaned by a pruned heading surfaces as its own warning — delete the paired
+ flag and its reference from the check.
3. **Answer the signature dials.** Walk each `signature.*` node as a
questionnaire item. Keep the fixed relationship (the part the node marks
as worth keeping); replace the starter's answer — or the open question —
@@ -52,7 +53,7 @@ itself.
palette, type sizes, durations, eases. Change the values; keep the role
names. The role names are the grammar's vocabulary and the reason the
grammar nodes survive untouched.
-5. **Regenerate the refs.** This is the step that decides whether the fork
+5. **Regenerate the refs.** This is the step that decides whether the adaptation
succeeded. Exemplars dominate prose: a prose rule contradicted by a stale
ref loses. Rebuild each `materials/ref/*.html` against the new tokens and
answered dials, keep the annotation headers (`normative-for` /
@@ -75,8 +76,11 @@ itself.
Work does not block on adaptation. Before the procedure runs (or midway
through it), cite starter content honestly:
-- Grammar and surviving median rules: **Ghost-backed** — they hold for any
+- Grammar: **Ghost-backed** — value-free decision logic that holds for any
brand.
+- Surviving median rules: **owner-backed after init** — Ghost stamps this
+ measured model truth into every initialized package; you own the pruning and
+ any adaptation thereafter.
- The starter's signature values (a body) or your provisional choices (the
skeleton): **Ghost-backed (starter default, unadapted)** or **provisional**
— never plain brand truth. The manifest id tells you which state you are
diff --git a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
index f244ae32..3bef7fb1 100644
--- a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
+++ b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
@@ -86,7 +86,7 @@ and enforced in review, not repeated as the model's main example.
## 4. Draft The Nodes
Write the smallest useful set of nodes, each a purpose-coherent prose truth with
-a one-line `description`, named `..md` (or a bare slug when no kind is present). Ask three questions of each body: why (the stance), with what
+a one-line `description`, named `..md` (or a bare slug when no kind is present). Ask three questions of each node body: why (the stance), with what
(the materials), and how it is assembled (the patterns). These are drafting
prompts, not fields.
diff --git a/packages/ghost/src/skill-bundle/references/blocks.md b/packages/ghost/src/skill-bundle/references/blocks.md
index b6a2df40..6d6f31e0 100644
--- a/packages/ghost/src/skill-bundle/references/blocks.md
+++ b/packages/ghost/src/skill-bundle/references/blocks.md
@@ -57,7 +57,7 @@ Neither is correct. A concrete block node is a deliberate trade, not a leak.
reasoning, sources…) earns **one short prose body**. This is what the method is
for.
- The **composer middle** (card, table, form, sidebar…) is a call to weigh. Give
- it a body when its arrangement carries a stance worth matching.
+ it a prose body when its arrangement carries a stance worth matching.
If a primitive ever seems to need stance guidance, that is a signal it is doing
a composer's job. Promote the pattern into a node; do not write a body on the
diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md
index 2e4324fd..76655ebb 100644
--- a/packages/ghost/src/skill-bundle/references/capture.md
+++ b/packages/ghost/src/skill-bundle/references/capture.md
@@ -181,7 +181,7 @@ action beats completeness...
`materials`, a substantial fenced example, or a `## Skeleton`. You do not
declare a separate type.
-## What a body answers
+## What a node body answers
While drafting, ask three questions of every truth — *why* (the stance), *with
what* (the materials), and *how it is assembled* (the patterns). These are
@@ -204,7 +204,7 @@ genuinely a different truth.
## Node prose stances
-Node prose is steering payload. A generic sentence in a body averages every
+Node prose is steering payload. A generic sentence in a node body averages every
future generation toward the median, so hold drafts to these stances before the
human sees them.
@@ -226,13 +226,13 @@ Everywhere else:
fits a competitor's brand, it is retrieval-dead; rewrite it until it could
belong to no one else.
- **Cut unratified hedges.** "Generally," "where possible," and "consider" in a
- body mean the human never picked a side. Get the ratification or cut the
+ node body mean the human never picked a side. Get the ratification or cut the
sentence.
- **Ban brand-deck filler.** "Elevate," "delight," "seamless," "best-in-class,"
"empower." When a brand doc supplies these words, they are testimony to
distill, never prose to keep.
- **Settle the altitude on purpose.** Every truth is either claimed universal
- or given its condition in the prose. A body that does neither was never
+ or given its condition in the prose. A node body that does neither was never
curated for altitude; ask the human which it is.
## Score drafts before curation
@@ -244,7 +244,7 @@ dimension:
| --- | --- |
| Testimony | Can you quote the human words or artifact this node came from? |
| Discrimination | Does the description fit only this brand? |
-| Force | Does the body decide something, or merely describe something? |
+| Force | Does the node body decide something, or merely describe something? |
| Altitude | Is it universal on purpose, or given its condition? |
| Residue | Is it free of starter-demo prose and brand-deck filler? |
diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts
index 3bad77d9..ea313adb 100644
--- a/packages/ghost/test/cli.test.ts
+++ b/packages/ghost/test/cli.test.ts
@@ -281,7 +281,7 @@ describe("ghost CLI", () => {
expect(median.stdout).toContain(
"This is the model's median, not your brand.",
);
- expect(median.stdout).toContain("");
+ expect(median.stdout).toContain("### Side-stripe");
// The dials ship unanswered and forbid freehanding.
const shape = await runCli(["pull", "signature.shape"], dir);
@@ -331,11 +331,15 @@ describe("ghost CLI", () => {
expect(initOutput.written).toContain("manifest.yml");
expect(initOutput.written).toContain("glossary.md");
expect(initOutput.written).toContain("index.md");
+ expect(initOutput.written).toContain("anti-goal.median.md");
expect(initOutput.written).not.toContain("principle.stance.md");
expect(initOutput.written).not.toContain("decision.tradeoff.md");
- const validate = await runCli(["validate"], dir);
+ const validate = await runCli(["validate", "--format", "json"], dir);
expect(validate.code).toBe(0);
+ const report = JSON.parse(validate.stdout);
+ expect(report.errors).toBe(0);
+ expect(report.warnings).toBe(0);
});
it("keeps default and steering as aliases for the skeleton template", async () => {
@@ -370,10 +374,14 @@ describe("ghost CLI", () => {
expect(initOutput.written).toContain("index.md");
expect(initOutput.written).toContain("principle.composition.md");
expect(initOutput.written).toContain("pattern.status-with-next-step.md");
+ expect(initOutput.written).toContain("anti-goal.median.md");
// The scaffolded package is valid as written.
- const validate = await runCli(["validate"], dir);
+ const validate = await runCli(["validate", "--format", "json"], dir);
expect(validate.code).toBe(0);
+ const report = JSON.parse(validate.stdout);
+ expect(report.errors).toBe(0);
+ expect(report.warnings).toBe(0);
// The ladder nodes surface in the gather menu with their kinds.
const gather = await runCli(["gather", "--format", "json"], dir);
@@ -908,13 +916,13 @@ a deliberate provocation past the fingerprint — surfaced only on request
const gather = await runCli(["gather", "--format", "json"], dir);
expect(gather.code).toBe(0);
expect(JSON.parse(gather.stdout).coverage).toEqual({
- nodes: 4,
+ nodes: 5,
concrete: 1,
- guards: 1,
+ guards: 2,
});
const markdown = await runCli(["gather"], dir);
expect(markdown.stdout).toContain(
- "4 nodes · 1 carry concrete material · 1 guards",
+ "5 nodes · 1 carry concrete material · 2 guards",
);
const steering = await runCli(
@@ -1876,6 +1884,7 @@ a deliberate provocation past the fingerprint — surfaced only on request
expect(add.code).toBe(0);
const added = JSON.parse(add.stdout);
expect(added.written).toEqual(["median-tells.md", "example.md.example"]);
+ expect(added.skipped).toEqual([]);
await expect(
readFile(join(dir, ".ghost", "checks", "example.md.example"), "utf-8"),
).resolves.toContain("references:");
@@ -1886,9 +1895,11 @@ a deliberate provocation past the fingerprint — surfaced only on request
"utf-8",
);
expect(median).toContain("anti-goal.median");
- expect(median).toContain("rule:median-hover-lift");
+ expect(median).toContain("anti-goal.median > Hover-lift");
expect(median).toContain("prefers-reduced-motion");
- expect(median).toContain("delete both together");
+ expect(median).toContain(
+ "`ghost validate` warns; delete the flag and its reference together.",
+ );
expect(median).not.toContain("Vessel");
// Running init twice is a usage error.
@@ -1907,6 +1918,53 @@ a deliberate provocation past the fingerprint — surfaced only on request
expect(unresolved).toEqual([]);
});
+ it("checks init skips median tells when the median node is absent", async () => {
+ await runCli(["init", "--template", "minimal"], dir);
+ await rm(join(dir, ".ghost", "anti-goal.median.md"));
+
+ const add = await runCli(["checks", "init"], dir);
+ expect(add.code).toBe(0);
+ expect(add.stdout).toContain(
+ "skipped median-tells.md (no anti-goal.median node)",
+ );
+
+ await expect(
+ readFile(join(dir, ".ghost", "checks", "median-tells.md"), "utf-8"),
+ ).rejects.toThrow();
+
+ const validate = await runCli(["validate", "--format", "json"], dir);
+ expect(validate.code).toBe(0);
+ const report = JSON.parse(validate.stdout);
+ expect(report.errors).toBe(0);
+ expect(report.warnings).toBe(0);
+ });
+
+ it("validate warns when a pruned median heading orphans its paired check", async () => {
+ await runCli(["init"], dir);
+ await runCli(["checks", "init"], dir);
+ const path = join(dir, ".ghost", "anti-goal.median.md");
+ const median = await readFile(path, "utf-8");
+ await writeFile(
+ path,
+ median.replace(/### Side-stripe\n[\s\S]*?(?=\n### Cream surface)/, ""),
+ );
+
+ const validate = await runCli(["validate", "--format", "json"], dir);
+ expect(validate.code).toBe(0);
+ const report = JSON.parse(validate.stdout);
+ expect(report.warnings).toBe(1);
+ expect(report.issues).toEqual([
+ expect.objectContaining({
+ severity: "warning",
+ rule: "check-reference-heading-missing",
+ message: expect.stringContaining("anti-goal.median > Side-stripe"),
+ }),
+ ]);
+ expect(report.issues[0].message).toContain(
+ "if you pruned this rule from the node, delete its paired flag in the check too",
+ );
+ });
+
it("checks rejects unknown actions", async () => {
await runCli(["init"], dir);
const result = await runCli(["checks", "remove"], dir);
diff --git a/packages/vessel-light/.ghost/anti-goal.median.md b/packages/vessel-light/.ghost/anti-goal.median.md
index 68e2bfd0..a0a5cefb 100644
--- a/packages/vessel-light/.ghost/anti-goal.median.md
+++ b/packages/vessel-light/.ghost/anti-goal.median.md
@@ -4,79 +4,82 @@ description: "The model's median defaults this fingerprint refuses — gather fo
This is the model's median, not your brand. Each rule is reject→replace.
Delete every line your brand legitimately violates — `ghost validate` will
-surface any check the deletion orphans.
+warn on any check reference the deletion orphans — delete the paired flag too.
These are not aesthetic opinions. Where a count is given, it is the measured
convergence of 300 unsteered generations across three frontier models (the
antimedian experiment): the defaults a model reaches for when nobody hands it
a brand. An output showing several of these tells reads as generated,
-whatever else it does right.
+whatever else it does right. A deterministic floor rides alongside these
+rules in the paired check — mechanical tells like gradient-filled text,
+emoji as interface icons, em-dash pileups, and aphoristic cadence —
+verified at review, never steered in prose.
-
+### Hover-lift
Reject hover-lift (`translateY` + growing shadow) as the default interaction
→ confirm with color and background change at the fingerprint's fast
duration. (measured: 341)
-
+### Indigo accent
Reject the indigo/blue/purple default accent (`#4f46e5`, `#2563eb`,
`#8b5cf6` family; purple/violet hue 260–310) → the fingerprint's declared
palette; absent one, monochrome plus a single functional accent.
-
+### Dark theme
Reject unprompted dark theme → the fingerprint's declared surface.
Dark-to-look-cool and light-to-be-safe are the same retreat from a decision.
(measured: 271)
-
+### Gradients
Reject gradient page and section backgrounds and gradient-filled CTAs → flat
surfaces in semantic roles.
-
+### Side-stripe
Reject side-stripe borders (a thick colored border on one side of a rounded
card) — the single most recognizable tell of AI-generated UI → a full
hairline border, a 4–8% surface tint, or a leading glyph.
-
+### Cream surface
Reject the cream/sand/beige default surface (warm off-white; token names
like `--cream`, `--sand`, `--parchment` are the tell) → a true off-white,
the brand's own hue, or a committed color; warmth via accent and type, not
the ground.
-
+### Glassmorphism
Reject glassmorphism (decorative backdrop blur) → flat surfaces with real
borders and the fingerprint's elevation tiers.
-
+### Chat bubbles
Reject chat bubbles with initials-circle avatars for assistant turns →
assistant text plain on the page surface; user turns marked compactly.
-
+### Stock copy
Reject stock template copy ("Simple, transparent pricing") → headings that
state what this product specifically does.
-
+### Celebration
Reject celebration copy and UI (exclamation success, confetti language) →
quiet factual confirmation.
-
+### Nested cards
Reject nested cards and everything-in-cards → one surface level per region;
interior hierarchy from spacing and type; spacing and alignment group
without card overhead.
-
+### Hero metric
Reject the hero-metric template (big number + small label + supporting stats
as default proof) → evidence specific to the product, or nothing. A
prominent metric showing real user data is fine.
-
+### Every button primary
Reject every button a primary button → one primary per view; the rest step
down the fingerprint's emphasis ladder.
-
+### Eyebrow kicker
Reject an eyebrow kicker on every section → at most one, where the register
sanctions it; one named kicker is voice, every-section is AI grammar.
-
+### Decorative motion
Reject decorative looping animation (pulse/float/shimmer) and uniform
fade-and-rise on every scrolled section → motion as evidence of state
change; loops only for genuine loading; decoration never compensates for
diff --git a/packages/vessel-light/.ghost/checks/median-tells.md b/packages/vessel-light/.ghost/checks/median-tells.md
index 1570e0ed..c15e44e5 100644
--- a/packages/vessel-light/.ghost/checks/median-tells.md
+++ b/packages/vessel-light/.ghost/checks/median-tells.md
@@ -3,49 +3,60 @@ name: Median tells
description: Flags the measured defaults of unsteered generation and current model-signature tells — hover-lift, default accents, unprompted dark theme, gradient text, frequency tells, and per-model signatures.
severity: high
references:
- - anti-goal.median
- - grammar.motion
- - grammar.conversation
+ - anti-goal.median > Hover-lift
+ - anti-goal.median > Indigo accent
+ - anti-goal.median > Dark theme
+ - anti-goal.median > Gradients
+ - anti-goal.median > Glassmorphism
+ - anti-goal.median > Side-stripe
+ - anti-goal.median > Cream surface
+ - anti-goal.median > Chat bubbles
+ - anti-goal.median > Stock copy
+ - anti-goal.median > Celebration
+ - anti-goal.median > Hero metric
+ - anti-goal.median > Eyebrow kicker
---
-These flags target the measured convergence patterns of unsteered model
-generation, plus tells specific to individual models. Each is mechanically
-detectable in a diff. Pruning a rule from `anti-goal.median` orphans its
-paired flag here — delete both together.
+This is Vessel's adaptation of the shared median check; the fidelity carve-outs
+below are the body's own. These flags target the measured convergence patterns
+of unsteered model generation, plus tells specific to individual models. Each
+is mechanically detectable in a diff. Pruning a rule from `anti-goal.median`
+orphans its paired reference here — `ghost validate` warns; delete the flag and
+its reference together.
Flag `transform` with `translateY` inside a `:hover` rule on cards, buttons,
or list items, especially paired with a shadow increase. Hover confirmation
in Vessel is color and background change, not lift.
-(`anti-goal.median > rule:median-hover-lift`)
+(`anti-goal.median > Hover-lift`)
Flag accent values in the indigo/blue/purple default family (`#4f46e5`,
`#6366f1`, `#2563eb`, `#3b82f6`, `#8b5cf6`, and close neighbors) unless the
diff shows the user asked for them. They are model defaults, not palette
-members. (`rule:median-indigo-accent`)
+members. (`anti-goal.median > Indigo accent`)
Flag whole-page dark backgrounds when the ask did not request dark mode.
Dark surfaces in Vessel are the editorial dark band or an explicit `.dark`
-theme, never an unprompted default. (`rule:median-dark-theme`)
+theme, never an unprompted default. (`anti-goal.median > Dark theme`)
Flag `linear-gradient` or `radial-gradient` as page or section backgrounds,
-and gradient-filled buttons. (`rule:median-gradients`)
+and gradient-filled buttons. (`anti-goal.median > Gradients`)
Flag `backdrop-filter: blur` used for glassmorphism cards.
-(`rule:median-glassmorphism`)
+(`anti-goal.median > Glassmorphism`)
Flag `background-clip: text` (with or without the `-webkit-` prefix) paired
with a gradient. Emphasis comes from weight or size in a single solid color.
Flag a thick colored border on one side of an element (`border-left` or a
`border-l-*` utility at 2px or more in a non-neutral color) while the other
-sides stay thin. (`rule:median-side-stripe`)
+sides stay thin. (`anti-goal.median > Side-stripe`)
Flag warm off-white page backgrounds in the cream/sand/beige band, and token
names like `--cream`, `--sand`, `--parchment`, `--linen` introduced by the
-diff. (`rule:median-cream-surface`)
+diff. (`anti-goal.median > Cream surface`)
Flag assistant messages rendered as bubbles with initials-circle avatars.
-(`rule:median-chat-bubbles`)
+(`anti-goal.median > Chat bubbles`)
Flag emoji used as icons or imagery in interface chrome. Text labels carry
meaning.
@@ -53,13 +64,22 @@ meaning.
Flag stock template copy in headings: "Simple, transparent pricing",
"Welcome back", and interchangeable-with-a-competitor phrasing. Recommend
copy that states what this product specifically does.
-(`rule:median-stock-copy`)
+(`anti-goal.median > Stock copy`)
+
+Flag exclamation-marked success copy, confetti language, and celebratory UI
+("You did it!", "Awesome!"). Confirmation is quiet and factual.
+(`anti-goal.median > Celebration`)
+
+Flag the hero-metric template — a big number, small label, and supporting
+stats as default proof — unless the metric shows real user data. Recommend
+evidence specific to the product, or nothing.
+(`anti-goal.median > Hero metric`)
Frequency tells — the crime is repetition, not the move (advisory):
Flag three or more uppercase, tracked eyebrow kickers above section headings
in one page. One named kicker is voice; a kicker on every section is model
-grammar. (`rule:median-eyebrow-kicker`)
+grammar. (`anti-goal.median > Eyebrow kicker`)
Flag five or more em-dashes in body copy in one view.
diff --git a/packages/vessel-light/.ghost/checks/motion-restraint.md b/packages/vessel-light/.ghost/checks/motion-restraint.md
index 0de32a48..52040bfb 100644
--- a/packages/vessel-light/.ghost/checks/motion-restraint.md
+++ b/packages/vessel-light/.ghost/checks/motion-restraint.md
@@ -4,13 +4,14 @@ description: Flags non-token motion, looping decoration, and keyframes that do n
severity: medium
references:
- grammar.motion
+ - signature.temperature
---
Review changed transitions and animations for vocabulary first.
Flag durations that do not use `--duration-fast`, `--duration-normal`, or `--duration-slow`. A hard-coded millisecond value is a drift even when it matches the token today.
-Flag custom easing unless it uses the approved spring ease or a browser default already required by the primitive.
+Flag custom easing unless it uses the fingerprint's one ease or a browser default already required by the primitive.
Flag looping animations outside explicit loading states. Loading must be tied to ongoing work, not ambient motion.
diff --git a/packages/vessel-light/.ghost/checks/relationships.md b/packages/vessel-light/.ghost/checks/relationships.md
index 29ecb88b..1dfe6391 100644
--- a/packages/vessel-light/.ghost/checks/relationships.md
+++ b/packages/vessel-light/.ghost/checks/relationships.md
@@ -1,15 +1,16 @@
---
name: Relationship discipline
-description: Flags structural violations of the grammar — emphasis-ladder breaks, sibling margins, decorative borders, nested cards. These rules survive any rebrand.
+description: Flags structural violations of the grammar — emphasis-ladder breaks, sibling margins, decorative borders, nested cards. These rules survive any adaptation.
severity: high
references:
- grammar.hierarchy
- grammar.rhythm
- grammar.surfaces
+ - anti-goal.median > Nested cards
---
These assertions test relationships between token roles, not the values behind
-them. They hold for any brand built on this grammar; a fork keeps this check
+them. They hold for any brand built on this grammar; an adapted package keeps this check
unchanged.
Flag more than one primary-variant button per view. Secondary actions step
diff --git a/packages/vessel-light/.ghost/checks/values.md b/packages/vessel-light/.ghost/checks/values.md
index 031e2d40..8587251c 100644
--- a/packages/vessel-light/.ghost/checks/values.md
+++ b/packages/vessel-light/.ghost/checks/values.md
@@ -1,17 +1,18 @@
---
name: Value discipline
-description: Flags off-signature values — non-pill controls, off-palette hues, raw color literals, expression over budget. A fork rewrites this check alongside the signature nodes.
+description: Flags off-signature values — non-pill controls, off-palette hues, raw color literals, expression over budget. Adapting the dials rewrites this check alongside the signature nodes.
severity: high
references:
- signature.shape
- signature.palette
+ - grammar.color-roles
- register.data-density
- register.editorial
- register.email
---
These assertions test Vessel's current answers to the signature dials. A
-rebrand that changes the dials rewrites this check with them; the paired
+adaptation that changes the dials rewrites this check with them; the paired
relationship check stays.
Review changed HTML and CSS by view, not just by file. Classify the register
diff --git a/packages/vessel-light/.ghost/glossary.md b/packages/vessel-light/.ghost/glossary.md
index b97c1c38..8017688a 100644
--- a/packages/vessel-light/.ghost/glossary.md
+++ b/packages/vessel-light/.ghost/glossary.md
@@ -10,15 +10,15 @@ kinds:
# grammar
The brand's decision logic: closed sets, role vocabularies, and assembly
-rules, stated in token roles and never in literal values. Grammar survives a
-rebrand unchanged — fork the package, swap every value, and these nodes still
+rules, stated in token roles and never in literal values. Grammar survives an
+adaptation unchanged — adapt the package, swap every value, and these nodes still
hold. Gather grammar before inventing structure.
# signature
The dials: the choices that make this brand this brand, each stated as a
current answer that stands until you replace it. Signature nodes name real
-values because they are the values — on fork, edit the token roles in
+values because they are the values — on adaptation, edit the token roles in
`materials/tokens.css` and restate the node. Gather signature before setting
any value a dial governs.
diff --git a/packages/vessel-light/.ghost/grammar.color-roles.md b/packages/vessel-light/.ghost/grammar.color-roles.md
index 99e28261..01f24754 100644
--- a/packages/vessel-light/.ghost/grammar.color-roles.md
+++ b/packages/vessel-light/.ghost/grammar.color-roles.md
@@ -21,9 +21,9 @@ brand accents, and they never moonlight as atmosphere, in any register.
One view should not perform a color palette. If a status color is present,
let the rest of the view stay on the base roles. Richness beyond this is
-register-gated: the expression roles (`--expression-1` through
-`--expression-5`) exist, but their volume ladder is a brand answer — see the
-palette signature — and each register caps how loud they may be.
+register-gated: a closed expression set (`--expression-*`) exists, but its
+size, members, and volume ladder are a brand answer — see the palette
+signature — and each register caps how loud they may be.
The constant that holds across every register: expression never touches what
you click. Buttons, inputs, and links stay on the base roles everywhere. A
diff --git a/packages/vessel-light/.ghost/grammar.motion.md b/packages/vessel-light/.ghost/grammar.motion.md
index 406ea1c4..d7cbfbfd 100644
--- a/packages/vessel-light/.ghost/grammar.motion.md
+++ b/packages/vessel-light/.ghost/grammar.motion.md
@@ -22,6 +22,6 @@ keyframes are off-language.
Prefer opacity and small transform changes. If removing an animation does not
reduce comprehension, the animation was decoration.
-Condition: editorial surfaces may stage entrances — scroll reveals and
+Condition: marketing and editorial surfaces may stage entrances — scroll reveals and
section transitions are part of editorial rhythm, still built from the three
durations and the one ease. In product UI the same staging is decoration.
diff --git a/packages/vessel-light/.ghost/index.md b/packages/vessel-light/.ghost/index.md
index 37753ab5..7b01cbc2 100644
--- a/packages/vessel-light/.ghost/index.md
+++ b/packages/vessel-light/.ghost/index.md
@@ -12,7 +12,7 @@ closed sets the grammar enumerates. Imitate the refs when the task matches
them — they are worked examples to copy from, not a framework to import.
The corpus carries three trust tiers. Grammar and the median floor are law:
-safe to consume verbatim, unchanged by any rebrand. Signature nodes are
+safe to consume verbatim, unchanged by any adaptation. Signature nodes are
dials awaiting your values: each states Vessel's current answer and stands
until you replace it. Registers are conditions: editorial, email, and
data-density each name the situation where parts of the default contract
diff --git a/packages/vessel-light/.ghost/signature.type.md b/packages/vessel-light/.ghost/signature.type.md
index c0c7b5b2..f5d59003 100644
--- a/packages/vessel-light/.ghost/signature.type.md
+++ b/packages/vessel-light/.ghost/signature.type.md
@@ -14,8 +14,7 @@ outside the text variants — and product UI never mixes the two vocabularies
in one view.
Vessel's current answer: HK Grotesk. vessel-light ships it as its embedded
-voice; the React package currently falls back to system-ui. This fingerprint
-is the intended future — here, `tokens.css` is canonical.
+voice — here, `tokens.css` is canonical.
The heading scale is editorial: display, section, sub, and card each carry
their own rhythm (`--heading-display-*`, `--heading-section-*`,
diff --git a/packages/vessel-light/README.md b/packages/vessel-light/README.md
index 8f03e0fd..7df8c5dc 100644
--- a/packages/vessel-light/README.md
+++ b/packages/vessel-light/README.md
@@ -25,7 +25,7 @@ ghost review
## Structure
-The corpus is factored by rate of change under rebrand: `grammar.*` nodes are value-free decision logic that survives any fork; `signature.*` nodes are the identity dials (shape, palette, type, temperature), each stating Vessel's current answer; `register.*` nodes are named conditions that re-tune the contract; `anti-goal.median` is the model's measured defaults (prune lines your brand legitimately violates) and `anti-goal.tells` guards near-misses of Vessel's own signature. Every literal value lives in `materials/tokens.css`.
+The corpus is factored by rate of change under adaptation: `grammar.*` nodes are value-free decision logic that survives any adaptation; `signature.*` nodes are the identity dials (shape, palette, type, temperature), each stating Vessel's current answer; `register.*` nodes are named conditions that re-tune the contract; `anti-goal.median` is the model's measured defaults (prune lines your brand legitimately violates) and `anti-goal.tells` guards near-misses of Vessel's own signature. Every literal value lives in `materials/tokens.css`.
## Curation
diff --git a/scripts/check-release-tarball.mjs b/scripts/check-release-tarball.mjs
index c30b59f9..ed8aa742 100644
--- a/scripts/check-release-tarball.mjs
+++ b/scripts/check-release-tarball.mjs
@@ -66,7 +66,8 @@ try {
"package.json",
"dist/bin.js",
"dist/cli.js",
- "dist/init-payloads/skeleton/anti-goal.median.md",
+ "dist/init-payloads/median/anti-goal.median.md",
+ "dist/init-payloads/median/median-tells.md",
"dist/init-payloads/vessel-light/manifest.yml",
"dist/init-payloads/vessel-light/materials/fonts/HKGrotesk-Regular.woff2",
"node_modules/cac",