diff --git a/.changeset/adapting-a-starter.md b/.changeset/adapting-a-starter.md
new file mode 100644
index 00000000..362e661a
--- /dev/null
+++ b/.changeset/adapting-a-starter.md
@@ -0,0 +1,5 @@
+---
+"@design-intelligence/ghost": minor
+---
+
+The skill bundle gains the `adapting-a-starter` recipe: the seven-step transplant procedure for turning an installed starter — a body like vessel-light or the naked skeleton — into your own brand's fingerprint, plus guidance for consuming an unadapted starter honestly.
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/.changeset/review-locator-matching.md b/.changeset/review-locator-matching.md
new file mode 100644
index 00000000..c966d0fe
--- /dev/null
+++ b/.changeset/review-locator-matching.md
@@ -0,0 +1,5 @@
+---
+"@design-intelligence/ghost": patch
+---
+
+`ghost review` now resolves package-relative `materials/…` locators to their repo-relative form before matching diff paths, so packages living below the repo root offer their value checks correctly.
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/install/manifest.json b/install/manifest.json
index 2595f730..951098a7 100644
--- a/install/manifest.json
+++ b/install/manifest.json
@@ -8,6 +8,7 @@
},
"files": [
"SKILL.md",
+ "references/adapting-a-starter.md",
"references/authoring-scenarios.md",
"references/blocks.md",
"references/brief.md",
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/commands/review-command.ts b/packages/ghost/src/commands/review-command.ts
index eb314d3b..730e027c 100644
--- a/packages/ghost/src/commands/review-command.ts
+++ b/packages/ghost/src/commands/review-command.ts
@@ -53,6 +53,7 @@ export function registerReviewCommand(cli: CAC): void {
diff: opts.diff,
});
const packet = await buildReviewPacket(fingerprint, diffText, {
+ packageDir: paths.packageDir,
runProbes: opts.probes !== false,
cwd: process.cwd(),
});
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 8cb384e1..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
@@ -39,5 +39,6 @@ anti-goals before styling anything greenfield.
---
-The ghost skill bundle teaches authoring: how to answer the dials, add
-registers, and grow this skeleton into a real fingerprint.
+The ghost skill bundle teaches authoring — its `adapting-a-starter` recipe is
+the procedure for growing this skeleton into a real fingerprint: answer the
+dials, prune the median floor, edit your values, then generate refs and tells.
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/review/resolve.ts b/packages/ghost/src/review/resolve.ts
index d7e79043..10c6c909 100644
--- a/packages/ghost/src/review/resolve.ts
+++ b/packages/ghost/src/review/resolve.ts
@@ -1,11 +1,12 @@
import {
classifyMaterialLocator,
type GhostCatalog,
+ type MaterialTransportOptions,
+ materialLocatorClaimsPath,
parseSourceRef,
} from "#ghost-core";
import type { LoadedCheck } from "../scan/check-files.js";
import { parseTouchedFiles, type TouchedFile } from "./diff.js";
-import { matchesGlob } from "./glob.js";
export interface MatchedMaterialNode {
id: string;
@@ -39,6 +40,7 @@ export function resolveReview(
catalog: GhostCatalog,
checks: Map,
diffText: string,
+ transport: MaterialTransportOptions,
): ReviewResolution {
const touchedFiles = parseTouchedFiles(diffText);
const materialNodeIds = new Set();
@@ -55,8 +57,13 @@ export function resolveReview(
if (localLocators.length === 0) continue;
materialNodeIds.add(node.id);
for (const file of touchedFiles) {
+ // Resolve each locator the same way validate does: package-relative
+ // `materials/…` locators expand to their repo-relative form before
+ // matching diff paths (which git emits repo-relative). Matching the
+ // raw locator text silently missed every exact-path locator whenever
+ // the package lives below the repo root.
const locators = localLocators.filter((locator) =>
- matchesGlob(locator, file.path),
+ materialLocatorClaimsPath(locator, file.path, transport),
);
if (locators.length === 0) continue;
claimedFiles.add(file.path);
diff --git a/packages/ghost/src/review/review-packet.ts b/packages/ghost/src/review/review-packet.ts
index 9ed87017..34f72895 100644
--- a/packages/ghost/src/review/review-packet.ts
+++ b/packages/ghost/src/review/review-packet.ts
@@ -1,5 +1,8 @@
+import { join } from "node:path";
import type { GhostCatalogNode } from "#ghost-core";
+import { GHOST_MATERIALS_DIR } from "../scan/constants.js";
import type { LoadedFingerprintPackage } from "../scan/fingerprint-package.js";
+import { resolveGitRoot } from "../scan/package-paths.js";
import { type BaselineProse, resolveBaseline } from "./baseline.js";
import { type ProbeEvidence, runProbe } from "./probes.js";
import type { CoverageGap } from "./resolve.js";
@@ -38,6 +41,8 @@ export interface ReviewPacket {
}
export interface BuildReviewPacketOptions {
+ /** Absolute path of the fingerprint package directory (default: cwd/.ghost). */
+ packageDir?: string;
runProbes?: boolean;
cwd?: string;
probeTimeoutMs?: number;
@@ -48,10 +53,16 @@ export async function buildReviewPacket(
diffText: string,
options: BuildReviewPacketOptions = {},
): Promise {
+ const cwd = options.cwd ?? process.cwd();
const resolution = resolveReview(
fingerprint.catalog,
fingerprint.checks,
diffText,
+ {
+ repoRoot: await resolveGitRoot(cwd),
+ packageDir: options.packageDir ?? join(cwd, ".ghost"),
+ materialsDir: GHOST_MATERIALS_DIR,
+ },
);
const materialNodes: PacketMaterialNode[] = resolution.materialNodes.map(
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/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index 414bfcc0..a54843df 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -110,6 +110,7 @@ against the unpacked package with `--package `.
- Author or update the fingerprint: follow [references/capture.md](references/capture.md).
- Author material-backed nodes: follow [references/blocks.md](references/blocks.md).
- Choose the right human-agent authoring workflow: follow [references/authoring-scenarios.md](references/authoring-scenarios.md).
+- Adapt a starter (a body or the skeleton) to your brand: follow [references/adapting-a-starter.md](references/adapting-a-starter.md).
- Gather applicable truths for a task: follow [references/recall.md](references/recall.md).
- Shape a pre-generation brief: follow [references/brief.md](references/brief.md).
- Explore deliberate provocations with wild nodes: follow [references/wild.md](references/wild.md).
diff --git a/packages/ghost/src/skill-bundle/references/adapting-a-starter.md b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md
new file mode 100644
index 00000000..31792f16
--- /dev/null
+++ b/packages/ghost/src/skill-bundle/references/adapting-a-starter.md
@@ -0,0 +1,102 @@
+---
+name: adapting-a-starter
+description: Transplant a starter fingerprint — a body like vessel-light or the naked skeleton — into your own brand, one procedure from manifest id to regenerated refs.
+handoffs:
+ - label: Validate the adapted package
+ command: ghost validate --format json
+ prompt: Does the adapted fingerprint validate, and did any pruned rule orphan a check reference?
+---
+
+# Recipe: Adapt A Starter Fingerprint
+
+**Goal:** turn an installed starter — a full body (`ghost init --body
+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 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 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. |
+| Derived artifacts | `materials/ref/*.html`, `anti-goal.tells` | Regenerate — they demonstrate the values and near-misses of a *specific* signature. |
+
+Do the steps **in order and in one sitting** where possible. A half-adapted
+package is worse than an unadapted one: stale refs steer harder than any prose
+you rewrote, so stopping after step 4 ships a fingerprint that contradicts
+itself.
+
+## The procedure
+
+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
+ 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`.** 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 —
+ with your brand's: restate the node as "this brand's current answer is …".
+ Ask the human for any dial they have not decided; never freehand a value
+ and present it as brand-backed.
+4. **Edit `materials/tokens.css`.** Every literal value lives here — radii,
+ 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 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` /
+ `incidental`) current, and make each ref demonstrate its closed sets
+ completely. If the starter shipped no refs (the skeleton), generate them
+ now — a fingerprint with no exemplars steers at half strength.
+6. **Rewrite `anti-goal.tells`.** The tells are near-misses of the *starter's*
+ signature; yours are different. For each answered dial, name the failure
+ mode one step away from your answer and its replacement. If the starter
+ shipped no tells node (the skeleton), author one.
+7. **Re-run the checks — including against the refs.** `ghost validate` for
+ package shape, then review the regenerated refs against the median and
+ value checks (stage the ref changes and run `ghost review`). The floor
+ only holds if the refs hold it too: a ref that trips a median flag will
+ teach every future generation the violation. Rewrite the body's
+ `checks/values.md` alongside the dials it references.
+
+## Consuming an unadapted starter
+
+Work does not block on adaptation. Before the procedure runs (or midway
+through it), cite starter content honestly:
+
+- 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
+ in: a starter id means unadapted.
+- In a brief, `anti-goal.median` sits in the anti-goals slot — after intent,
+ inventory, and composition, never before the brand truths. If a median rule
+ conflicts with an answered signature node, the signature wins and the
+ median line is a prune candidate to report.
+
+## Never
+
+- Never rewrite grammar nodes to taste — if a grammar rule is wrong for your
+ brand, it was never grammar; move it to a signature node and answer it.
+- Never leave the starter's refs alongside your new tokens — regenerate or
+ delete; a stale exemplar outweighs your rewritten prose.
+- Never prune a median rule without deleting its paired check flag, or keep a
+ check that asserts an obligation no node states.
+- Never pre-write the manifest id change into automation — it is the human's
+ act of ownership, the one step an agent should not take alone.
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 b9913e9e..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(
@@ -1603,6 +1611,54 @@ a deliberate provocation past the fingerprint — surfaced only on request
});
});
+ it("review resolves package-relative locators when the package sits below the repo root", async () => {
+ // Regression: exact-path `materials/…` locators were matched as raw text
+ // against repo-relative diff paths, so a package below the repo root
+ // (e.g. packages/vessel-light/.ghost) never matched them — its value
+ // checks were silently dropped from the packet.
+ const packageDir = join("nested", "app", ".ghost");
+ await runCli(["init", "--package", packageDir], dir);
+ await mkdir(join(dir, packageDir, "materials"), { recursive: true });
+ await writeFile(
+ join(dir, packageDir, "materials", "tokens.css"),
+ ":root{}\n",
+ );
+ await writeFile(
+ join(dir, packageDir, "asset.tokens.md"),
+ "---\ndescription: Tokens.\nmaterials:\n - materials/tokens.css\n---\n\nTokens prose.\n",
+ );
+ await mkdir(join(dir, packageDir, "checks"), { recursive: true });
+ await writeFile(
+ join(dir, packageDir, "checks", "token-discipline.md"),
+ "---\nname: token-discipline\ndescription: Tokens hold.\nseverity: high\nreferences:\n - asset.tokens\n---\n\nGrade token discipline.\n",
+ );
+ const touched = `${packageDir.replaceAll("\\", "/")}/materials/tokens.css`;
+ const diff = [
+ `diff --git a/${touched} b/${touched}`,
+ `--- a/${touched}`,
+ `+++ b/${touched}`,
+ "@@ -1 +1 @@",
+ "-old",
+ "+new",
+ ].join("\n");
+
+ const result = await runCli(
+ ["review", "--package", packageDir, "--diff=-", "--format", "json"],
+ dir,
+ { stdin: diff },
+ );
+
+ expect(result.code).toBe(0);
+ const packet = JSON.parse(result.stdout);
+ expect(packet.materialNodes.map((n: { id: string }) => n.id)).toContain(
+ "asset.tokens",
+ );
+ const check = packet.checks.find(
+ (c: { id: string }) => c.id === "token-discipline",
+ );
+ expect(check).toMatchObject({ offered: "matched" });
+ });
+
it("review runs check probes as evidence and supports --no-probes", async () => {
await runCli(["init", "--with", "checks"], dir);
await writeFile(
@@ -1828,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:");
@@ -1838,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.
@@ -1859,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 04718897..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.
@@ -39,7 +39,7 @@ anti-goals before styling anything greenfield.
---
-To adapt this package to another brand, follow the transplant procedure —
-it ships with the ghost skill bundle. In short: new manifest id, prune the
+To adapt this package to another brand, follow the `adapting-a-starter`
+recipe in the ghost skill bundle. In short: new manifest id, prune the
median rules your brand legitimately violates, answer each signature node,
edit `materials/tokens.css`, regenerate the refs, rewrite the tells.
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/packages/vessel-light/demo/.gitignore b/packages/vessel-light/demo/.gitignore
index ba1fd25c..e362c4c5 100644
--- a/packages/vessel-light/demo/.gitignore
+++ b/packages/vessel-light/demo/.gitignore
@@ -1,3 +1,3 @@
-# Eval run artifacts are not committed on mainline; the 2026-07-08 matrix run
-# is parked on the `vessel-eval-artifacts` branch. Regenerate via steering-eval.
+# Eval run artifacts are never committed; regenerate via steering-eval
+# (prompt → generate → shoot → score → report) from asks.md + ballast.md.
out/
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",