brand-intent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/brands/krume/krume.brand +135 -0
- package/brands/krume/krume.identity +127 -0
- package/cli/index.mjs +162 -0
- package/compositions/editorial.composition +15 -0
- package/compositions/simple-stack.composition +21 -0
- package/formats/instagram-4-5-feed-portrait.format +21 -0
- package/package.json +31 -0
- package/purposes/daily-bread.purpose +78 -0
- package/skills/brand-intent.md +168 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Brand Intent
|
|
2
|
+
|
|
3
|
+
An open file format specification for making brand intent machine-readable.
|
|
4
|
+
|
|
5
|
+
Five layers. One brand. Any agent.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Identity → Brand → Format → Purpose → Composition
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What this is
|
|
12
|
+
|
|
13
|
+
Design tokens capture decisions. Brand guidelines capture rules. Neither captures *intent*: the reasoning, the derived expression conditions, the signal-level decisions that make a brand specific.
|
|
14
|
+
|
|
15
|
+
Brand Intent is a set of five composable file types that encode brand knowledge from strategy to composition, in a format any AI agent can read and respect.
|
|
16
|
+
|
|
17
|
+
| Layer | File | Question |
|
|
18
|
+
|-------|------|----------|
|
|
19
|
+
| Identity | `.identity` | Who is this brand? |
|
|
20
|
+
| Brand | `.brand` | What expression intent does it derive? |
|
|
21
|
+
| Format | `.format` | Where does this content appear? |
|
|
22
|
+
| Purpose | `.purpose` | What is this content trying to do? |
|
|
23
|
+
| Composition | `.composition` | How are elements composed? |
|
|
24
|
+
|
|
25
|
+
## Get started
|
|
26
|
+
|
|
27
|
+
### 1. Install the VS Code extension
|
|
28
|
+
|
|
29
|
+
Syntax highlighting, autocomplete, and validation for all five file types.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
ext install stoll.brand-intent-lang
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Install skills for your AI agent
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx brand-intent install-skills
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Works with Claude Code, Cursor, Gemini CLI, VS Code Copilot, Codex CLI, and any MCP-compatible agent.
|
|
42
|
+
|
|
43
|
+
### 3. Author your brand
|
|
44
|
+
|
|
45
|
+
Start with `.identity` (strategy, voice, audiences), then derive `.brand` (expression intent). Add formats, purposes, and compositions as needed. The Krume bakery brand in `brands/krume/` is a fully commented reference implementation.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx brand-intent init --from krume
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
- [Specification](SPEC.md): formal grammar for all five file types
|
|
54
|
+
- [Design Decisions](DESIGN.md): reasoning behind the framework
|
|
55
|
+
- [intent.stoll.studio](https://intent.stoll.studio): project landing page
|
|
56
|
+
|
|
57
|
+
## Relationship to Impeccable
|
|
58
|
+
|
|
59
|
+
Brand Intent is complementary to [Impeccable](https://impeccable.style). Where Impeccable teaches agents *how to design well* (general craft), Brand Intent teaches them *what this brand's expression intent is* (specific identity). Together: execution quality and brand specificity.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
By [Christophe Stoll / Studio Stoll](https://stoll.studio)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# krume.brand - expression intent derived from krume.identity
|
|
2
|
+
# Every decision here should be traceable to a statement in the identity file.
|
|
3
|
+
# No strategic reasoning. No audience definitions. No prose.
|
|
4
|
+
|
|
5
|
+
id: krume
|
|
6
|
+
name: Krume
|
|
7
|
+
language: de
|
|
8
|
+
locale: de-DE
|
|
9
|
+
|
|
10
|
+
# ── Color Primitives ──
|
|
11
|
+
# Derived from the bakery's material world: dark roasted crust,
|
|
12
|
+
# warm wood, flour dust, rye. No corporate blue, no lifestyle pastels.
|
|
13
|
+
|
|
14
|
+
brand-colors
|
|
15
|
+
primary: #2C1810 # dark roast - the color of a well-baked crust
|
|
16
|
+
secondary: #5C3A28 # walnut - warm wood of the shop counter
|
|
17
|
+
accent1: #C8A47E # crust - golden, used for dividers and highlights
|
|
18
|
+
accent2: #8B4513 # rye - deep warm brown for CTAs
|
|
19
|
+
white: #FAF6F0 # flour - not pure white, slightly warm
|
|
20
|
+
|
|
21
|
+
# ── Themes ──
|
|
22
|
+
# Laden (shop): light background, dark text. The everyday.
|
|
23
|
+
# Tafel (chalkboard): dark background, light text. Announcements, specials.
|
|
24
|
+
|
|
25
|
+
theme Laden
|
|
26
|
+
background: $white
|
|
27
|
+
text-primary: $primary
|
|
28
|
+
text-secondary: $secondary
|
|
29
|
+
text-tertiary: $accent1
|
|
30
|
+
logo: $primary
|
|
31
|
+
cta: $accent2
|
|
32
|
+
label-bg: $accent1
|
|
33
|
+
label-fg: $primary
|
|
34
|
+
divider: $accent1
|
|
35
|
+
|
|
36
|
+
theme Tafel
|
|
37
|
+
background: $primary
|
|
38
|
+
text-primary: $white
|
|
39
|
+
text-secondary: #D4C4B0
|
|
40
|
+
text-tertiary: #8A7A68
|
|
41
|
+
logo: $accent1
|
|
42
|
+
cta: $accent1
|
|
43
|
+
label-bg: $accent2
|
|
44
|
+
label-fg: $white
|
|
45
|
+
divider: $accent1
|
|
46
|
+
|
|
47
|
+
# ── Typography ──
|
|
48
|
+
# Libre Franklin: utilitarian, no-nonsense, legible.
|
|
49
|
+
# Not a display typeface. Not decorative. Feels like signage.
|
|
50
|
+
|
|
51
|
+
font primary
|
|
52
|
+
name: Libre Franklin
|
|
53
|
+
fallback: sans-serif
|
|
54
|
+
source: google
|
|
55
|
+
|
|
56
|
+
typography headline
|
|
57
|
+
weight: 700
|
|
58
|
+
strong: 800
|
|
59
|
+
size: 6 # cqh - large but not shouting
|
|
60
|
+
lineHeight: 1.15
|
|
61
|
+
autofit-min: 4
|
|
62
|
+
autofit-max: 10
|
|
63
|
+
|
|
64
|
+
typography kicker
|
|
65
|
+
weight: 500
|
|
66
|
+
size: 3.5
|
|
67
|
+
lineHeight: 1.3
|
|
68
|
+
|
|
69
|
+
typography body
|
|
70
|
+
weight: 400
|
|
71
|
+
size: 2.8
|
|
72
|
+
lineHeight: 1.5
|
|
73
|
+
|
|
74
|
+
typography caption
|
|
75
|
+
weight: 400
|
|
76
|
+
size: 2.2
|
|
77
|
+
lineHeight: 1.4
|
|
78
|
+
|
|
79
|
+
typography label
|
|
80
|
+
weight: 600
|
|
81
|
+
size: 2
|
|
82
|
+
lineHeight: 1.2
|
|
83
|
+
uppercase
|
|
84
|
+
|
|
85
|
+
typography number
|
|
86
|
+
weight: 300
|
|
87
|
+
size: 14 # intentionally large - display element
|
|
88
|
+
lineHeight: 0.9
|
|
89
|
+
letterSpacing: -0.03
|
|
90
|
+
|
|
91
|
+
# ── Spacing ──
|
|
92
|
+
|
|
93
|
+
spacing:
|
|
94
|
+
unit: cqmin
|
|
95
|
+
xs: 1.5
|
|
96
|
+
s: 2
|
|
97
|
+
m: 3
|
|
98
|
+
l: 5
|
|
99
|
+
xl: 6.5
|
|
100
|
+
|
|
101
|
+
# ── Dividers ──
|
|
102
|
+
|
|
103
|
+
divider line
|
|
104
|
+
thickness: 0.15
|
|
105
|
+
width: 100%
|
|
106
|
+
color: divider
|
|
107
|
+
align: left
|
|
108
|
+
spacing: xs
|
|
109
|
+
|
|
110
|
+
divider short
|
|
111
|
+
thickness: 0.2
|
|
112
|
+
width: 25%
|
|
113
|
+
color: divider
|
|
114
|
+
align: left
|
|
115
|
+
spacing: s
|
|
116
|
+
|
|
117
|
+
# ── Voice Constraints ──
|
|
118
|
+
# Operationalized from identity voice reasoning.
|
|
119
|
+
# These are enforceable limits, not restated strategy.
|
|
120
|
+
# Derived from: persona is direct, rhythm is short sentences.
|
|
121
|
+
|
|
122
|
+
voice-constraints
|
|
123
|
+
register: informal
|
|
124
|
+
sentence-max: 8 # words - short, direct, like the baker talks
|
|
125
|
+
headline-pattern: noun-first
|
|
126
|
+
number-format: cardinal # "drei", not "3"
|
|
127
|
+
|
|
128
|
+
# ── Content Defaults ──
|
|
129
|
+
# Derived from identity: this is a small bakery, not a media brand.
|
|
130
|
+
# Low density. Natural images. Simple dividers.
|
|
131
|
+
|
|
132
|
+
content-defaults
|
|
133
|
+
density: low
|
|
134
|
+
image-treatment: natural
|
|
135
|
+
divider: line
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# krume.identity
|
|
2
|
+
# Krume - neighborhood sourdough bakery, Berlin-Neukölln.
|
|
3
|
+
# This file is the reference implementation for Brand Intent.
|
|
4
|
+
# Every decision is commented to explain its derivation.
|
|
5
|
+
|
|
6
|
+
# ── Positioning ──
|
|
7
|
+
# Krume exists because its founder believes bread is craft,
|
|
8
|
+
# not lifestyle. The bakery is small, the hours are early,
|
|
9
|
+
# the sourdough is slow. Nothing about this is "curated."
|
|
10
|
+
|
|
11
|
+
essence: Brot ist Handwerk, nicht Lifestyle. Mehl, Wasser, Zeit.
|
|
12
|
+
|
|
13
|
+
promise: Wer reinkommt, riecht sofort, dass hier seit vier Uhr
|
|
14
|
+
morgens jemand steht.
|
|
15
|
+
|
|
16
|
+
tagline: Mehl, Wasser, Zeit. # also known as "Claim" (DACH)
|
|
17
|
+
|
|
18
|
+
positioning: Krume ist die einzige Bäckerei im Kiez, die jedes
|
|
19
|
+
Brot beim Mehl nennt, nicht beim Marketing.
|
|
20
|
+
|
|
21
|
+
mission: Ehrliches Brot für den Kiez - jeden Tag, aus dem
|
|
22
|
+
gleichen Ofen, mit den gleichen Händen.
|
|
23
|
+
|
|
24
|
+
vision: Eine Nachbarschaft, in der niemand mehr erklären muss,
|
|
25
|
+
warum gutes Brot sechs Euro kostet.
|
|
26
|
+
|
|
27
|
+
archetype: craftsman
|
|
28
|
+
|
|
29
|
+
# ── Narrative ──
|
|
30
|
+
# The origin story. Useful for about pages, editorial, press.
|
|
31
|
+
|
|
32
|
+
narrative
|
|
33
|
+
Gegründet 2019, als die Bäckerin ihren Agenturjob
|
|
34
|
+
kündigte, um das zu tun, was sie jeden Morgen seit
|
|
35
|
+
ihrem sechzehnten Lebensjahr getan hat: Brot backen.
|
|
36
|
+
Der Laden in der Weserstraße war vorher ein Copyshop.
|
|
37
|
+
Die erste Woche war der Ofen zu klein. Die zweite
|
|
38
|
+
Woche war das Brot zu schnell ausverkauft.
|
|
39
|
+
Beides hat sich nicht geändert.
|
|
40
|
+
|
|
41
|
+
# ── Voice ──
|
|
42
|
+
# The voice is the baker herself: direct, dry, proud of the work.
|
|
43
|
+
# Not warm-and-fuzzy. Not educational. Not performative.
|
|
44
|
+
# She talks the way she'd talk to someone across the counter.
|
|
45
|
+
|
|
46
|
+
voice
|
|
47
|
+
register: informal, Du
|
|
48
|
+
persona: Bäckerin hinterm Tresen - direkt, trocken, stolz auf das Handwerk
|
|
49
|
+
rhythm: kurze Sätze, konkrete Angaben, kein Erklären
|
|
50
|
+
|
|
51
|
+
# always/never: concrete, testable rules.
|
|
52
|
+
# An AI agent can evaluate any text against these with a yes/no answer.
|
|
53
|
+
|
|
54
|
+
always:
|
|
55
|
+
- Sauerteig, not "artisan bread"
|
|
56
|
+
- konkrete Mehlsorten (Tipo 1, Roggen 1150) over "beste Zutaten"
|
|
57
|
+
- Backzeit in Stunden, nicht "lange Teigführung"
|
|
58
|
+
- Kiez-Bezug (Neukölln, Weserstraße) over "unser Standort"
|
|
59
|
+
- Gewicht in Gramm bei jedem Brot
|
|
60
|
+
|
|
61
|
+
never:
|
|
62
|
+
- Lifestyle-Sprache (kuratiert, handverlesen, Genussmoment)
|
|
63
|
+
- "Artisan" oder "Craft" - wir sagen Handwerk
|
|
64
|
+
- Englische Begriffe wenn ein deutsches Wort existiert
|
|
65
|
+
- Superlative ohne Substanz (bestes Brot, einzigartiger Geschmack)
|
|
66
|
+
- Wellness- oder Gesundheitsversprechen (gut für dich, clean eating)
|
|
67
|
+
|
|
68
|
+
# ── Values ──
|
|
69
|
+
# Not keywords - behavior statements. Each value describes
|
|
70
|
+
# how it manifests in what we say and do.
|
|
71
|
+
|
|
72
|
+
values
|
|
73
|
+
Handwerk
|
|
74
|
+
Wir benennen den Prozess, nicht das Ergebnis.
|
|
75
|
+
Backzeit in Stunden, Mehlsorte beim Namen.
|
|
76
|
+
Kein Brot ohne nachvollziehbaren Weg vom Korn zum Laib.
|
|
77
|
+
|
|
78
|
+
Ehrlichkeit
|
|
79
|
+
Kein Brot ohne Zutatenliste. Keine Versprechen
|
|
80
|
+
die der Teig nicht halten kann. Wenn etwas
|
|
81
|
+
nicht gelungen ist, sagen wir das.
|
|
82
|
+
|
|
83
|
+
Kiez
|
|
84
|
+
Der Laden gehört zur Weserstraße wie der Späti
|
|
85
|
+
nebenan. Wir kennen die Stammkunden beim Namen
|
|
86
|
+
und die Nachbarn beim Brot.
|
|
87
|
+
|
|
88
|
+
# ── Content Pillars ──
|
|
89
|
+
# What Krume talks about, and what it explicitly avoids.
|
|
90
|
+
|
|
91
|
+
pillars
|
|
92
|
+
primary: Sauerteig, Handwerk, Zutaten, Kiez
|
|
93
|
+
secondary: Saisonales, Nachhaltigkeit, Backwissen
|
|
94
|
+
avoid: Lifestyle-Gastronomie, Wellness-Ernährung, Influencer-Kultur
|
|
95
|
+
|
|
96
|
+
# ── Audiences ──
|
|
97
|
+
# Two audiences with genuinely different language needs.
|
|
98
|
+
# The regulars don't need explanations. The newcomers do -
|
|
99
|
+
# but never in a condescending way.
|
|
100
|
+
|
|
101
|
+
audience regulars
|
|
102
|
+
label: Stammkunden aus dem Kiez
|
|
103
|
+
profile: 25-65, täglich oder wöchentlich, kennen die Sorten
|
|
104
|
+
motivation: ihr Brot, ihr Laden, verlässlicher Rhythmus
|
|
105
|
+
language: knapp, direkt, kein Erklären - die wissen Bescheid
|
|
106
|
+
|
|
107
|
+
audience newcomers
|
|
108
|
+
label: Neugierige und Laufkundschaft
|
|
109
|
+
profile: Touristen, neue Nachbarn, Marktbesucher
|
|
110
|
+
motivation: gutes Brot finden, verstehen was anders ist
|
|
111
|
+
language: einladend, konkret, erklärt ohne belehrend zu sein
|
|
112
|
+
|
|
113
|
+
# ── Anti-Audiences ──
|
|
114
|
+
# Explicitly not-our-people. Gives the AI a concrete
|
|
115
|
+
# "never sound like you're talking to this person" signal.
|
|
116
|
+
|
|
117
|
+
anti-audience influencers
|
|
118
|
+
label: Lifestyle-Influencer
|
|
119
|
+
description: Menschen die Brot als Aesthetic posten, nicht
|
|
120
|
+
als Lebensmittel essen. Wir sind nicht ihr Content.
|
|
121
|
+
|
|
122
|
+
anti-audience discount-hunters
|
|
123
|
+
label: Schnäppchenjäger
|
|
124
|
+
description: Wer den Preis eines Sauerteigbrots mit dem
|
|
125
|
+
Supermarkt-Toastbrot vergleicht, ist hier falsch.
|
|
126
|
+
Wir erklären gerne warum es kostet was es kostet -
|
|
127
|
+
aber wir rechtfertigen uns nicht.
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve, basename } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const SKILL_FILE = join(PKG_ROOT, 'skills', 'brand-intent.md');
|
|
12
|
+
|
|
13
|
+
// ── Harness detection and install paths ──
|
|
14
|
+
|
|
15
|
+
const HARNESS_DIRS = {
|
|
16
|
+
claude: '.claude/commands',
|
|
17
|
+
cursor: '.cursor/rules',
|
|
18
|
+
gemini: '.gemini',
|
|
19
|
+
copilot: '.github/copilot-instructions',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function detectHarnesses(cwd) {
|
|
23
|
+
const found = [];
|
|
24
|
+
for (const [name, dir] of Object.entries(HARNESS_DIRS)) {
|
|
25
|
+
// Check if the harness config directory exists (or parent does)
|
|
26
|
+
const parent = dir.split('/')[0];
|
|
27
|
+
if (existsSync(join(cwd, parent))) {
|
|
28
|
+
found.push({ name, dir });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Commands ──
|
|
35
|
+
|
|
36
|
+
function installSkills(cwd) {
|
|
37
|
+
if (!existsSync(SKILL_FILE)) {
|
|
38
|
+
console.error('Error: skill file not found at', SKILL_FILE);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skill = readFileSync(SKILL_FILE, 'utf-8');
|
|
43
|
+
const harnesses = detectHarnesses(cwd);
|
|
44
|
+
|
|
45
|
+
if (harnesses.length === 0) {
|
|
46
|
+
// No harness detected — install to .claude/ by default
|
|
47
|
+
harnesses.push({ name: 'claude', dir: HARNESS_DIRS.claude });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let installed = 0;
|
|
51
|
+
for (const { name, dir } of harnesses) {
|
|
52
|
+
const target = join(cwd, dir);
|
|
53
|
+
mkdirSync(target, { recursive: true });
|
|
54
|
+
const dest = join(target, 'brand-intent.md');
|
|
55
|
+
writeFileSync(dest, skill, 'utf-8');
|
|
56
|
+
console.log(` ✓ ${name} — ${dir}/brand-intent.md`);
|
|
57
|
+
installed++;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\nInstalled Brand Intent skill to ${installed} harness${installed > 1 ? 'es' : ''}.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function copyDirRecursive(src, dest) {
|
|
64
|
+
mkdirSync(dest, { recursive: true });
|
|
65
|
+
for (const entry of readdirSync(src)) {
|
|
66
|
+
const srcPath = join(src, entry);
|
|
67
|
+
const destPath = join(dest, entry);
|
|
68
|
+
if (statSync(srcPath).isDirectory()) {
|
|
69
|
+
copyDirRecursive(srcPath, destPath);
|
|
70
|
+
} else {
|
|
71
|
+
copyFileSync(srcPath, destPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function initFromBrand(brandName, cwd) {
|
|
77
|
+
const brandSrc = join(PKG_ROOT, 'brands', brandName);
|
|
78
|
+
if (!existsSync(brandSrc)) {
|
|
79
|
+
console.error(`Error: brand "${brandName}" not found.`);
|
|
80
|
+
console.error(`Available brands: ${readdirSync(join(PKG_ROOT, 'brands')).join(', ')}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Copy brand files
|
|
85
|
+
const brandDest = join(cwd, `brands/${brandName}`);
|
|
86
|
+
copyDirRecursive(brandSrc, brandDest);
|
|
87
|
+
console.log(` ✓ brand files → brands/${brandName}/`);
|
|
88
|
+
|
|
89
|
+
// Copy shared layers (formats, purposes, compositions)
|
|
90
|
+
for (const layer of ['formats', 'purposes', 'compositions']) {
|
|
91
|
+
const layerSrc = join(PKG_ROOT, layer);
|
|
92
|
+
if (existsSync(layerSrc) && readdirSync(layerSrc).length > 0) {
|
|
93
|
+
const layerDest = join(cwd, layer);
|
|
94
|
+
copyDirRecursive(layerSrc, layerDest);
|
|
95
|
+
console.log(` ✓ ${layer}/ → ${layer}/`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`\nScaffolded from "${brandName}". Edit the .identity and .brand files to make it yours.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addBrand(brandRef, cwd) {
|
|
103
|
+
// brandRef is "brandName/brand" — extract the brand name
|
|
104
|
+
const brandName = brandRef.split('/')[0];
|
|
105
|
+
const brandSrc = join(PKG_ROOT, 'brands', brandName);
|
|
106
|
+
if (!existsSync(brandSrc)) {
|
|
107
|
+
console.error(`Error: brand "${brandName}" not found.`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const brandDest = join(cwd, `brands/${brandName}`);
|
|
112
|
+
copyDirRecursive(brandSrc, brandDest);
|
|
113
|
+
console.log(` ✓ brand files → brands/${brandName}/`);
|
|
114
|
+
console.log(`\nAdded "${brandName}" brand files to your project.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── CLI entry point ──
|
|
118
|
+
|
|
119
|
+
const args = process.argv.slice(2);
|
|
120
|
+
const command = args[0];
|
|
121
|
+
const cwd = process.cwd();
|
|
122
|
+
|
|
123
|
+
console.log('Brand Intent v0.1.0\n');
|
|
124
|
+
|
|
125
|
+
switch (command) {
|
|
126
|
+
case 'install-skills':
|
|
127
|
+
installSkills(cwd);
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'init': {
|
|
131
|
+
const fromIdx = args.indexOf('--from');
|
|
132
|
+
if (fromIdx === -1 || !args[fromIdx + 1]) {
|
|
133
|
+
console.error('Usage: brand-intent init --from <brand-name>');
|
|
134
|
+
console.error('Example: brand-intent init --from krume');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
initFromBrand(args[fromIdx + 1], cwd);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'add': {
|
|
142
|
+
if (!args[1]) {
|
|
143
|
+
console.error('Usage: brand-intent add <brand-name>/brand');
|
|
144
|
+
console.error('Example: brand-intent add krume/brand');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
addBrand(args[1], cwd);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
console.log('Usage:');
|
|
153
|
+
console.log(' brand-intent install-skills Install Brand Intent skill for your AI harness');
|
|
154
|
+
console.log(' brand-intent init --from <brand> Scaffold a project from a reference brand');
|
|
155
|
+
console.log(' brand-intent add <brand>/brand Add a brand to an existing project');
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log('Examples:');
|
|
158
|
+
console.log(' npx brand-intent install-skills');
|
|
159
|
+
console.log(' npx brand-intent init --from krume');
|
|
160
|
+
console.log(' npx brand-intent add krume/brand');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# editorial.composition - grid mode
|
|
2
|
+
# Secondary above primary (newspaper style).
|
|
3
|
+
# Label badge top-right, info bottom-left, CTA bottom-right.
|
|
4
|
+
# This file contains zero brand-specific information.
|
|
5
|
+
|
|
6
|
+
id: editorial
|
|
7
|
+
mode: grid
|
|
8
|
+
image: full
|
|
9
|
+
logo: top left
|
|
10
|
+
|
|
11
|
+
label row 1 cols 3--4 right
|
|
12
|
+
secondary row 2 cols 1--4 left center
|
|
13
|
+
primary row 3 cols 1--4 left
|
|
14
|
+
meta row 5 cols 1--3 left bottom
|
|
15
|
+
cta row 5 cols 4--4 right bottom
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# simple-stack.composition - flow mode
|
|
2
|
+
# Vertical stack in a bottom panel over a full-bleed image.
|
|
3
|
+
# Logo sticky at bottom. Content flows top-to-bottom.
|
|
4
|
+
# This file contains zero brand-specific information.
|
|
5
|
+
|
|
6
|
+
id: simple-stack
|
|
7
|
+
mode: flow
|
|
8
|
+
image: full
|
|
9
|
+
|
|
10
|
+
sticky: bottom s
|
|
11
|
+
| logo right
|
|
12
|
+
|
|
13
|
+
flow: panel bottom-left 2/3 1/1
|
|
14
|
+
| spacer-auto
|
|
15
|
+
| label left
|
|
16
|
+
| primary left
|
|
17
|
+
| divider-line
|
|
18
|
+
| secondary left
|
|
19
|
+
| spacer-s
|
|
20
|
+
| meta left
|
|
21
|
+
| spacer-auto
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# instagram-4-5-feed-portrait.format
|
|
2
|
+
# Instagram feed post, portrait orientation.
|
|
3
|
+
# This format knows nothing about what content means -
|
|
4
|
+
# only where it appears and what fits.
|
|
5
|
+
|
|
6
|
+
id: 4:5
|
|
7
|
+
label: 4:5
|
|
8
|
+
sublabel: Feed Portrait
|
|
9
|
+
category: instagram
|
|
10
|
+
|
|
11
|
+
size: 1080 1350
|
|
12
|
+
grid: 5 4 # 5 rows × 4 columns
|
|
13
|
+
|
|
14
|
+
# Safe zones - platform-specific, not brand decisions.
|
|
15
|
+
danger: 0 0 0 0 # px - nothing always cropped on feed posts
|
|
16
|
+
crop: 0 34 0 34 # px - profile grid crops top/bottom
|
|
17
|
+
comfort: 40 # px - safe reading margin
|
|
18
|
+
|
|
19
|
+
# Which purposes are editorially valid here.
|
|
20
|
+
# This is a brand decision, not a technical constraint.
|
|
21
|
+
purposes: daily-bread, seasonal-special, baking-tip
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brand-intent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An open file format specification for making brand intent machine-readable.",
|
|
5
|
+
"author": "Christophe Stoll <christophe@stoll.studio>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/studiostoll/brand-intent.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://intent.stoll.studio",
|
|
12
|
+
"bin": {
|
|
13
|
+
"brand-intent": "./cli/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"brand",
|
|
17
|
+
"design-tokens",
|
|
18
|
+
"ai",
|
|
19
|
+
"specification",
|
|
20
|
+
"brand-identity",
|
|
21
|
+
"design-system"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"cli/",
|
|
25
|
+
"skills/",
|
|
26
|
+
"brands/",
|
|
27
|
+
"formats/",
|
|
28
|
+
"purposes/",
|
|
29
|
+
"compositions/"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# daily-bread.purpose
|
|
2
|
+
# A single bread announcement - today's bread, name and facts.
|
|
3
|
+
# Used for daily Instagram posts: what's fresh, what's available.
|
|
4
|
+
|
|
5
|
+
id: daily-bread
|
|
6
|
+
name: Tagesbrot
|
|
7
|
+
|
|
8
|
+
# Which compositions can render this purpose.
|
|
9
|
+
compositions: editorial, simple-stack
|
|
10
|
+
|
|
11
|
+
density: light
|
|
12
|
+
palette: dynamic
|
|
13
|
+
|
|
14
|
+
# ── Voice ──
|
|
15
|
+
# Purpose-specific writing rules for AI text generation.
|
|
16
|
+
|
|
17
|
+
voice
|
|
18
|
+
headline-pattern: noun-first
|
|
19
|
+
number-format: digit
|
|
20
|
+
sentence-max: 8
|
|
21
|
+
|
|
22
|
+
# ── Identity Composition ──
|
|
23
|
+
# Filter the composed upstream: both audiences, primary pillars only.
|
|
24
|
+
|
|
25
|
+
scope
|
|
26
|
+
audience: regulars, newcomers
|
|
27
|
+
pillars: primary
|
|
28
|
+
|
|
29
|
+
# Context: only what is genuinely specific to this content type.
|
|
30
|
+
# These are slot-level instructions - not voice, not production params.
|
|
31
|
+
context
|
|
32
|
+
primary is the bread name, 1-3 words (e.g. "Roggenlaib").
|
|
33
|
+
secondary is the weight and price ("750 g · 5,80 €").
|
|
34
|
+
meta is one sentence: what makes this bread specific today.
|
|
35
|
+
Never describe the bread as "delicious" or "special" -
|
|
36
|
+
state a fact (flour type, fermentation time, seasonal ingredient).
|
|
37
|
+
|
|
38
|
+
# ── Slots ──
|
|
39
|
+
|
|
40
|
+
slot primary
|
|
41
|
+
label: Brotname
|
|
42
|
+
typography: headline
|
|
43
|
+
color: text-primary
|
|
44
|
+
maxLength: 30
|
|
45
|
+
samples:
|
|
46
|
+
- Roggenlaib
|
|
47
|
+
- Weizenmisch
|
|
48
|
+
- Dinkelvollkorn
|
|
49
|
+
|
|
50
|
+
slot secondary
|
|
51
|
+
label: Gewicht & Preis
|
|
52
|
+
typography: kicker
|
|
53
|
+
color: text-secondary
|
|
54
|
+
maxLength: 25
|
|
55
|
+
samples:
|
|
56
|
+
- 750 g · 5,80 €
|
|
57
|
+
- 500 g · 4,20 €
|
|
58
|
+
- 1 kg · 7,50 €
|
|
59
|
+
|
|
60
|
+
slot meta
|
|
61
|
+
label: Beschreibung
|
|
62
|
+
typography: caption
|
|
63
|
+
color: text-secondary
|
|
64
|
+
maxLength: 100
|
|
65
|
+
samples:
|
|
66
|
+
- Roggen 1150, 18 Stunden Teigführung, Kümmel aus dem Wendland.
|
|
67
|
+
- Tipo 1 aus der Drax-Mühle, 72 % Hydration.
|
|
68
|
+
- Saisonale Walnüsse vom Wochenmarkt, Emmer-Vollkorn.
|
|
69
|
+
|
|
70
|
+
slot label
|
|
71
|
+
label: Badge
|
|
72
|
+
typography: label
|
|
73
|
+
color: label-fg
|
|
74
|
+
maxLength: 20
|
|
75
|
+
samples:
|
|
76
|
+
- Heute frisch
|
|
77
|
+
- Nur samstags
|
|
78
|
+
- Neu im Laden
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Brand Intent / AI Skill
|
|
2
|
+
|
|
3
|
+
You are working in a project that uses **Brand Intent**, an open file format for encoding brand identity, expression intent, and content composition rules across five composable layers. This skill teaches you how to read, compose, and validate Brand Intent files.
|
|
4
|
+
|
|
5
|
+
## The Five Layers
|
|
6
|
+
|
|
7
|
+
Brand Intent distributes brand knowledge across five independent file types. Each answers one question:
|
|
8
|
+
|
|
9
|
+
| Layer | File | Question | Changes |
|
|
10
|
+
|-------|------|----------|---------|
|
|
11
|
+
| **Identity** | `.identity` | Who is this brand? | Rarely (strategy) |
|
|
12
|
+
| **Brand** | `.brand` | What expression intent does it derive? | Occasionally (design) |
|
|
13
|
+
| **Format** | `.format` | Where does this content appear? | When platforms change |
|
|
14
|
+
| **Purpose** | `.purpose` | What is this content trying to do? | When content types evolve |
|
|
15
|
+
| **Composition** | `.composition` | How are elements composed? | Reusable across brands |
|
|
16
|
+
|
|
17
|
+
The arrow between layers is a **derivation arrow**: Identity contains reasoning. Brand derives expression intent from that reasoning. Format, Purpose, and Composition condition specific acts of expression without repeating what upstream layers establish.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Identity → Brand → Format → Purpose → Composition
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How to Read Each Layer
|
|
24
|
+
|
|
25
|
+
### `.identity`: Strategy (no visual properties)
|
|
26
|
+
|
|
27
|
+
**Required:** essence, promise, voice (register, persona, rhythm, always/never), pillars, audience segments.
|
|
28
|
+
|
|
29
|
+
**Optional:** tagline, positioning, mission, vision, archetype, narrative, values (with behavior statements), anti-audience segments.
|
|
30
|
+
|
|
31
|
+
**The `always`/`never` construct is your primary guardrail.** These are concrete, testable rules. When generating or reviewing content, check every item in `never:` and confirm every item in `always:`. They are not suggestions; they are constraints.
|
|
32
|
+
|
|
33
|
+
**`values`** are not just keywords; each has a behavior statement that describes how it manifests. Use these to inform decisions when voice rules don't cover a situation.
|
|
34
|
+
|
|
35
|
+
**`anti-audience`** segments define who the brand is explicitly *not* for. Never generate content that sounds like it's addressing an anti-audience. This is as important as `voice.never`.
|
|
36
|
+
|
|
37
|
+
**`positioning`** gives the competitive frame: what the brand is the only one of. Use this when the content needs to differentiate.
|
|
38
|
+
|
|
39
|
+
**Does NOT contain:** hex values, font names, type sizes, spacing, composition rules, or any visual properties.
|
|
40
|
+
|
|
41
|
+
### `.brand`: Derived Expression Intent
|
|
42
|
+
|
|
43
|
+
Contains: color primitives, semantic themes, font definitions, named typography styles, `voice-constraints` (operationalized voice parameters), `content-defaults`, spacing, divider styles.
|
|
44
|
+
|
|
45
|
+
**`voice-constraints`** are production-enforceable limits derived from identity voice reasoning:
|
|
46
|
+
- `sentence-max:` maximum words per sentence. Enforce this.
|
|
47
|
+
- `headline-pattern:` structural pattern for headlines (e.g., `noun-first`, `verb-first`).
|
|
48
|
+
- `number-format:` `cardinal` means write "drei" not "3".
|
|
49
|
+
- `register:` must match the identity register.
|
|
50
|
+
|
|
51
|
+
**Does NOT contain:** identity reasoning, audience definitions, strategic prose, composition rules.
|
|
52
|
+
|
|
53
|
+
### `.format`: Canvas Definition
|
|
54
|
+
|
|
55
|
+
Contains: platform, dimensions, safe zones (danger/crop/comfort), grid, allowed purposes.
|
|
56
|
+
|
|
57
|
+
**You rarely need this directly** unless generating content that must fit specific dimensions or validating whether a purpose is allowed on a given platform.
|
|
58
|
+
|
|
59
|
+
### `.purpose`: Content Type Definition
|
|
60
|
+
|
|
61
|
+
Contains: semantic slot definitions (primary, secondary, detail, meta, cta, label), typography and color per slot, sample content, length constraints, `scope`, `context`.
|
|
62
|
+
|
|
63
|
+
**Semantic slots are roles, not positions:**
|
|
64
|
+
- `primary` = main communicative act (headline, key message)
|
|
65
|
+
- `secondary` = supporting signal (subline, number)
|
|
66
|
+
- `detail` = extended content (body, list)
|
|
67
|
+
- `meta` = contextual info (date, caption)
|
|
68
|
+
- `cta` = call to action
|
|
69
|
+
- `label` = orientation marker (badge, tag)
|
|
70
|
+
|
|
71
|
+
### `.composition`: Spatial Arrangement
|
|
72
|
+
|
|
73
|
+
Contains: slot-to-position mappings in grid or flow mode.
|
|
74
|
+
|
|
75
|
+
**You rarely need to read this** unless debugging layout issues. Compositions contain zero brand-specific information.
|
|
76
|
+
|
|
77
|
+
## Composing Context for Content Generation
|
|
78
|
+
|
|
79
|
+
When generating content for a specific purpose, compose your context by reading layers in order:
|
|
80
|
+
|
|
81
|
+
### Step 1: Read `.identity`
|
|
82
|
+
|
|
83
|
+
Load the full identity. Pay special attention to:
|
|
84
|
+
- `voice.always` and `voice.never`: these are your hard constraints
|
|
85
|
+
- `voice.persona`: adopt this voice
|
|
86
|
+
- `voice.register`: match this formality level
|
|
87
|
+
- `pillars.avoid`: never generate content in these topic areas
|
|
88
|
+
- `anti-audience`: never sound like you're addressing these people
|
|
89
|
+
- `values`: use behavior statements to guide decisions when voice rules don't cover a situation
|
|
90
|
+
- `positioning`: use when content needs to differentiate
|
|
91
|
+
|
|
92
|
+
### Step 2: Read `.brand`
|
|
93
|
+
|
|
94
|
+
Load voice-constraints and content-defaults:
|
|
95
|
+
- Enforce `sentence-max` on all generated text
|
|
96
|
+
- Follow `headline-pattern` for headlines
|
|
97
|
+
- Use `number-format` for numbers
|
|
98
|
+
- Apply `density` from content-defaults
|
|
99
|
+
|
|
100
|
+
### Step 3: Apply `scope` from `.purpose`
|
|
101
|
+
|
|
102
|
+
The purpose file's `scope` narrows the composed upstream:
|
|
103
|
+
- `audience:` only use language guidance from these audience blocks
|
|
104
|
+
- `pillars:` restrict to this pillar scope (`primary`, `secondary`, or `all`)
|
|
105
|
+
|
|
106
|
+
If no filter is specified, include everything.
|
|
107
|
+
|
|
108
|
+
### Step 4: Apply `context` from `.purpose`
|
|
109
|
+
|
|
110
|
+
Read the `context` block. This contains slot-specific instructions that cannot be derived from identity or brand. Follow them literally; they are precise, not general.
|
|
111
|
+
|
|
112
|
+
### Step 5: Generate per-slot content
|
|
113
|
+
|
|
114
|
+
For each slot in the purpose:
|
|
115
|
+
- Respect `maxLength` (hard character limit)
|
|
116
|
+
- Use `samples` as structural reference (match the pattern, not the words)
|
|
117
|
+
- Follow the `context` instructions for this slot
|
|
118
|
+
|
|
119
|
+
## Validation Checklist
|
|
120
|
+
|
|
121
|
+
After generating or reviewing content, validate against these rules in order:
|
|
122
|
+
|
|
123
|
+
1. **`voice.never`:** Does any generated text contain a phrase, pattern, or concept from the never list? If yes, rewrite.
|
|
124
|
+
2. **`voice.always`:** Does the text follow every rule in the always list? If not, fix.
|
|
125
|
+
3. **`anti-audience`:** Could this content be read as addressing an anti-audience? If yes, rewrite.
|
|
126
|
+
4. **`pillars.avoid`:** Does the content touch an avoided topic area? If yes, reject.
|
|
127
|
+
5. **`values`:** Does the content contradict any value's behavior statement? If yes, fix.
|
|
128
|
+
6. **`sentence-max`:** Count words in every sentence. Does any exceed the limit? If yes, split.
|
|
129
|
+
7. **`headline-pattern`:** Does the headline follow the specified pattern? If not, restructure.
|
|
130
|
+
8. **`maxLength`:** Does each slot's content fit within its character limit? If not, shorten.
|
|
131
|
+
9. **`register`:** Does the formality level match? (e.g., if `informal, Du`, no formal address)
|
|
132
|
+
|
|
133
|
+
## What NOT to Do
|
|
134
|
+
|
|
135
|
+
- **Do not restate identity in purpose-level work.** If a purpose's `context` block says "keep it warm and friendly," that belongs in `.identity`, not here. Flag it.
|
|
136
|
+
- **Do not invent voice rules.** Only apply constraints from the actual files. Your interpretation of the brand is less reliable than the authored rules.
|
|
137
|
+
- **Do not mix layers.** If asked to suggest a color, look in `.brand`. If asked about audience, look in `.identity`. Never guess from the wrong layer.
|
|
138
|
+
- **Do not ignore `never` rules.** These exist because the brand has been burned by generic AI output before. The `never` list is the most important part of the identity file.
|
|
139
|
+
|
|
140
|
+
## File Discovery
|
|
141
|
+
|
|
142
|
+
Brand Intent files follow this structure:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
# Single brand
|
|
146
|
+
brandname.identity
|
|
147
|
+
brandname.brand
|
|
148
|
+
formats/*.format
|
|
149
|
+
purposes/*.purpose
|
|
150
|
+
compositions/*.composition
|
|
151
|
+
|
|
152
|
+
# Multi-brand
|
|
153
|
+
brands/brandname/brandname.identity
|
|
154
|
+
brands/brandname/brandname.brand
|
|
155
|
+
formats/*.format ← shared
|
|
156
|
+
purposes/*.purpose ← shared
|
|
157
|
+
compositions/*.composition ← shared
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
To find the active brand, look for `.identity` and `.brand` files. There is exactly one of each per brand.
|
|
161
|
+
|
|
162
|
+
## Relationship to Impeccable
|
|
163
|
+
|
|
164
|
+
If this project also uses [Impeccable](https://impeccable.style), the two are complementary:
|
|
165
|
+
- **Impeccable** teaches you how to design well (general craft)
|
|
166
|
+
- **Brand Intent** teaches you what this brand's expression intent is (specific identity)
|
|
167
|
+
|
|
168
|
+
Apply both. Impeccable governs execution quality. Brand Intent governs brand specificity.
|