Configure white-label client branding for Parallax report output. Extracts colors, logos, fonts AND writing voice from a folder of client collateral (PowerPoint, Word docs), a website URL, a PDF brand guide, or guided wizard intake. Multi-source ingestion supported: when more than one source is provided, mismatches between sources are flagged at the confirmation gate rather than silently resolved. Validates visual assets against WCAG accessibility standards and voice section against corpus-si...
Scanned 9/11/2026
Install to Claude Code
npx -y skills add bencharoenwong/parallax-workflows --skill parallax-white-label-onboard --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Parallax White Label Onboard?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/bencharoenwong-parallax-white-label-onboard-parallax-workflows)More formats (shields.io, HTML) on the badges page.
---
name: parallax-white-label-onboard
description: "Configure white-label client branding for Parallax report output. Extracts colors, logos, fonts AND writing voice from a folder of client collateral (PowerPoint, Word docs), a website URL, a PDF brand guide, or guided wizard intake. Multi-source ingestion supported: when more than one source is provided, mismatches between sources are flagged at the confirmation gate rather than silently resolved. Validates visual assets against WCAG accessibility standards and voice section against corpus-size and completeness thresholds, presents a confirmation gate, then saves branding to ~/.parallax/client-branding/ where downstream skills inject it: visual goes into client-review, due-diligence, and deep-dive PDFs; voice goes into letter-writing, newsletter, and meeting-prep skills. Use to onboard a new client brand, update an existing one, or audit what branding is currently active."
---
# White-Label Client Onboarding
## When not to use
- Generating a client report → use /parallax-client-review (it loads the saved branding automatically)
- One-off color/font question → answer inline, don't save as config
- Portfolio construction → use /parallax-portfolio-builder
- Single-prompt voice rewrite of one piece of content → use /humanizer or /chicago-global-voice; this skill is for persistent client brand state
## Gotchas
- JIT-load _parallax/white-label/schema.yaml before extraction — it is the single source of truth for config.yaml shape
- JIT-load _parallax/white-label/extract/ (the extract package) and _parallax/white-label/validator.py before running Steps 2 and 3
- JIT-load `_parallax/white-label/persistence.py` before the confirmation gate. Route the final disposition through `persist_disposition()`; do not reproduce the config/DESIGN.md/audit write sequence inline.
- URL input — use `extract_from_url()` from `_parallax/white-label/extract/` so public-destination
validation, redirect checks, and bounded reads cannot be bypassed; do not fetch the URL with
`fetch-url` or any other network path
- PDF input — `load-reference` the file with a page range; read up to first 10 pages unless the brand guide is clearly deeper
- PPTX input — extract.extract_from_pptx() reads OOXML theme XML directly (precise colors/fonts) and aggregates slide text for voice corpus. Requires python-pptx
- DOCX input — extract.extract_from_docx() same pattern via word/theme/theme1.xml. Requires python-docx
- Folder input — when given a directory, run all .pptx/.docx/.pdf files inside it through their respective extractors and merge_drafts() the result before validation
- Voice extraction is LLM-driven (NOT regex) — the extract package only assembles the corpus. SKILL.md Step 2b prompts the model to fill the voice schema using the Lago 7-section template
- Voice corpus must be ≥500 words for credible extraction; ≥2000 words is recommended. VoiceValidator.validate_corpus_size enforces this with warn/fail. Below the floor, refuse to populate voice or surface a hard warning
- Wizard mode is triggered by invoking with no argument — guide intake via `ask-operator`, one prompt per group
- Confirmation gate is REQUIRED — config must not be written until the user explicitly confirms
- Validation warnings (warn/fail) never block the save — surface them and let the user decide
- Logo download: always download to assets/ during save so downstream PDF generation has local files; external URLs in config are a fallback reference only
- Audit entries must be written even for aborted sessions (extraction_attempt with disposition=rejected)
- If config.yaml exists and is corrupted or fails yaml.safe_load, warn and offer to overwrite; do not crash
- Pre-edit snapshot: when user chooses "Edit specific fields," serialize the pre-edit draft to .archive/ before applying edits — mirrors load-house-view Layer 2 pattern
- config.yaml permissions: 0600. assets/ directory: 0700. Parent directory ~/.parallax/client-branding/: 0700 on creation.
- Cross-validate extracted company name vs. source — extractor may misidentify which brand it found
- File paths in schema.yaml use tilde notation (~/.parallax/...); expand to absolute path at runtime
- Every host interaction is a host primitive from `parallax-conventions.md` §14 (bindings §14.2, fail-open §14.3). This skill makes no Parallax calls. Persistence fails closed: nothing is written without an explicit `ask-operator` confirmation this session.
Configure client branding for Parallax equity research report output.
## Usage
```
/parallax-white-label-onboard # wizard mode — guided intake via prompts
/parallax-white-label-onboard <https://client.com> # auto-extract from website
/parallax-white-label-onboard <path/to/brand-guide.pdf> # extract from PDF brand guide
/parallax-white-label-onboard <path/to/newsletter.pptx> # extract from PowerPoint (visual + voice)
/parallax-white-label-onboard <path/to/letter.docx> # extract from Word (visual + voice)
/parallax-white-label-onboard <path/to/client-collateral/> # extract from folder of mixed PPTX/DOCX/PDF
/parallax-white-label-onboard <url> <path/to/folder> # multi-source: cross-validate URL vs documents
/parallax-white-label-onboard --status # show active branding summary
/parallax-white-label-onboard --clear # remove active branding (requires confirmation)
```
**Multi-source priority.** When more than one source is supplied, visual fields with mismatches are surfaced at the confirmation gate (never auto-merged). Voice corpus is concatenated across all sources before voice extraction. The merged draft's `source.type` becomes `"multi"` with all components listed in `source.components`.
## Where branding lives
`~/.parallax/client-branding/`
- `config.yaml` — canonical config per `_parallax/white-label/schema.yaml`
- `DESIGN.md` — Google Labs DESIGN.md companion file emitted from the same draft (frontmatter tokens + 8-section markdown rationale); consumed by frontend coding-agent skills that want a recognized spec
- `assets/` — downloaded logo files (logo-primary.png, favicon.ico, etc.)
- `audit.jsonl` — append-only hash-chained log
- `.archive/<timestamp>-<source>/` — superseded configs kept for traceability
Files are written `0600`; `assets/` is `0700`; the directory is `0700` on creation.
## Required prerequisite: config validation
`jsonschema` validates `config.yaml` against the canonical schema on every load:
```
python3 -m pip install -r skills/_parallax/white-label/requirements.txt
```
Without it the loader does not crash and does not silently pass the config. It
returns `error="schema_unavailable"`, logs a warning naming the remedy, and
downstream reports render the branding marked "best-effort, schema unavailable".
That is a deliberate degradation, not a pass: a malformed or hand-edited config
is used **unchecked** until the package is installed. Install it for any real
deployment.
## Optional prerequisites
The DESIGN.md emit path is in-process and pure Python. The optional **DESIGN.md linter** wraps `npx @google/design.md` (Google Labs CLI, published to npm). When Node 18+ is on PATH, the validator runs the linter at save time and surfaces findings in the confirmation gate. When Node is absent, the validator returns `status: "skipped"` and the save proceeds — nothing breaks; the operator just doesn't get inline lint feedback.
If you want the inline lint feedback, install Node 18+ (e.g., `brew install node@20`) and run `/parallax-white-label-onboard <source>` once to warm the npm cache. To pin the version for reproducibility, run `npm install -g @google/design.md@<version>` before invoking the skill. The validator passes `-y` so first-call invocation does not block on the npm install prompt; this also means npx will silently fetch whatever the registry resolves for `@google/design.md` on a clean machine — pin the version in regulated environments.
## Integration with Downstream Skills
Two consumer classes: **visual** (PDF/report skills, read colors/logos/fonts) and **voice** (letter/newsletter/writing skills, read `voice.*`). Both fall back silently to defaults if config is absent.
Visual consumers call `loader.load_visual_branding()` (7-key visual subset). Voice consumers call `loader.load_client_branding()` (full 14-key shape including voice + v2 token tree + render defaults).
Currently integrated: **Tier 1** (`/parallax-cio-letter-prep`, `/parallax-client-review`, `/parallax-due-diligence`, `/parallax-deep-dive`) and **Tier 2** (`/parallax-should-i-buy`, `/parallax-thematic-screen`, `/parallax-portfolio-checkup`, `/parallax-portfolio-builder`, `/parallax-rebalance`, `/parallax-morning-brief`, `/parallax-explain-portfolio`, `/parallax-scenario-analysis`, `/parallax-country-deep-dive`, `/parallax-pair-finder`, `/parallax-peer-comparison`, `/parallax-macro-outlook`). New visual consumers must JIT-load `_parallax/white-label/integration-pattern.md` via the `<!-- white-label: integration-pattern.md -->` sentinel; the drift gate at `tests/test_integration_pattern_referenced.py` enforces sentinel ↔ load-directive pairing.
> Full loader API + visual/voice loading patterns + voice prompt-prepend template: see `references/integration-contract.md`.
## Workflow
Config-producer shape (authoring conventions, "Canonical step spine"): Steps 0–4, then `Step 5 — Confirm` and `Step 6 — Persist`. No Parallax calls; no render gate.
### Step 0 — Pre-flight
1. Resolve every `_parallax/...` path named in this file to the canonical copy (conventions §0.0 item 1).
2. `load-reference` `_parallax/white-label/schema.yaml`, `_parallax/white-label/extract/` (the extract package), `_parallax/white-label/validator.py`, and `_parallax/white-label/persistence.py` before executing any step below.
3. Bind the primitives this skill uses: `load-reference`, `ask-operator`, `run-shell` (the extract, validator, and persistence helpers run in Python; every write goes through `persist_disposition`), `read-config`. Without `ask-operator` or `run-shell` the skill can display a draft but cannot save; say so before Step 1.
<!-- host-note -->
Claude Code: `load-reference` = `Read` (use `pages` for PDFs); `ask-operator` = `AskUserQuestion`; `run-shell` = `Bash`.
<!-- /host-note -->
4. `read-config`: `~/.parallax/client-branding/` (create `0700` at save time if absent).
```python
from skills._parallax.white_label.extract import (
ColorExtractor, LogoExtractor, FontExtractor,
extract_from_url, extract_from_pdf, extract_from_wizard
)
from skills._parallax.white_label.validator import (
ColorValidator, LogoValidator, FontValidator
)
```
### Step 1 — Resolve inputs (detect input mode)
Examine each invocation argument (the skill accepts one or more):
| Condition | Mode |
|---|---|
| No argument | **Entry menu** (see below) |
| Argument starts with `http://` or `https://` | URL extraction |
| Argument is a file path ending in `.pdf` | PDF extraction |
| Argument is a file path ending in `.pptx` | PPTX extraction |
| Argument is a file path ending in `.docx` | DOCX extraction |
| Argument is an existing directory | Folder extraction (run all `.pptx`/`.docx`/`.pdf` inside) |
| Argument is `--status` | Show status block |
| Argument is `--clear` | Clear branding |
| Argument is `--regenerate-design-md` | Regenerate `DESIGN.md` from current `config.yaml` (no extraction; see `## Modes`) |
**Multi-source.** If two or more arguments resolve to extraction modes (any combination of URL, PDF, PPTX, DOCX, folder), run each through its respective extractor and call `extract.merge_drafts(drafts)` + `extract.cross_validate_visual(drafts)` before the validation step. Mismatches are stored in the draft for surfacing at Step 5.
For `--status` and `--clear`, jump directly to `## Modes`. For all extraction modes (single or multi-source), proceed to Step 2a.
If an argument does not match any row above (e.g., a plain ticker, a relative path with no extension, a file that does not exist), stop and ask: "I couldn't identify the input type for `<arg>`. Please provide a URL, a `.pdf`/`.pptx`/`.docx` path, a folder path, or run without arguments for the entry menu."
**Entry menu (no-argument invocation).** Do NOT jump straight into the manual color-hex wizard. The operator may not know that PPTX/DOCX or folder ingestion exists. First show the routing menu via `ask-operator`:
> How do you want to onboard this client's brand?
>
> - **Point me at their website** — I'll extract colors, fonts, logo, and writing voice from the homepage. Best when their site is the primary brand reference. *Recommended starting point.*
> - **Point me at a folder of their materials** — drop their newsletters / letters / decks (`.pptx`, `.docx`, `.pdf`) in a folder; I'll extract everything from the OOXML theme XML and aggregate the body text for voice analysis. Best for voice fidelity.
> - **Both — website AND folder** — I'll cross-validate visual fields between sources, flag mismatches for you to resolve, and combine the corpora for stronger voice extraction. *Best when both are available.*
> - **Manual wizard** — I'll prompt you for each value (colors, fonts, logo URL). Use only if you have no website and no document samples.
After the operator picks, ask the follow-up:
| Choice | Follow-up question |
|---|---|
| Website | "What's the URL?" — accept and re-route through Step 1 with the URL as argument. |
| Folder | "What's the folder path? (Absolute path; should contain `.pptx`, `.docx`, or `.pdf` files.)" — re-route through Step 0. |
| Both | Two questions: URL, then folder path. Re-route through Step 1 with both arguments. |
| Manual wizard | Proceed directly to Step 2a wizard mode. |
### Step 2 — Fetch (parallel batches)
No Parallax calls: the fetch is the extraction of the operator's sources. Folder files may be extracted together; everything else is one source at a time.
#### Step 2a — Extract brand assets
**Wizard mode:**
Use one `ask-operator` per numbered group below. Do not ask all fields in a single wall of text — group them logically. Confidence for all wizard-supplied values is 1.0 (the user is the source).
1. **Identity:** Client display name, contact name/email (optional, for audit log).
2. **Colors:** Primary brand color (hex), secondary color (hex), accent color (hex). For each, if the user provides a name like "navy blue" rather than a hex code, ask for the hex code explicitly or offer to pick a nearest standard hex.
3. **Background and text defaults:** Background color (hex, default `#FFFFFF`), default text color (hex, default `#333333`).
4. **Logos:** Primary logo (URL or local file path), favicon (URL or local file path, optional).
5. **Fonts:** Header font name, body font name, monospace font name (optional, default `Courier New`).
Assemble the `draft` dict from wizard answers. Skip any field the user leaves blank and flag it as missing with confidence 0.0.
**URL mode:** `draft = extract_from_url(url)`. If `draft` contains `"error"`, surface it: `"Extraction returned partial results due to: <error>. Review and edit before confirming."`
**PDF mode:** `draft = extract_from_pdf(pdf_path)`. Reads up to 10 pages by default. If the brand guide appears to be a dedicated multi-page document and extraction confidence is low (<0.6 average), offer to read more pages or switch to wizard mode.
**PPTX mode:** `draft = extract_from_pptx(pptx_path)`. Reads `ppt/theme/theme1.xml` directly, so colors and fonts come from the canonical OOXML theme declarations (confidence 0.9). Body text from every text frame is aggregated into `draft["voice_corpus"]` for the voice extraction step. The OOXML slot mapping is fixed: `accent1`→primary, `accent2`→secondary, `accent3`→accent, `dk1`→text, `lt1`→background. If the deck uses a heavily customized theme that overrides these slots inline, theme XML may understate the actual on-slide colors — flag this and offer wizard override.
**DOCX mode:** `draft = extract_from_docx(docx_path)`. Identical pattern via `word/theme/theme1.xml`. Body text from all paragraphs (skipping headers/footers) aggregated into `draft["voice_corpus"]`. Word's defaults are commonly Calibri (header) + Cambria (body); if the document inherits defaults rather than declaring custom fonts, the extracted values reflect the default theme — note this in `notes`.
**Folder mode.** Folder mode is NOT a blind iteration — it inventories, classifies by role (branded marketing / internal memo / compliance / transcript / ambiguous), confirms the classification with the operator, then extracts per classification. Decision points: ambiguous files are surfaced one-per-`ask-operator`; voice-only OOXML keeps voice and discards visual; 2+ branded drafts merge via `merge_drafts + cross_validate_visual`; voice-only folders seed `source.type = "folder-voice-only"`; the corpus is re-truncated at the 3000-word cap.
> Full folder procedure (Steps F-1 inventory → F-2 classification tables → F-3 operator gate → F-4 extract) and the three named voice frameworks (Lago / Rezvani / Genesys) it grounds on: see `references/folder-mode.md`. F-4 Python: `references/workflow-code.md` § Step 2a — Folder extraction.
**Multi-source mode (URL + folder, or any 2+ sources):** iterate `args`, dispatch each to its extractor, call `merge_drafts(drafts) + cross_validate_visual(drafts)` and store mismatches/agreements in `draft["multi_source"]`. Mismatches are NOT auto-resolved here — they surface in the Step 5 confirmation gate.
> Full Python (multi-source dispatch loop) and the full `draft` dict shape: see `references/workflow-code.md` § Step 2a — Multi-source extraction and § Draft structure after extraction.
Missing fields (empty dict values or absent keys) are acceptable — they will surface as warnings at Step 3 and can be filled at Step 5.
#### Step 2b — Voice extraction (only when corpus is available)
**Skip this step entirely if `draft["voice_corpus"]["word_count"] < 500`.** Below that floor, voice extraction is unreliable; set `draft["voice"] = {"enabled": False}` and continue to Step 3. Surface the gap to the user: `"Voice section not populated — corpus is only <N> words, below the 500-word minimum. Add more sample documents to enable voice extraction."`
**When corpus is sufficient,** drive voice extraction via in-skill prompting (no Python — this is LLM-native work). Read the corpus from `draft["voice_corpus"]["text"]`, prompt yourself with the Lago 7-section + Rezvani tone-matrix template, then write the resulting fields into `draft["voice"]`.
After extraction, set `draft["voice"]["enabled"] = True`, `draft["voice"]["source_corpus"]["word_count"] = N`, `draft["voice"]["source_corpus"]["documents"] = [list of source references]`, and `draft["voice"]["source_corpus"]["confidence"] = your_self_assessed_confidence_0_to_1`. Leave `drafted_vs_sent: []` — it is not auto-extractable (requires draft↔human-edited comparison) and is populated incrementally by downstream skills; note that in `voice.source_corpus.notes`.
> Full voice prompt template, the 3-item anti-boilerplate self-check, and the drafted-vs-sent rationale: see `references/voice-extraction.md`.
#### Step 2c — Completeness audit & supplement offer
Before validation, audit the draft for thinness and proactively offer to supplement. The operator may have given just a URL not realizing that adding a sample folder dramatically improves voice extraction; saving a thin config without surfacing the gap is a UX failure.
Audit checks: count brand colors, count fonts, check voice corpus word count, voice enabled flag, logo presence. Each thinness condition (colors_thin / fonts_missing / logo_missing / voice_corpus_thin / voice_corpus_low / voice_disabled_unexpectedly) appends a tuple to the `audit` list with a human-readable description.
> Full Python (audit checks): see `references/workflow-code.md` § Step 2c — Completeness audit.
**If any audit item fires AND the operator is in single-source mode**, surface them clearly via `ask-operator` BEFORE the confirmation gate:
> The extraction is thin in these areas:
> - <list of audit items, one per line, with the human-readable description>
>
> Do you want to supplement before saving?
>
> - **Add a folder of sample materials** — point me at a folder with their newsletters / letters / decks. I'll re-extract and merge. *Recommended if voice corpus is thin or fonts are missing.*
> - **Add their website URL** — only useful if you started with a folder. *Recommended if you want a sanity-check cross-source.*
> - **Fill the gaps manually in wizard mode** — I'll prompt you for the missing fields only.
> - **Save what I have** — I'll save the partial config; downstream consumers fall back to defaults for missing fields. The audit notes go into `metadata.notes`.
**If the operator chooses to supplement,** loop back to Step 1 with the additional source(s) and run multi-source extraction. If they choose "save what I have," append the audit list to `draft["metadata"]["notes"]` so the gap is durable in the saved config and the operator can re-run with more sources later.
**If the operator is already in multi-source mode** (e.g., URL + folder), skip the supplement offer — they've already given the skill its best shot. Surface the audit items in the confirmation-gate "MISSING FIELDS" block instead, so they're visible but don't block.
### Step 3 — Verify (validate assets)
Run all validators in parallel (no inter-dependency). Collect results into a `validation_results` dict. Validators:
- **Color contrast (WCAG AA)** — `ColorValidator.validate_text_contrast(text_hex, bg_hex)` → `{"status": "pass"|"warn"|"fail", "ratio": float, "recommendation": str|None}`
- **Individual hex format** — `ColorValidator.is_valid_hex(hex_val)` per color slot
- **Logo dimensions/format** — `LogoValidator.validate_logo(path)` for local logos only; URLs marked `"pending"` until Step 6 download
- **Font availability** — `FontValidator.validate_font(font_name)` per font slot
- **Voice section** — `VoiceValidator.validate_voice(draft.get("voice", {"enabled": False}))` → `{"status": "pass"|"warn"|"fail"|"skipped", "checks": {...}}`. `status: "skipped"` is the default when voice extraction was bypassed (visual-only sources) — informational, never an error. Do NOT block the save on a skipped voice section.
> Full code per validator: see `references/workflow-code.md` § Step 3 — Per-validator code blocks.
**Validation rules:**
- `fail` on: invalid hex format, logo file >5MB, logo format unsupported
- `warn` on: WCAG contrast 3.0–4.49, logo dimensions <200×200, font not on system
- `pass` on: all checks clear
Validation warnings and failures are informational only. They never block the save. Surface them clearly at Step 5 so the user can make an informed choice.
### Step 4 — Compute
Deterministic outputs the gate displays; no operator interaction here.
**Assemble validation summary table:**
```
Asset Check Status Note
─────────────────────────────────────────────────────────────────────
colors.primary hex format pass
colors.secondary hex format pass
colors.text+bg WCAG AA contrast warn 4.1:1 — large text only
logos.primary (URL — validate post-download)
fonts.header system availability warn "Montserrat" not found; fallback: Helvetica
fonts.body system availability pass
fonts.monospace system availability pass
```
Compute `avg_conf` and `lowest_field` from `draft["confidence_scores"]` for the Step 5 summary header. Generate the DESIGN.md preview via `emit_design_md(...)` (split on `---`, indent YAML) and its lint via `DesignMdValidator.lint(...)`; both are informational.
### Step 5 — Confirm (operator gate, REQUIRED before save)
**The gate is mandatory: config must NOT be written until the user explicitly confirms.** Render the draft display block (colors / logos / fonts / voice / multi-source mismatches / validation summary / DESIGN.md frontmatter preview + lint / missing fields), then ask via `ask-operator`:
> Confirm this branding configuration?
> - **Confirm and save** (Recommended if extraction looks right)
> - **Edit specific fields** — you'll be prompted for which fields to change
> - **Re-extract from different source** — provide a different URL or PDF path
> - **Abort** — abandon this session, no save
Decision points to preserve:
- **DESIGN.md preview + lint are informational** — generated via `emit_design_md(...)` (split on `---`, indent YAML) and `DesignMdValidator.lint(...)`; they never auto-block the save.
- **Mismatch resolution (multi-source):** if mismatches are present, the user MUST pick a winner per field via `ask-operator` (candidate values shown with source attribution). Collect `{field: source_reference}` choices and call `merge_resolved_drafts(drafts, resolutions)` before save. `drafts` is the per-source draft list: multi-source mode builds it directly; folder mode reads it from `draft["multi_source"]["component_drafts"]`, which `extract_from_folder` attaches whenever it records a mismatch. The helper refuses an unresolved or unknown source choice. Do NOT auto-pick by confidence or recency — the PM/CIO is canonical on which brand version is current.
- **Edit / Re-extract / Abort** each have their own disposition and audit handling: `edited` archives the pre-edit draft and hashes the pre-edit draft (pass `pre_edit_draft=`, and `edit_notes=` for the optional one-liner); `re_extracted` returns to Step 1; `rejected` writes no files; `confirmed` proceeds to Step 6.
> Full draft display template, color-swatch fallback, and the per-option procedures (Edit-fields 7-step / Re-extract / Abort / Confirm): see `references/confirmation-gate.md`.
### Step 6 — Persist
**Callable transaction boundary (required).** After Step 6b has prepared any
local logo paths in the draft, call `persist_disposition(draft, disposition=...,
branding_root=...)` from `_parallax/white-label/persistence.py`. It builds
`config.yaml` and `DESIGN.md` from the same draft, serializes concurrent writers,
stages the compatible hash-chained audit entry, and rolls all three live files
back if any replacement fails. For `rejected` and `re_extracted`, call the same
function before returning; it appends an `extraction_attempt` entry and cannot
activate the draft. The subsections below define inputs and artifact contracts;
they are not separate write operations.
#### Step 6a — Pre-write validation
Before any I/O:
- Verify `~/.parallax/client-branding/` is writable (create it with `os.makedirs(..., mode=0o700, exist_ok=True)` if absent).
- Verify the draft contains at minimum: one color (`branding.colors.primary`), or one logo, or one font. An entirely empty config is a user error — ask to confirm or abort.
- Validate all hex colors one final time via `ColorValidator.is_valid_hex()`. Reject any that fail (they should have been caught at Step 3, but this is a hard gate).
#### Step 6b — Download logos to local assets/
For each logo with a URL (not already a local path), download to `~/.parallax/client-branding/assets/` as `logo-primary.<ext>` / `favicon.<ext>`, then set `draft["logos"]["<role>"]["local_path"]`. On download failure: warn and preserve URL only. After successful downloads, re-run `LogoValidator.validate_logo(dest)` and append to the validation summary.
> Full code: see `references/workflow-code.md` § Step 6b — Download logos to local assets/.
#### Step 6c — Construct config.yaml
**Use `build_config_from_draft(draft, schema_version=2)` from `loader.py` — do not hand-assemble the config dict.** The builder is the single source of truth for the v2 shape (drops `fonts.*`, emits `colors.tertiary` and flat `colors.neutral`, wires `components.body-text`). It also handles the `voice` section and the `multi_source` provenance block — no post-build mutation needed.
v2 emits: `metadata`, `branding.{colors[primary|secondary|tertiary|neutral], logos, typography[h1..h5|body-md|code], rounded, spacing, components.body-text}`, `validation_summary`, `confidence_scores`. `fonts.*` is NOT in v2 — the loader bridges v1↔v2 at read time so consumers always see `fonts.{header,body,monospace}` derived from `typography.{h1,body-md,code}.fontFamily`.
Compute `config_hash = sha256(yaml.safe_dump(config["branding"], sort_keys=True).encode()).hexdigest()`.
> Full code (builder call + v2 yaml shape + v1 legacy fallback): see `references/workflow-code.md` § Step 6c — Construct config.yaml.
#### Step 6d — Persist the disposition (single transaction boundary)
Call `persist_disposition(draft, disposition=..., client_name=..., validation_summary=..., lint_status=...)`, adding `pre_edit_draft=` (required) and optional `edit_notes=` when the disposition is `edited`. One call archives any existing `config.yaml`, writes `config.yaml` and `DESIGN.md`, and appends the hash-chained audit entry — the three files are one transaction under a writer lock, so a failure restores the previous active state instead of leaving them describing different brands. Do not write them separately.
`confirmed` and `edited` activate and return `{"applied": True, "config_hash": ..., "design_md_hash": ..., "client_name": ..., "archive_dir": ...}`. An `edited` save hashes the pre-edit draft into the audit entry and archives it as `pre_edit.yaml` under `archive_dir`; omitting `pre_edit_draft` raises `ValueError`. `re_extracted` and `rejected` write no active artifact and return `{"applied": False, "disposition": ...}`.
**Every extraction attempt that does not result in a save also appends an audit entry** (`action: "extraction_attempt"`, `applied: false`) with `disposition`: `confirmed` / `edited` / `re_extracted` / `rejected`. This includes aborted sessions.
On failure the call raises `PersistenceError`; report it cleanly and treat the save as not applied. `RecoveryError` means an earlier interrupted save left a state this process cannot repair — surface it rather than retrying.
**Audit-entry schema bump (intentional chain discontinuity).** Save entries include `design_md_hash` and `lint_status`. The chain hash is `sha256(prior-line-bytes)`, so the first new-shape entry after a v1 audit log will appear as a chain break to any downstream verifier comparing entry shape across the bump. This is intentional: keep the prior chain readable for forensics but treat entries before this point as belonging to the v1 audit schema. If a verifier exists, gate it on `entry.get("design_md_hash") is not None` to detect schema-2 entries; absent that field, treat the entry as v1.
> Full code: see `references/workflow-code.md` § Step 6d — Persist the disposition.
## Modes
| Flag | Behavior |
|---|---|
| `--status` | Read `config.yaml`, render status block. If no config: print "No active client branding configured." |
| `--clear` | Ask for explicit confirmation ("Type YES to remove client branding"). On confirm: archive `config.yaml`, `DESIGN.md`, and `assets/` to `.archive/<timestamp>-clear/`, remove live files. Append `{"action":"clear"}` audit entry. |
| `--regenerate-design-md` | Re-emit `DESIGN.md` from the current `config.yaml` without re-extracting from source. See section below. |
> Status block output template: see `references/status-format.md`.
If `config.yaml` fails to load (YAML parse error, missing required keys), the `--status` mode shows: `"! Config corrupted or outdated. Re-run /parallax-white-label-onboard to reconfigure."`
### Regenerate-from-config mode (`--regenerate-design-md`)
Purpose: keep `DESIGN.md` in sync with `config.yaml` after manual edits, without re-running source extraction. Single source of truth: `config.yaml` is canonical; `DESIGN.md` is derived.
**What `--regenerate-design-md` preserves and recomputes:**
- **Preserved from config.yaml:** all color tokens, typography/font declarations, rounded/spacing values, logos, voice section (if present), `extracted_at` timestamp, source provenance, `multi_source` block.
- **Recomputed:** DESIGN.md frontmatter YAML and 8-section markdown body. The `design_md_hash` in the audit chain is updated to reflect the new output.
- **Not archived:** the prior `DESIGN.md` is overwritten without an `.archive/` snapshot. If you need to retain the old file, copy it manually before running this flag.
- **v1 config behavior:** `fonts.header`/`fonts.body`/`fonts.monospace` are used as fallbacks for `typography.h1`/`body-md`/`code` slots (confidence 0.5). No `typography.*` block is required in the config.
Implementation: load `config.yaml`, validate required keys (`branding.colors.primary` AND either `branding.typography.h1` OR `branding.fonts.header`), call `_config_to_draft(cfg)` from `loader.py` to reconstruct a synthetic draft, call `emit_design_md`, atomic-write to `DESIGN.md`, append `action: "regenerate_design_md"` audit entry.
> Full Python (validation + draft reconstruction + atomic write + audit entry): see `references/workflow-code.md` § Regenerate-from-config — Python block.
Hard errors (printed + return, no partial emit):
- `config.yaml` not present → "No active branding to regenerate from. Run /parallax-white-label-onboard first."
- `config.yaml` present but missing `branding.colors.primary` or both typography & fonts → "Cannot regenerate — config.yaml missing required keys: ...". Do not write a partial `DESIGN.md`.
Behavior on a v1 config (no `typography.*` block): emitter takes `fonts.header` / `fonts.body` / `fonts.monospace` as the `typography.h1.fontFamily` / `body-md.fontFamily` / `code.fontFamily` defaults (with confidence-neutral 0.5).
## Output Format
### Post-save summary
Output after a successful Step 6:
```
Branding saved.
client: <client_name>
source: <type>: <reference> (or "multi: N sources" when multi-source)
colors: <count of non-empty color slots> configured
logos: <count> downloaded to assets/
fonts: <count of non-empty font slots> configured
voice: <enabled: yes|no — corpus N words from M docs, register: ...>
warnings: <count of warn/fail checks> (see validation above)
Active in:
visual: client-review, due-diligence, deep-dive
voice: any skill that calls cfg["voice"] when enabled
Fallback: if branding load fails, downstream skills use default Parallax styling/voice.
Audit & inspect:
/parallax-white-label-onboard --status # active branding summary
/parallax-white-label-onboard --clear # remove branding
Try it:
/parallax-client-review <ticker> # generates a branded client report
```
For `--status`: `references/status-format.md`. For other modes, the requested success/failure message.
## Failure modes
- `ask-operator` absent, or no explicit confirmation this session: render the gate in prose and stop; nothing is written (fails closed, conventions §14.3).
- `run-shell` absent: extraction helpers and `persist_disposition` cannot run; say so at Step 0 and stop before Step 6.
- `PersistenceError`: report cleanly, treat the save as not applied. `RecoveryError`: surface it; do not retry.
- Logo download fails: warn, keep the URL reference only, continue.
- Voice corpus < 500 words: `voice.enabled = False`, surface the gap; never populate voice from a thin corpus.
- `config.yaml` corrupted: warn and offer to overwrite; never crash.
Edge-case handling (corrupted config, logo-download failure, empty color extraction, font-not-on-system, WCAG fail, unwritable directory, mid-way staging failure, >10-page PDF) and the full Success Criteria checklist (config/assets/audit assertions, voice-extracted vs not, multi-source) are catalogued in `references/edge-cases.md`. Consult it when a failure mode fires or before declaring a run successful.
## Done when
- The operator answered the Step 5 gate through `ask-operator`; every extraction attempt, including aborted ones, appended its audit entry through `persist_disposition`.
- On confirm: `config.yaml` and `DESIGN.md` written `0600` from the same draft, logos downloaded to `assets/` (`0700`), the audit entry appended; the post-save summary rendered.
- Multi-source mismatches were resolved by the operator, never by confidence or recency.
## See also
- `references/workflow-code.md` — full Python for Steps 2–6 + regenerate-from-config
- `references/folder-mode.md` — folder inventory/classification (F-1..F-4) + Lago/Rezvani/Genesys voice frameworks
- `references/voice-extraction.md` — Step 2b voice prompt template, self-check, drafted-vs-sent rationale
- `references/confirmation-gate.md` — Step 5 draft display template + Edit/Re-extract/Abort/Confirm procedures
- `references/edge-cases.md` — error handling, edge cases, and Success Criteria checklist
- `references/status-format.md` — `--status` output template
- `references/integration-contract.md` — visual + voice consumer loading patterns, voice prompt-prepend template
- `references/overview.md` — architecture, data flow, test inventory
- `references/installation.md` — setup, dependencies, troubleshooting
- `references/validation-rules.md` — color/logo/font validation reference
- `references/supported-fonts.md` — web-safe fonts + fallback chains
- `_parallax/white-label/integration-pattern.md` — canonical consumer-side contract for visual rendering (§1–§9)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!