| name | pack-author |
| description | Author a new ccsidekick character pack end-to-end - source the figure, pick a theme, write 620 voice lines and 25 spinner verbs, then ship through lint, a generated README, and an autonomous review. Use when creating, building, or scaffolding a ccsidekick character pack. |
Authoring a ccsidekick character pack
A pack is data: one pack.json and a package.json, never executed code. The engine reads it;
nothing in a pack runs. All constraints are enforced by pack:lint. This skill drives the authoring
loop to a hard finish.
Prerequisite
The skill scripts run under bun. Confirm it is on PATH before starting:
bun --version
Both Claude Code and Codex CLIs spawn a shell, but bun is not guaranteed in a Codex environment.
If it is absent, none of the skill scripts will run.
Terminal state
The pack is done when all four conditions hold:
bun run pack:lint packages/packs/<name> exits 0: schema guard, placeholder gate, cross-cell
gate, pool counts, 66-column width, spinner-verb floor, near-duplicate threshold, legibility, and
package.json completeness (name, files covering the README and assets,
repository.directory, author).
bun test packages/core/src/packs/registry.test.ts passes.
- Every user-approval gate in the stages below is cleared.
- The user has approved the final
pack.json.
Lint passing is necessary, not sufficient. It passes on placeholder text, so a green lint does not
certify voice or art. User approval (gates 3 and 4) is the binding quality check.
Stage 1: Figure
The figure is a single static array of at most 9 rows × 25 columns. Mood changes engine-applied
color only: no per-mood art, no glyph shifts.
1. Scaffold the pack first (skip if the directory already exists):
bun .claude/skills/pack-author/scripts/scaffold.ts <name> --display "<DisplayName>" --emblem "<glyph>"
Creates packages/packs/<name>/ with a skeleton pack.json (every pool keyed, one unique
placeholder per leaf cell), a package.json, a README.md, and a REVIEW.md. Registers <name>
in PACKS and links it as a workspace:* runtime dependency of packages/core (every pack ships
bundled), both idempotently; re-running the scaffold overwrites the skeleton files and leaves an
existing registry entry and dependency untouched.
2. Install the workspace link. Run bun install from the workspace root. The render loader
resolves each pack through packages/core/node_modules/@ccsidekick/pack-<name>, which the workspace
only materializes for the declared core dependency the scaffold just added. Skip this and the pack
fails to load at render time: the statusline shot (Stage 7) silently drops the figure and leads with
the chip. This is a one-time step per pack.
3. Ask the user whether they have a reference image, a specific ASCII or braille art source, or
want generated candidates.
4. Generate five candidates. Art is sourced through tooling, never hand-drawn. The figure is
ALWAYS braille: every candidate, every pack, no exceptions. Braille packs four times the vertical
resolution of block or ASCII glyphs into the same 9×25 box, so the figure stays legible at
status-line size; ASCII and block art do not. Do not ship an ASCII or block figure even if it looks
fine in the preview.
- Use the
ascii-art skill with --style braille to convert a reference image, then hand-clean
it: fix ragged rows, thin the density, drop stray glyphs. Set attribution.artist and
attribution.source to the image credit. A clean, high-contrast
line drawing or bold silhouette converts far better than a flat-colored render or a
photograph. A solid-fill vector collapses to a featureless blob, and a photo turns to mush at nine
rows.
- For braille art, draw from a catalog (emojicombos.com, asciiart.eu). Pad blank braille cells
with
⠀ (U+2800), not an ASCII space. Mixing braille glyphs with ASCII spaces skews alignment
in most fonts; a uniform braille grid stays aligned. The legibility gate counts both ⠀ and a
space as empty, so density is the inked glyphs alone.
- Catalog braille sized larger than 9×25 rarely survives a down-rescale: shrinking a detailed
line drawing fragments its strokes, and a near-solid fill flattens to a block. Prefer a source
that is already at box size, or re-source from a clean line-drawing image.
Fit each candidate to ≤9×25.
5. Run the figure-options preview. Write the candidates to
packages/packs/<name>/figure-candidates.json — an array of { name, rows } objects — then pass
that file path:
bun .claude/skills/pack-author/scripts/figure-options.ts <packDir> \
--candidates packages/packs/<name>/figure-candidates.json
This writes .author/figures.html. Open it in a browser. The user picks a candidate or asks for a
tweak. Delete figure-candidates.json once a figure is chosen.
6. Ingest the figure. Write the chosen art into .author/figure.txt (the user may hand-edit
it), then run:
bun .claude/skills/pack-author/scripts/figure-ingest.ts <packDir>
This writes the art array into pack.json and runs --schema-only lint to confirm the figure
clears the box and legibility gate.
7. Attribution. Fill attribution.artist and attribution.source. Both are required; lint
fails on either being empty.
Gate: bun run pack:lint --schema-only packages/packs/<name> exits 0.
Stage 2: Emblem
Present four emblem candidates drawn from the character's visual vocabulary, plus a custom option.
Write the chosen glyph into pack.json emblem.
Gate: User confirms the emblem.
Stage 3: Theme
Every pack ships a theme block in pack.json, registering it as a selectable palette under its
own name. Four fields:
hues: 4 or 5 xterm-256 indices. Paints the statusline as solid bands and the figure as a
diagonal shimmer gradient.
comment: 2 or 3 xterm-256 indices for the character-line gradient.
signals: nominal (green-family, HSV 70° to 165°), caution (amber-family, 20° to 55°),
critical (red-family, ≤15° or ≥345°). Shade a signal color to fit the character; do not invert
its meaning.
separator: one index for │ and ⋯ dividers.
All indices must be in 17..231 with chroma (max minus min of R, G, B) ≥ 40 and brightness (max of
R, G, B) ≥ 95. No system colors (0 to 16), no greyscale (232 to 255).
1. Seed 5–10 candidates from the character's dominant colors. Write them to
packages/packs/<name>/theme-candidates.json — an array of objects, each with name, hues,
comment, signals, and separator.
2. Run the preview:
bun .claude/skills/pack-author/scripts/theme-options.ts <packDir> \
--candidates packages/packs/<name>/theme-candidates.json
This writes .author/themes.html. Open it. The script skips failing candidates and prints a
diagnostic for any index that violates the visibility rule.
3. Iterate until the user picks a candidate. Write the chosen theme object into pack.json.
Delete theme-candidates.json.
Gate: User has named the chosen theme and it is written into pack.json.
Stage 4: Voice anchor
1. Ask the user for voice references: canonical quotes, approved samples, character-defining
moments, and tone constraints. Record everything.
2. Propose tone (mild | edgy | offensive). Config carries no edginess knob; the pack's tone
is the only choice a user gets. Write every line at the pack's tone: there is no separate edgy pool.
Confirm tone after the taste sample in step 4.
3. Build voice-pack.md from voice-pack.template.md: calibration notes (tone, phrasing,
edginess ceiling, taboo subjects) and roughly 50 canonical lines drawn from the user's references.
4. Draft about 50 taste-sample lines spanning all five familiarity tiers (
stranger | acquaintance | friend | partner | legend), one named stack key per family, and a few
spinner verbs. Keep each line ≤ 66 display columns. Keep failure and limit lines uplifting. The
character emotes; it does not scold.
5. Show the samples to the user and confirm tone.
Gate: User approves the samples. Approval certifies the voice axes and unblocks Stage 5.
Stage 5: Write the 620
The voice library is exactly 620 lines, lint-enforced:
| pool | per cell | cells | lines |
|---|
| mood idle | 10 | 5 tiers | 50 |
| mood busy / happy / struggling / recovery | 5 | 4 × 5 tiers | 100 |
| greeting | 3 | 5 buckets × 5 tiers | 75 |
| first contact | 3 | 5 tiers | 15 |
| tier_up / comeback / streak / anniversary | 3 | 4 × 5 tiers | 60 |
| positive-git | 3 | 4 moments × 5 tiers | 60 |
| easter egg | 5 | 5 tiers | 25 |
| pressure mood | 3 | 3 moods | 9 |
| event reaction | 3 | 18 categories | 54 |
| stack moment | 3 | 2 moments × 27 stacks | 162 |
| date/clock egg | 10 | flat | 10 |
Rules for every line:
- ≤ 66 display columns (ANSI-stripped). Longer lines may wrap or truncate.
- No near-duplicates: within any leaf cell, no two lines at token-set Jaccard ≥ 0.80. Two lines that
share most of their words read as a repeat.
- Stay off the helpful catalog. The engine gives actionable instructions: billing, quota, context,
git commands. A character line reacts to the moment; it never restates a do-this directive. Do not
write "add it to
.gitignore", "unset ANTHROPIC_API_KEY", "run /compact", or any other action
command.
Writing strategy:
Partition leaf cells into non-overlapping batches.
On Claude: spawn one Sonnet subagent per batch (model: sonnet). Hand each writer voice-pack.md,
its assigned cells with per-cell counts, packages/packs/batman/pack.json as the structural
template, and bun run pack:lint --status packages/packs/<name> as the self-check command.
Writers share one working tree, so forbid tree-wide commands in every writer. A writer edits
only its assigned pack.json cells and runs only bun run pack:lint to self-check. It must never
run bun run format (Prettier rewrites the whole workspace) or any mutating git command (
checkout, add, reset, stash) — a writer that formats and then reverts with git checkout
silently wipes every other writer's edits and any staged changeset. Normalize formatting once
yourself, after all batches merge; never inside a writer.
Without subagents: write batches sequentially, same inputs per batch.
After merging all batches, run one cross-cell variety pass: break any joke or phrase that reappears
across cells, which the per-cell gate cannot catch.
Gate: bun run pack:lint packages/packs/<name> passes the full content gates.
Stage 6: Spinner verbs
The character rewrites Claude Code's spinner verbs in its own voice.
- Draft five verbs and show them to the user.
- After the user confirms the style, write the full set of at least 25 verbs into
pack.json
spinnerVerbs.
Gate: User approves the verb set. Lint enforces the 25-verb floor.
Stage 7: Ship
1. README + shot. Generate the pack's README.md and its themed statusline shot (needs a built
render binary):
bun run build
bun run pack:readme packages/packs/<name>
This writes README.md: the non-affiliation disclaimer, a tagline, the statusline preview rendered
in the pack's own theme (assets/statusline.svg), the figure as verbatim plain glyphs (no ANSI,
braille blanks preserved), one representative line per pool, and attribution. Do not hand-edit it;
regenerate whenever the figure, theme, or voice changes — including any hand-edit to pack.json
art, which the user may make directly. README.md and assets/statusline.svg are generated,
tracked artifacts: after any such change, re-run bun run pack:readme packages/packs/<name> (with a
current bun run build) so both stay in sync with pack.json, then eyeball the shot.
The shot only shows the figure if the pack resolves through its workspace link. If the statusline
leads with a [name] chip and no figure appears, the Stage 1 bun install never ran (or the core
devDependency is missing); fix the link and regenerate, don't touch the art.
2. Review. Dispatch a reviewer subagent (never an author). It records its findings in
REVIEW.md. The lint gates content the per-cell way; the reviewer catches what a per-cell check
cannot. Hand it the character's canonical references so it can judge authenticity, and require it to
quote offenders with their pool/tier location. It must check:
- Figure legibility and recognizability, and attribution filled with a real credit.
- Voice authenticity: every line reads as the actual character, not a generic buddy. Flag lines
that could belong to any pack once the emblem is swapped.
- Cross-cell templates. The
stack.* slow/fail pools are the top offenders: they invite a
fill-in-the-blank mold ([subsystem] does X slowly. [detached aphorism]. or
[error]. [one-word reaction].) with a tech noun swapped per cell. Real molds caught in review: a
shared closer verb (We'll <fix> it and <retry>. on 64 of 81 fail lines), a shared shape
([failure]. [Gerund action]! on most fail lines), and a shared reassurance (every slow[2]
ending "soon it arrives"). Read down a column, not across a row: flag any phrase, joke, image,
metaphor, or closer that recurs across cells with only trivial variation. The per-cell Jaccard
gate is blind to it, but a user working across stacks sees the same clever line every time.
- Signature-reference saturation. A catchphrase, name, or motif (the character's "elementary",
"Watson", cards, cheeks, ketchup) reused as a catch-all until it reads as wallpaper. Name a
sensible ceiling per motif. A signature syntax counts too: if the character's gimmick is a
speech pattern (Yoda's inverted word order), canon applies it selectively; a pack that inverts
every line reads as parody. Mix the gimmick with plain sentences.
- Cross-pack contamination. Every line must be this character's own. Flag any line lifted from
another pack's canon (Batman's "Why do we fall?" pasted into another figure) or any signature line
that belongs to a different character within the same franchise (a Naruto pack voicing Kakashi's
or Rock Lee's catchphrase). When a shared reference is unavoidable, it must sound like this
character saying it.
- AI tells against the house norm: em-dash density (the batman reference uses the em-dash in
~1 line across all 620; flag a pack that leans on it in dozens), negative-parallelism closers
("X, not Y"; "not defeat, merely data"), the rule of three, hollow motivational filler, and
doubled-adverb tics.
- Safety and uplift. Failure/limit lines must stay uplifting and must never nudge the user
toward harm or toward prolonging a breakage — no cheering on a dangerous op, no "keep it broken".
This holds even for a menacing character; the figure emotes, it does not scold or egg the user on.
- No actionable instructions: the character reacts to a moment, it never restates a do-this
directive (no "run /compact", "add to
.gitignore", "try a shorter query", or any command).
3. Re-author. Address every cell the reviewer flags. Re-run pack:lint and regenerate the
README.md + shot.
4. Final approval. Show the user the final pack.json and generated README.md. Their sign-off
satisfies terminal-state gate 4.
5. Reflect the pack in the top-level README, then refresh the reel and the landing page. Make
two hand edits to the repo root README.md (the readme:drift guard enforces the count):
- Add the character to the Available packs list — its display name and a one-line description in
the existing style (tone + signature palette, e.g. "a mild, upbeat pack that crackles in electric
yellow"). Keep the list alphabetical.
- Bump the
characters-<n>%20packs count badge to the new pack-directory count. Widgets and themes
do not change per pack (themes is an advertised floor, so a growing catalog still passes).
Then regenerate the repo-wide assets. Both the reel and the landing page auto-discover packs from
packages/packs/*, so the new one is picked up with no list to edit:
- the cross-fade reel MP4 (
assets/characters.mp4, for social) and a slower, size-capped GIF (
assets/characters.gif, the animated README hero) — every card a live render in a varied session
state (different widgets, helpful tip, and mood);
- the landing page under
website/ — site:build re-renders every theme and character shot and
regenerates website/data.js. The character wall, the theme and widget filmstrips, and the
headline counts all derive from pack.json and the engine's exported constants, so the new pack's
card, its own theme, and the bumped counts appear automatically. Its position on the wall is
the one thing that is not auto-derived: it comes from the ORDER list in
scripts/website/site-data-build.ts, and site:drift fails until the new pack is added there (
see below).
bun run build && bun run character:reel # reel: needs ffmpeg + Chrome
bun run site:build # landing page: re-render every shot + regenerate website/data.js
bun run readme:drift && bun run site:drift # confirm the README and the website both match the packs
site:drift is the website's counterpart to readme:drift: it recomputes website/data.js from
the
packs/themes/widgets and deep-compares, failing on any drift (a changed colour, an added pack, a
relabelled widget). Add the new pack to two ordered lists:
ORDER in scripts/website/site-data-build.ts — the authoritative character-wall order.
site:drift fails until every pack is listed here, so add the name wherever you want it on
the wall (not just the tail).
CURATED=(…) in scripts/assets/character-reel.sh — the reel order; add the name to lead the
pack earlier than the tail.
Also add a "<name> pack" entry to .github/labeler.yml: every pack has its own PR label, and the
CI labeler (which colours each pack label from its theme) reads this list. Commit the README and the
refreshed source assets under assets/ alongside the pack in step 7; website/data.js and the
copied website/ media are CI-built and gitignored, so they are not committed.
6. Add a changeset so the pack gets released. Releases run
on Changesets:
a package is published only when a changeset raises its version. The Release workflow discovers
packs through a packages/packs/* glob, so no workflow edit is ever needed. Just declare the bump:
bun run changeset
Select @ccsidekick/pack-<name>, choose minor (a brand-new pack's first real version), and write
a one-line summary. That writes a .changeset/*.md file; commit it with the pack. Nothing publishes
until a maintainer runs the Release workflow, which opens a "Version Packages" PR and, once merged,
publishes.
Note the internal-dependency behavior, in case the pack is batman: the engine (ccsidekick) has a
runtime dependency on @ccsidekick/pack-batman, so a batman changeset also cascades a patch bump to
the engine (it ships batman). The other packs are engine devDependencies and never cascade;
they version independently. Do not add linked/fixed entries for packs; independent versioning is
intentional.
7. Delete REVIEW.md and commit. REVIEW.md is transient reviewer scratch, removed at ship
so packs never carry it — the same treatment as the .author/ working files. Delete it before the
commit so it never enters history, confirm the parity test, then commit by path (include the
changeset from step 6 and the refreshed assets from step 5):
rm packages/packs/<name>/REVIEW.md
bun test packages/core/src/packs/registry.test.ts
git add packages/packs/<name> .changeset README.md assets/characters.mp4 assets/characters.gif website/data.js
git commit -m "feat(packs): add <name> pack"
Do not git commit --amend across a multi-commit local series (amend hits HEAD, not the commit
you intend). A secret-scanning hook runs on every push.
Quick reference
| Command | Purpose |
|---|
bun .claude/skills/pack-author/scripts/scaffold.ts <name> --display "<N>" --emblem "<g>" | Create skeleton pack, register in PACKS, link as core runtime dependency (then bun install) |
bun .claude/skills/pack-author/scripts/figure-options.ts <packDir> --candidates <json> | Render figure candidates → .author/figures.html |
bun .claude/skills/pack-author/scripts/figure-ingest.ts <packDir> | Write .author/figure.txt into pack.json art; runs --schema-only lint |
bun .claude/skills/pack-author/scripts/theme-options.ts <packDir> --candidates <json> | Render theme candidates → .author/themes.html |
bun run pack:lint --schema-only packages/packs/<name> | Schema guard + legibility gate (skips content counts) |
bun run pack:lint --status packages/packs/<name> | Per-cell fill status: current count vs. target per leaf |
bun run pack:lint packages/packs/<name> | Full lint: all schema, content, and quality gates |
bun run pack:readme packages/packs/<name> | Write README.md + themed assets/statusline.svg (needs bun run build) |
bun run character:reel | Regenerate the roster reel: assets/characters.mp4 (social) + assets/characters.gif (README) |
bun run site:build | Rebuild the landing page: re-render every theme/character shot + regenerate website/data.js |
bun run site:drift | Guard: fail if website/data.js or the render set is stale against the packs |
bun test packages/core/src/packs/registry.test.ts | Registry parity test |