spirewise 1.0.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 ADDED
@@ -0,0 +1,68 @@
1
+ # spirewise
2
+
3
+ Installable **Agent Skills** for copywriting — generate ready-to-paste copy for
4
+ your **F6S** and **LinkedIn** company pages, with strict character-limit safety.
5
+ Works with **GitHub Copilot**, **Claude Code**, and **Cursor**.
6
+
7
+ ## Install
8
+
9
+ No clone needed — run it with `npx`:
10
+
11
+ ```bash
12
+ # install every skill
13
+ npx spirewise install --all
14
+
15
+ # install a single skill
16
+ npx spirewise install f6s-copywriting
17
+
18
+ # install several
19
+ npx spirewise install f6s-copywriting linkedin-copywriting
20
+
21
+ # interactive picker
22
+ npx spirewise install
23
+
24
+ # list what's available
25
+ npx spirewise list
26
+ ```
27
+
28
+ Or install the CLI globally:
29
+
30
+ ```bash
31
+ npm install -g spirewise
32
+ spirewise install --all
33
+ ```
34
+
35
+ ## Choose the target agent / folder
36
+
37
+ ```bash
38
+ spirewise install --all --agent claude # ~/.claude/skills (default)
39
+ spirewise install --all --agent copilot # ~/.copilot/skills
40
+ spirewise install --all --agent cursor # ~/.cursor/skills
41
+ spirewise install --all --target ./skills # any custom folder
42
+ ```
43
+
44
+ Run `spirewise --help` for all options.
45
+
46
+ ## Skills included
47
+
48
+ | Skill | Output file | Purpose |
49
+ |-------|-------------|---------|
50
+ | `f6s-copywriting` | `f6s/f6s-profile-copy.txt` | Full F6S startup profile copy |
51
+ | `linkedin-copywriting` | `linkedin copywriting/linkedin-page-copy.txt` | Full LinkedIn Company Page copy |
52
+
53
+ Each skill keeps **every field below** the platform's character limit (with ~10%
54
+ headroom) and verifies counts before finishing.
55
+
56
+ ## Usage after install
57
+
58
+ Once installed, just ask your agent:
59
+
60
+ > "Write our F6S profile copy"
61
+ > "Create the LinkedIn company page copy"
62
+
63
+ The agent follows the skill: gathers your company details, writes the `.txt`
64
+ file in the right folder, and confirms all character counts stay under limit.
65
+
66
+ ## License
67
+
68
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * spirewise — install copywriting Agent Skills into your AI agent.
6
+ *
7
+ * Usage:
8
+ * npx spirewise list
9
+ * npx spirewise install --all
10
+ * npx spirewise install f6s-copywriting
11
+ * npx spirewise install f6s-copywriting linkedin-copywriting
12
+ * npx spirewise install (interactive picker)
13
+ * npx spirewise install --agent cursor --all
14
+ * npx spirewise install --target ./my/skills --all
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const readline = require('readline');
20
+
21
+ const PKG_ROOT = path.resolve(__dirname, '..');
22
+ const SKILLS_DIR = path.join(PKG_ROOT, 'skills');
23
+
24
+ const c = {
25
+ reset: '\x1b[0m', blue: '\x1b[1;34m', green: '\x1b[1;32m',
26
+ yellow: '\x1b[1;33m', red: '\x1b[1;31m', dim: '\x1b[2m',
27
+ };
28
+ const info = (m) => console.log(`${c.blue}==>${c.reset} ${m}`);
29
+ const ok = (m) => console.log(`${c.green} ok${c.reset} ${m}`);
30
+ const warn = (m) => console.error(`${c.yellow} !${c.reset} ${m}`);
31
+ const die = (m) => { console.error(`${c.red}err${c.reset} ${m}`); process.exit(1); };
32
+
33
+ const AGENT_DIRS = {
34
+ claude: path.join(os_home(), '.claude', 'skills'),
35
+ copilot: path.join(os_home(), '.copilot', 'skills'),
36
+ cursor: path.join(os_home(), '.cursor', 'skills'),
37
+ };
38
+
39
+ function os_home() {
40
+ return process.env.HOME || process.env.USERPROFILE || require('os').homedir();
41
+ }
42
+
43
+ function availableSkills() {
44
+ if (!fs.existsSync(SKILLS_DIR)) return [];
45
+ return fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
46
+ .filter((d) => d.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, d.name, 'SKILL.md')))
47
+ .map((d) => d.name)
48
+ .sort();
49
+ }
50
+
51
+ function copyDir(src, dest) {
52
+ fs.mkdirSync(dest, { recursive: true });
53
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
54
+ const s = path.join(src, entry.name);
55
+ const d = path.join(dest, entry.name);
56
+ if (entry.isDirectory()) copyDir(s, d);
57
+ else fs.copyFileSync(s, d);
58
+ }
59
+ }
60
+
61
+ function installSkill(skill, targetDir) {
62
+ const src = path.join(SKILLS_DIR, skill);
63
+ if (!fs.existsSync(src)) die(`Skill '${skill}' not found in package.`);
64
+ const dest = path.join(targetDir, skill);
65
+ copyDir(src, dest);
66
+ ok(`installed ${skill} ${c.dim}-> ${dest}${c.reset}`);
67
+ }
68
+
69
+ function usage() {
70
+ console.log(`spirewise — install copywriting Agent Skills
71
+
72
+ Usage:
73
+ spirewise list
74
+ spirewise install [skill ...] [options]
75
+
76
+ Commands:
77
+ list List available skills
78
+ install Install skills (default command)
79
+
80
+ Options:
81
+ --all Install every available skill
82
+ --agent <name> Target agent: claude | copilot | cursor (default: claude)
83
+ --target <dir> Install into a specific folder (overrides --agent)
84
+ -h, --help Show this help
85
+
86
+ Available skills:
87
+ ${availableSkills().map((s) => ' - ' + s).join('\n') || ' (none found)'}
88
+
89
+ Examples:
90
+ npx spirewise install --all
91
+ npx spirewise install f6s-copywriting
92
+ npx spirewise install --agent cursor --all
93
+ `);
94
+ }
95
+
96
+ function parseArgs(argv) {
97
+ const opts = { all: false, agent: 'claude', target: null, skills: [] };
98
+ for (let i = 0; i < argv.length; i++) {
99
+ const a = argv[i];
100
+ if (a === '--all') opts.all = true;
101
+ else if (a === '--agent') opts.agent = argv[++i] || die('--agent needs a value');
102
+ else if (a === '--target') opts.target = argv[++i] || die('--target needs a value');
103
+ else if (a === '-h' || a === '--help') { usage(); process.exit(0); }
104
+ else if (a.startsWith('-')) die(`Unknown option: ${a}`);
105
+ else opts.skills.push(a);
106
+ }
107
+ return opts;
108
+ }
109
+
110
+ function pick(available) {
111
+ return new Promise((resolve) => {
112
+ if (!process.stdin.isTTY) {
113
+ die("No skills specified. Use --all or name skills (run in a terminal for the picker).");
114
+ }
115
+ info('Select skills to install (space-separated numbers, or "a" for all):');
116
+ available.forEach((s, i) => console.log(` ${i + 1}) ${s}`));
117
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
118
+ rl.question(' > ', (answer) => {
119
+ rl.close();
120
+ const t = answer.trim().toLowerCase();
121
+ if (t === 'a') return resolve(available.slice());
122
+ const chosen = [];
123
+ for (const tok of t.split(/\s+/)) {
124
+ const idx = parseInt(tok, 10) - 1;
125
+ if (idx >= 0 && idx < available.length) chosen.push(available[idx]);
126
+ }
127
+ resolve(chosen);
128
+ });
129
+ });
130
+ }
131
+
132
+ async function main() {
133
+ let argv = process.argv.slice(2);
134
+ let command = 'install';
135
+ if (argv[0] === 'list' || argv[0] === 'install') { command = argv[0]; argv = argv.slice(1); }
136
+ else if (argv[0] === '-h' || argv[0] === '--help') { usage(); return; }
137
+
138
+ const available = availableSkills();
139
+ if (available.length === 0) die('No skills found in package.');
140
+
141
+ if (command === 'list') {
142
+ info('Available skills:');
143
+ available.forEach((s) => console.log(' - ' + s));
144
+ return;
145
+ }
146
+
147
+ const opts = parseArgs(argv);
148
+
149
+ if (opts.agent && !AGENT_DIRS[opts.agent] && !opts.target) {
150
+ die(`Unknown agent '${opts.agent}' (use claude | copilot | cursor)`);
151
+ }
152
+ const targetDir = opts.target ? path.resolve(opts.target) : AGENT_DIRS[opts.agent];
153
+
154
+ let selected = opts.all ? available.slice() : opts.skills.slice();
155
+ if (selected.length === 0) selected = await pick(available);
156
+ if (selected.length === 0) die('Nothing selected.');
157
+
158
+ for (const s of selected) {
159
+ if (!available.includes(s)) die(`Unknown skill '${s}'. Run "spirewise list" to see options.`);
160
+ }
161
+
162
+ info(`Target: ${targetDir}`);
163
+ fs.mkdirSync(targetDir, { recursive: true });
164
+ for (const s of selected) installSkill(s, targetDir);
165
+ ok(`Done. Installed ${selected.length} skill(s).`);
166
+ info('Restart your agent or reload skills if needed.');
167
+ }
168
+
169
+ main().catch((e) => die(e && e.message ? e.message : String(e)));
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "spirewise",
3
+ "version": "1.0.0",
4
+ "description": "Installable Agent Skills for copywriting (F6S & LinkedIn company pages) for GitHub Copilot, Claude Code, and Cursor.",
5
+ "bin": {
6
+ "spirewise": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "skills/"
11
+ ],
12
+ "scripts": {
13
+ "test": "node bin/cli.js list"
14
+ },
15
+ "keywords": [
16
+ "agent-skills",
17
+ "skills",
18
+ "copywriting",
19
+ "f6s",
20
+ "linkedin",
21
+ "github-copilot",
22
+ "claude-code",
23
+ "cursor",
24
+ "cli"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=14"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/spirerise/spirewise.git"
33
+ }
34
+ }
@@ -0,0 +1,66 @@
1
+ # Copywriting Skills
2
+
3
+ Portable [Agent Skills](https://code.visualstudio.com/) for generating
4
+ ready-to-paste copy for company profile pages, with strict character-limit safety.
5
+
6
+ ## Skills
7
+
8
+ | Skill | Output | Purpose |
9
+ |-------|--------|---------|
10
+ | `f6s-copywriting` | `f6s/f6s-profile-copy.txt` | Full F6S startup profile copy |
11
+ | `linkedin-copywriting` | `linkedin copywriting/linkedin-page-copy.txt` | Full LinkedIn Company Page copy |
12
+
13
+ Each skill writes one plain-text file and keeps **every field below** the
14
+ platform's character limit (with ~10% headroom).
15
+
16
+ ## How to use with each agent
17
+
18
+ These follow the standard `SKILL.md` format (YAML frontmatter + instructions), so
19
+ they work anywhere a skill folder is recognized. To activate:
20
+
21
+ - **Claude Code**: copy a skill folder into `.claude/skills/` (or `~/.claude/skills/`),
22
+ then ask e.g. "write our F6S profile".
23
+ - **GitHub Copilot**: reference the skill file in chat, or place it where your
24
+ Copilot setup loads skills/instructions, then ask for the F6S/LinkedIn copy.
25
+ - **Cursor**: point Cursor at the relevant `SKILL.md` (e.g. add it to context or
26
+ your rules) and ask for the copy.
27
+
28
+ Either way, just ask the agent to "create the F6S page copy" or "write the
29
+ LinkedIn company page copy" and it will follow the skill: gather inputs, write the
30
+ `.txt` file in the right folder, and verify all character counts.
31
+
32
+ ## Install via command
33
+
34
+ The easiest way is the npm CLI (skills are bundled in the package):
35
+
36
+ ```bash
37
+ npx spirewise install --all # install every skill
38
+ npx spirewise install f6s-copywriting # install just one
39
+ npx spirewise install # interactive picker
40
+ npx spirewise list # list available skills
41
+ ```
42
+
43
+ Target a specific agent or folder:
44
+
45
+ ```bash
46
+ npx spirewise install --all --agent claude # ~/.claude/skills (default)
47
+ npx spirewise install --all --agent copilot # ~/.copilot/skills
48
+ npx spirewise install --all --agent cursor # ~/.cursor/skills
49
+ npx spirewise install --all --target ./skills
50
+ ```
51
+
52
+ ### No-Node fallback: `install.sh`
53
+
54
+ If you can't use npm, the bundled `install.sh` does the same over `curl`:
55
+
56
+ ```bash
57
+ # from a clone
58
+ ./install.sh --all
59
+
60
+ # remote
61
+ curl -fsSL https://raw.githubusercontent.com/spirerise/spirewise/main/install.sh | bash -s -- --all
62
+ ```
63
+
64
+ Run `./install.sh --help` or `npx spirewise --help` for all options.
65
+
66
+
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: f6s-copywriting
3
+ description: >-
4
+ Generate complete, ready-to-paste copywriting for an F6S (f6s.com) startup/company
5
+ profile page. Use when the user asks to "write our F6S profile", "create F6S
6
+ copy", "fill out F6S page", or prepare content for an accelerator / investor
7
+ application on F6S. Produces a single .txt file under an `f6s/` folder in the
8
+ project root, with every field kept STRICTLY under its character limit.
9
+ ---
10
+
11
+ # F6S Profile Copywriting
12
+
13
+ Produce field-by-field copy for an F6S company profile, save it to a plain-text
14
+ file, and guarantee every field stays **below** the platform character limits.
15
+
16
+ ## When to use
17
+
18
+ The user wants copy for their F6S page (startup directory, accelerator/investor
19
+ applications, hiring, or funding). If you lack details about the company, ask a
20
+ few focused questions first (see "Inputs needed").
21
+
22
+ ## Inputs needed
23
+
24
+ Before writing, gather (ask the user, or infer from the repo/website if available):
25
+
26
+ - Company name and a one-line description of what it does
27
+ - Industry / market and target customer
28
+ - Stage (idea, MVP, pre-revenue, seed, scale, etc.) and business model
29
+ - Key traction (users, revenue, growth, partnerships, awards, press)
30
+ - What they are seeking (funding, talent, partners, customers)
31
+ - Founders / key team, location, founded date, website, social links
32
+
33
+ If the user says "just draft it", make reasonable, clearly-marked placeholder
34
+ assumptions (e.g. `[FOUNDED: 2023]`) rather than blocking.
35
+
36
+ ## Output location
37
+
38
+ 1. Create a folder named `f6s` in the **project root** if it does not exist.
39
+ 2. Write a single file: `f6s/f6s-profile-copy.txt`.
40
+ 3. Use UTF-8 plain text only (no markdown styling inside the .txt).
41
+
42
+ ## F6S fields and HARD character limits
43
+
44
+ Treat these as ceilings. **Always stay below the limit** — leave ~10% headroom
45
+ and never emit a field that reaches or exceeds the cap. After writing each field,
46
+ count the characters and trim if needed.
47
+
48
+ | Field | Limit (chars) | Target (stay under) | Notes |
49
+ |------------------------|---------------|---------------------|-------|
50
+ | Company name | 50 | ≤ 40 | Public/legal name |
51
+ | Tagline / one-liner | 140 | ≤ 120 | Plain, specific, no hype |
52
+ | Short description | 255 | ≤ 230 | 1–2 sentences, what + for whom |
53
+ | Full description | 2000 | ≤ 1800 | Mission, product, differentiation, goals |
54
+ | Product description | 1000 | ≤ 900 | Features + what makes it different |
55
+ | Business model | 500 | ≤ 450 | How you make money |
56
+ | Traction / achievements| 1000 | ≤ 900 | Concrete metrics, awards, press |
57
+ | Needs / what we seek | 500 | ≤ 450 | Funding / talent / partners |
58
+ | Team member bio (each) | 500 | ≤ 450 | Per person |
59
+ | Market / industry tags | n/a (select) | — | Pick from F6S list |
60
+
61
+ If the platform shows a different live limit than above, obey the live limit and
62
+ keep the same "stay under" headroom rule.
63
+
64
+ ## Copy rules (F6S style)
65
+
66
+ - Be **specific and factual**. Avoid "best", "world-class", "#1", "revolutionary".
67
+ - Lead with the problem and the customer, then the solution.
68
+ - Use real numbers for traction; if unknown, insert a clear `[PLACEHOLDER]`.
69
+ - Write in third person or first-person plural ("we") consistently.
70
+ - No keyword stuffing; F6S profiles are read by humans (accelerators/investors).
71
+
72
+ ## Required .txt output template
73
+
74
+ Write the file using exactly this structure. Put the live character count in
75
+ brackets after each filled field, e.g. `(118/140)`.
76
+
77
+ ```
78
+ F6S PROFILE COPY — <Company Name>
79
+ Generated: <YYYY-MM-DD>
80
+
81
+ [COMPANY NAME] (xx/50)
82
+ <value>
83
+
84
+ [TAGLINE / ONE-LINER] (xx/140)
85
+ <value>
86
+
87
+ [SHORT DESCRIPTION] (xx/255)
88
+ <value>
89
+
90
+ [FULL DESCRIPTION] (xx/2000)
91
+ <value>
92
+
93
+ [PRODUCT DESCRIPTION] (xx/1000)
94
+ <value>
95
+
96
+ [BUSINESS MODEL] (xx/500)
97
+ <value>
98
+
99
+ [TRACTION & ACHIEVEMENTS](xx/1000)
100
+ <value>
101
+
102
+ [NEEDS / SEEKING] (xx/500)
103
+ <value>
104
+
105
+ [MARKET / INDUSTRY]
106
+ <comma-separated tags>
107
+
108
+ [LOCATION]
109
+ <city, country>
110
+
111
+ [FOUNDED]
112
+ <month year>
113
+
114
+ [WEBSITE]
115
+ <url>
116
+
117
+ [SOCIAL LINKS]
118
+ LinkedIn: <url>
119
+ X/Twitter: <url>
120
+ Other: <url>
121
+
122
+ [TEAM]
123
+ - <Name> — <Role> — <≤450 char bio> — <LinkedIn url>
124
+ ```
125
+
126
+ ## Verification checklist (run before finishing)
127
+
128
+ 1. The `f6s/` folder exists in the project root and contains `f6s-profile-copy.txt`.
129
+ 2. Every length-limited field shows a `(count/limit)` and `count < limit` with headroom.
130
+ 3. No field is empty unless intentionally a `[PLACEHOLDER]`.
131
+ 4. No banned hype words; tone is factual and specific.
132
+ 5. Confirm counts programmatically (e.g. `awk`/`wc -m` per field) — do not trust eyeballing.
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: linkedin-copywriting
3
+ description: >-
4
+ Generate complete, ready-to-paste copywriting for a LinkedIn Company Page.
5
+ Use when the user asks to "write our LinkedIn page", "create LinkedIn company
6
+ copy", "fill out the About section", or prepare LinkedIn page content. Produces
7
+ a single .txt file under a `linkedin copywriting/` folder in the project root,
8
+ with every field kept STRICTLY under its character limit.
9
+ ---
10
+
11
+ # LinkedIn Company Page Copywriting
12
+
13
+ Produce field-by-field copy for a LinkedIn **Company Page**, save it to a plain-text
14
+ file, and guarantee every field stays **below** LinkedIn's character limits.
15
+
16
+ ## When to use
17
+
18
+ The user wants copy for their LinkedIn company/organization page (tagline, About
19
+ section, specialties, etc.). If company details are missing, ask focused
20
+ questions first (see "Inputs needed").
21
+
22
+ ## Inputs needed
23
+
24
+ Before writing, gather (ask the user, or infer from the repo/website if available):
25
+
26
+ - Company name and what it does in one line
27
+ - Industry, target audience, and core offerings
28
+ - Mission / values and key differentiators
29
+ - Top keywords customers/recruiters would search (for tagline + specialties + SEO)
30
+ - Website, headquarters location, company size, founded year
31
+
32
+ If the user says "just draft it", use clearly-marked placeholders (e.g.
33
+ `[FOUNDED: 2023]`) instead of blocking.
34
+
35
+ ## Output location
36
+
37
+ 1. Create a folder named `linkedin copywriting` in the **project root** if it does not exist.
38
+ 2. Write a single file: `linkedin copywriting/linkedin-page-copy.txt`.
39
+ 3. Use UTF-8 plain text only (no markdown styling inside the .txt).
40
+
41
+ ## LinkedIn Company Page fields and HARD character limits
42
+
43
+ Treat these as ceilings. **Always stay below the limit** — leave ~10% headroom
44
+ and never emit a field that reaches or exceeds the cap. Count characters per
45
+ field and trim if needed.
46
+
47
+ | Field | Limit (chars) | Target (stay under) | Notes |
48
+ |------------------------------|---------------|---------------------|-------|
49
+ | Company name | 100 | ≤ 90 | Official name |
50
+ | Tagline | 120 | ≤ 110 | Appears under the name; keyword-rich |
51
+ | About / Overview | 2000 | ≤ 1800 | Front-load value in first ~150 chars (preview) |
52
+ | Specialty (each item) | 30 | ≤ 28 | Up to 20 items total |
53
+ | Specialties (total count) | 20 items | ≤ 20 | Keyword phrases |
54
+ | Custom button / CTA text | (preset) | — | Choose from LinkedIn presets |
55
+ | Featured post / update text | 3000 | ≤ 2700 | If drafting a launch/intro post |
56
+
57
+ If LinkedIn shows a different live limit than above, obey the live limit and keep
58
+ the same "stay under" headroom rule.
59
+
60
+ ## Copy rules (LinkedIn style)
61
+
62
+ - **Front-load the About section**: the first ~150 characters show as a preview
63
+ before "see more", so lead with the strongest value statement and keywords.
64
+ - Write for both **humans and search** — include the terms recruiters, customers,
65
+ and partners would search, naturally (no stuffing).
66
+ - Tagline should be benefit- or keyword-led, not a slogan with no information.
67
+ - Specialties = short keyword phrases (services, technologies, markets), each ≤ 28 chars.
68
+ - Tone: professional, confident, specific. Avoid empty hype.
69
+ - End the About section with a clear next step (visit site, follow, contact).
70
+
71
+ ## Required .txt output template
72
+
73
+ Write the file using exactly this structure. Put the live character count in
74
+ brackets after each filled field, e.g. `(108/120)`.
75
+
76
+ ```
77
+ LINKEDIN COMPANY PAGE COPY — <Company Name>
78
+ Generated: <YYYY-MM-DD>
79
+
80
+ [COMPANY NAME] (xx/100)
81
+ <value>
82
+
83
+ [TAGLINE] (xx/120)
84
+ <value>
85
+
86
+ [ABOUT / OVERVIEW] (xx/2000)
87
+ <value (first ~150 chars are the preview — make them count)>
88
+
89
+ [SPECIALTIES] (count: xx/20)
90
+ 1. <item> (xx/30)
91
+ 2. <item> (xx/30)
92
+ ...
93
+
94
+ [WEBSITE]
95
+ <url>
96
+
97
+ [INDUSTRY]
98
+ <value>
99
+
100
+ [COMPANY SIZE]
101
+ <range, e.g. 11-50 employees>
102
+
103
+ [HEADQUARTERS]
104
+ <city, country>
105
+
106
+ [FOUNDED]
107
+ <year>
108
+
109
+ [OPTIONAL — LAUNCH/INTRO POST] (xx/3000)
110
+ <value>
111
+ ```
112
+
113
+ ## Verification checklist (run before finishing)
114
+
115
+ 1. The `linkedin copywriting/` folder exists in the project root and contains
116
+ `linkedin-page-copy.txt`.
117
+ 2. Every length-limited field shows a `(count/limit)` and `count < limit` with headroom.
118
+ 3. Specialties: ≤ 20 items, each ≤ 28 chars.
119
+ 4. About section's first ~150 chars work as a standalone preview hook.
120
+ 5. Confirm counts programmatically (e.g. `awk`/`wc -m` per field) — do not trust eyeballing.